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.
Files changed (159) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +39 -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 +601 -42
  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 +234 -57
  16. package/bin/lean/conformance.test.mjs +168 -7
  17. package/bin/lean/context.mjs +333 -0
  18. package/bin/lean/context.test.mjs +279 -0
  19. package/bin/lean/credentials.mjs +29 -0
  20. package/bin/lean/depcache.mjs +312 -0
  21. package/bin/lean/depcache.test.mjs +298 -0
  22. package/bin/lean/drift.mjs +198 -0
  23. package/bin/lean/drift.test.mjs +223 -0
  24. package/bin/lean/effects.mjs +104 -9
  25. package/bin/lean/execution.e2e.test.mjs +81 -0
  26. package/bin/lean/execution.mjs +431 -0
  27. package/bin/lean/execution.test.mjs +583 -0
  28. package/bin/lean/heavy.mjs +289 -0
  29. package/bin/lean/heavy.test.mjs +207 -0
  30. package/bin/lean/lean.mjs +723 -154
  31. package/bin/lean/lean.test.mjs +1603 -35
  32. package/bin/lean/ledger.mjs +27 -2
  33. package/bin/lean/ledger.test.mjs +36 -1
  34. package/bin/lean/limits.mjs +58 -0
  35. package/bin/lean/limits.test.mjs +150 -0
  36. package/bin/lean/orchestrate.mjs +389 -19
  37. package/bin/lean/prefilter.mjs +2 -2
  38. package/bin/lean/prefilter.test.mjs +3 -0
  39. package/bin/lean/prompts.mjs +106 -17
  40. package/bin/lean/recovery.test.mjs +230 -0
  41. package/bin/lean/refactor.mjs +276 -0
  42. package/bin/lean/refactor.test.mjs +359 -0
  43. package/bin/lean/render.mjs +49 -10
  44. package/bin/lean/review-trigger.mjs +134 -0
  45. package/bin/lean/review-trigger.test.mjs +104 -0
  46. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  47. package/bin/lean/sandbox-remote.mjs +142 -0
  48. package/bin/lean/sched.mjs +221 -0
  49. package/bin/lean/sched.test.mjs +310 -0
  50. package/bin/lean/schedd.mjs +247 -0
  51. package/bin/lean/schedd.test.mjs +180 -0
  52. package/bin/lean/testmap.mjs +84 -0
  53. package/bin/lean/testmap.test.mjs +58 -0
  54. package/bin/lean/toolchain.mjs +228 -0
  55. package/bin/lean/toolchain.test.mjs +154 -0
  56. package/bin/lean/verify.mjs +66 -10
  57. package/bin/lean/verify.test.mjs +104 -3
  58. package/bin/lean/worktree.mjs +69 -0
  59. package/bin/lean/worktree.test.mjs +141 -0
  60. package/bin/runner.mjs +53 -10
  61. package/bin/stats/github-source.mjs +1 -1
  62. package/bin/stats/index.mjs +24 -2
  63. package/bin/stats/local-source.mjs +13 -0
  64. package/bin/stats/query.mjs +99 -0
  65. package/bin/stats/stats.test.mjs +94 -1
  66. package/commands/audit.md +2 -0
  67. package/commands/autopilot.md +2 -0
  68. package/commands/bootstrap.md +35 -0
  69. package/commands/cleanup.md +7 -22
  70. package/commands/forge-stats.md +2 -1
  71. package/commands/forgedock-init.md +7 -0
  72. package/commands/issue.md +89 -830
  73. package/commands/milestone.md +55 -6
  74. package/commands/orchestrate.md +62 -8
  75. package/commands/upgrade-deps.md +3 -3
  76. package/commands/work-on/build/validate.md +4 -0
  77. package/commands/work-on/close.md +2 -0
  78. package/commands/work-on/decompose.md +24 -6
  79. package/docs/CI.md +3 -3
  80. package/docs/CONFIG.md +192 -2
  81. package/docs/OUTCOME-CODES.md +22 -0
  82. package/docs/RELEASE-NOTES.md +9 -0
  83. package/docs/TELEMETRY.md +1 -1
  84. package/docs/site/command-reference.md +22 -0
  85. package/docs/site/getting-started.md +2 -0
  86. package/docs/site/start-a-new-project.md +327 -0
  87. package/package.json +1 -1
  88. package/scripts/affected-files-check.sh +91 -0
  89. package/scripts/affected-files-check.test.sh +80 -0
  90. package/scripts/bootstrap-create.mjs +393 -0
  91. package/scripts/bootstrap-validate.mjs +36 -2
  92. package/scripts/calibration.mjs +116 -0
  93. package/scripts/check-devdocs-freshness.sh +9 -4
  94. package/scripts/check-devdocs-freshness.test.sh +11 -1
  95. package/scripts/check-spec-markers.sh +4 -0
  96. package/scripts/check-spec-size.sh +29 -11
  97. package/scripts/check-spec-size.test.sh +27 -4
  98. package/scripts/ci-local/lib.mjs +8 -1
  99. package/scripts/ci-local/parallelism.test.mjs +27 -0
  100. package/scripts/ci-local/run.mjs +3 -3
  101. package/scripts/ci-local/scoping.mjs +82 -0
  102. package/scripts/ci-local.test.sh +1 -1
  103. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  104. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  105. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  106. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  107. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  108. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  109. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  110. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  111. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  112. package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +23 -0
  113. package/scripts/fixtures/test-map/go/Makefile +5 -0
  114. package/scripts/fixtures/test-map/go/README.md +1 -0
  115. package/scripts/fixtures/test-map/go/go.mod +3 -0
  116. package/scripts/fixtures/test-map/go/internal/util/util.go +3 -0
  117. package/scripts/fixtures/test-map/go/main.go +3 -0
  118. package/scripts/fixtures/test-map/node/package.json +10 -0
  119. package/scripts/fixtures/test-map/node/packages/ui/package.json +4 -0
  120. package/scripts/fixtures/test-map/node/packages/ui/src/button.tsx +1 -0
  121. package/scripts/fixtures/test-map/node/pnpm-lock.yaml +1 -0
  122. package/scripts/fixtures/test-map/node/src/a.ts +1 -0
  123. package/scripts/fixtures/test-map/node/vitest.config.ts +1 -0
  124. package/scripts/fixtures/test-map/python/pkg/__init__.py +0 -0
  125. package/scripts/fixtures/test-map/python/pkg/mod.py +2 -0
  126. package/scripts/fixtures/test-map/python/pyproject.toml +9 -0
  127. package/scripts/fixtures/test-map/python/tests/test_mod.py +5 -0
  128. package/scripts/fixtures/test-map/python/uv.lock +1 -0
  129. package/scripts/forge-root.test.sh +1 -1
  130. package/scripts/hotspots.sh +100 -0
  131. package/scripts/hotspots.test.sh +101 -0
  132. package/scripts/issue-create.sh +197 -0
  133. package/scripts/issue-create.test.sh +166 -0
  134. package/scripts/maintenance-scan.sh +185 -0
  135. package/scripts/maintenance-scan.test.sh +89 -0
  136. package/scripts/milestone-drift.sh +206 -0
  137. package/scripts/milestone-drift.test.sh +120 -0
  138. package/scripts/refactor-verify.sh +368 -0
  139. package/scripts/refactor-verify.test.sh +202 -0
  140. package/scripts/repo-facts.sh +146 -0
  141. package/scripts/repo-facts.test.sh +106 -0
  142. package/scripts/structure-delta.sh +10 -5
  143. package/scripts/structure-delta.test.sh +3 -2
  144. package/scripts/symbol-index.mjs +253 -0
  145. package/scripts/symbol-index.py +354 -0
  146. package/scripts/symbol-index.test.sh +119 -0
  147. package/scripts/symbol-move-ts.test.sh +161 -0
  148. package/scripts/symbol-move.mjs +379 -0
  149. package/scripts/symbol-move.py +383 -0
  150. package/scripts/symbol-move.test.sh +168 -0
  151. package/scripts/test-map.sh +211 -0
  152. package/scripts/test-map.test.sh +165 -0
  153. package/scripts/wait-ci-green.sh +10 -4
  154. package/scripts/wait-ci-green.test.sh +9 -0
  155. package/templates/bootstrap/interview.md +34 -0
  156. package/templates/bootstrap/issue-body.md +42 -0
  157. package/templates/bootstrap/needs-human-issue.md +45 -0
  158. package/templates/bootstrap/stack-profiles.md +34 -0
  159. package/templates/bootstrap/technical-spike.md +75 -0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # test-map.sh — deterministic `file glob → check command` map for a repo (forge#4272). No LLM, no network, runs none of
6
+ # the repo's commands: git + jq + awk only (no yq). Codebase-agnostic: node, python, go, make and CI workflow steps.
7
+ #
8
+ # Usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]
9
+ # default output <repo>/.forge/test-map.json, rewritten on every run (a regenerable cache; deleting it restores the
10
+ # pre-PR step's `learned.test_commands` behaviour).
11
+ # Output: {"version":1,"entries":[{"globs":[...],"command","cwd","source"}]}, sorted by source then command; entries
12
+ # with the same (command, cwd) are merged (globs unioned, first source kept).
13
+ # Sources:
14
+ # package.json (root + every tracked one outside node_modules): scripts test/lint/typecheck/type-check/check →
15
+ # `<pm> run <script>` in the package dir (pm from the root lockfile), JS/TS globs under that dir
16
+ # pytest (pyproject [tool.pytest*], pytest.ini, setup.cfg [tool:pytest], conftest.py) and ruff config → `pytest`,
17
+ # `ruff check .` (through `uv run` / `poetry run` when that lockfile exists), `**/*.py`
18
+ # go.mod (dir D) → `go test ./...`, `go vet ./...` in D, `D/**/*.go`, `D/go.mod`, `D/go.sum`
19
+ # Makefile / GNUmakefile targets test/lint/check/typecheck/vet → `make <target>`, `**`
20
+ # .github/workflows/*.yml|*.yaml `run:` steps (one command per line of a `|` block, a `>` block is one command;
21
+ # install/setup lines skipped): globs
22
+ # from the tool (python/go/shell/js), else the workflow's `paths:` filter, else `**`. A step whose job id, job
23
+ # name, step name or command matches deploy/publish/release/push/secret is never mapped (forge#4297): the engine
24
+ # runs these commands before a PR, so only check steps belong here.
25
+ # Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
26
+ set -uo pipefail
27
+
28
+ die() { echo "test-map: $*" >&2; exit 2; }
29
+ command -v jq >/dev/null 2>&1 || die "jq is required"
30
+ REPO="."; OUT=""; STDOUT=0
31
+ while [ $# -gt 0 ]; do
32
+ case "$1" in
33
+ --repo-path) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; REPO="$2"; shift 2 ;;
34
+ --output) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; OUT="$2"; shift 2 ;;
35
+ --stdout) STDOUT=1; shift ;;
36
+ *) die "unknown option: $1" ;;
37
+ esac
38
+ done
39
+ [ -d "$REPO" ] || die "repo path not found: $REPO"
40
+ case "$OUT" in ""|/*) ;; *) OUT="$PWD/$OUT" ;; esac
41
+ cd "$REPO" || die "cannot enter $REPO"
42
+
43
+ T=$(mktemp -d "${TMPDIR:-/tmp}/test-map.XXXXXX") || die "mktemp failed"
44
+ trap 'rm -rf "$T"' EXIT
45
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
46
+ # -z and quotepath=off (#4298): non-ASCII and spaced paths come out verbatim, never C-quoted
47
+ git -c core.quotepath=off ls-files -z --cached --others --exclude-standard | tr '\0' '\n' > "$T/files"
48
+ else
49
+ find . -type f -not -path './.git/*' | sed 's|^\./||' | sort > "$T/files"
50
+ fi
51
+ : > "$T/rows"
52
+ # row: source TAB cwd TAB command TAB globs separated by US (\037, #4298: a glob may contain spaces)
53
+ US="$(printf '\037')"
54
+ row() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" >> "$T/rows"; }
55
+ JS_EXT="js jsx ts tsx mjs cjs"
56
+ # globs for the extensions in $2 under dir $1 ("." = repo root)
57
+ ext_globs() {
58
+ g=""; pre=""; [ "$1" = "." ] || pre="$1/"
59
+ for e in $2; do g="$g$US${pre}**/*.$e"; done
60
+ printf '%s' "${g#"$US"}"
61
+ }
62
+
63
+ # ---- package.json scripts ----
64
+ PM=npm
65
+ [ -f yarn.lock ] && PM=yarn
66
+ [ -f pnpm-lock.yaml ] && PM=pnpm
67
+ grep -E '(^|/)package\.json$' "$T/files" | grep -v -E '(^|/)node_modules/' | sort | while IFS= read -r pj; do
68
+ [ -f "$pj" ] || continue
69
+ d=$(dirname "$pj")
70
+ globs="$(ext_globs "$d" "$JS_EXT")$US$pj"
71
+ tag=""
72
+ if [ -n "$(ls "$d"/vitest.config.* "$d"/jest.config.* 2>/dev/null)" ] || jq -e 'has("jest")' "$pj" >/dev/null 2>&1; then tag=" (vitest/jest config)"; fi
73
+ jq -r '(.scripts // {}) | to_entries[] | select(.value | type == "string") | .key' "$pj" 2>/dev/null | while IFS= read -r s; do
74
+ case "$s" in
75
+ test) row "$pj#scripts.$s$tag" "$d" "$PM run $s" "$globs" ;;
76
+ lint|typecheck|type-check|check) row "$pj#scripts.$s" "$d" "$PM run $s" "$globs" ;;
77
+ esac
78
+ done
79
+ done
80
+
81
+ # ---- python ----
82
+ RUN=""
83
+ [ -f poetry.lock ] && RUN="poetry run "
84
+ [ -f uv.lock ] && RUN="uv run "
85
+ PY_SRC=""
86
+ if [ -f pyproject.toml ] && grep -qE '^\[tool\.pytest' pyproject.toml; then PY_SRC="pyproject.toml#tool.pytest"
87
+ elif [ -f pytest.ini ]; then PY_SRC="pytest.ini"
88
+ elif [ -f setup.cfg ] && grep -qE '^\[tool:pytest\]' setup.cfg; then PY_SRC="setup.cfg#tool:pytest"
89
+ elif grep -qE '(^|/)conftest\.py$' "$T/files"; then PY_SRC="conftest.py"; fi
90
+ [ -n "$PY_SRC" ] && row "$PY_SRC" "." "${RUN}pytest" "**/*.py$US**/conftest.py"
91
+ RUFF_SRC=""
92
+ if [ -f pyproject.toml ] && grep -qE '^\[tool\.ruff' pyproject.toml; then RUFF_SRC="pyproject.toml#tool.ruff"
93
+ elif [ -f ruff.toml ]; then RUFF_SRC="ruff.toml"
94
+ elif [ -f .ruff.toml ]; then RUFF_SRC=".ruff.toml"; fi
95
+ [ -n "$RUFF_SRC" ] && row "$RUFF_SRC" "." "${RUN}ruff check ." "**/*.py"
96
+
97
+ # ---- go ----
98
+ grep -E '(^|/)go\.mod$' "$T/files" | grep -v -E '(^|/)vendor/' | sort | while IFS= read -r gm; do
99
+ d=$(dirname "$gm"); pre=""; [ "$d" = "." ] || pre="$d/"
100
+ globs="${pre}**/*.go$US${pre}go.mod$US${pre}go.sum"
101
+ row "$gm" "$d" "go test ./..." "$globs"
102
+ row "$gm" "$d" "go vet ./..." "$globs"
103
+ done
104
+
105
+ # ---- Makefile ----
106
+ for mf in Makefile GNUmakefile; do
107
+ [ -f "$mf" ] || continue
108
+ grep -oE '^(test|lint|check|typecheck|vet)[[:space:]]*:' "$mf" | sed -E 's/[[:space:]]*:$//' | sort -u | while IFS= read -r t; do
109
+ row "$mf#$t" "." "make $t" "**"
110
+ done
111
+ done
112
+
113
+ # ---- CI workflow run: steps ----
114
+ # Emits: job US jobname US stepname US command (US = \037, so empty names survive `read`), one per command line; and P TAB path for each `paths:` filter item
115
+ # before `jobs:`. A block ends at the first non-blank line not indented past the `run` key (sibling keys such as env:).
116
+ wf_extract() {
117
+ awk '
118
+ function indent(s) { match(s, /^ */); return RLENGTH }
119
+ function unq(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); if (s ~ /^".*"$/ || s ~ /^\x27.*\x27$/) s = substr(s, 2, length(s) - 2); return s }
120
+ function emit(c) { c = unq(c); if (c != "") print job "\037" jname "\037" sname "\037" c }
121
+ BEGIN { injobs = 0; jind = -1; block = 0; inpaths = 0; job = ""; jname = ""; sname = "" }
122
+ {
123
+ line = $0; sub(/\r$/, "", line)
124
+ if (block) {
125
+ if (line ~ /^[ \t]*$/) next
126
+ if (indent(line) > bind) {
127
+ l = line; sub(/^ +/, "", l)
128
+ if (fold) { pend = (pend == "" ? l : pend " " l); next }
129
+ if (pend != "") { l = pend " " l; pend = "" }
130
+ if (l ~ /\\$/) { pend = substr(l, 1, length(l) - 1); sub(/[ \t]+$/, "", pend); next }
131
+ emit(l); next
132
+ }
133
+ if (pend != "") { emit(pend); pend = "" }
134
+ block = 0
135
+ }
136
+ if (line ~ /^jobs:[ \t]*$/) { injobs = 1; inpaths = 0; next }
137
+ if (!injobs) {
138
+ if (line ~ /^[ \t]*paths:[ \t]*$/) { inpaths = 1; pind = indent(line); next }
139
+ if (inpaths) {
140
+ if (line ~ /^[ \t]*-[ \t]/ && indent(line) >= pind) { p = line; sub(/^[ \t]*-[ \t]*/, "", p); sub(/[ \t]+#.*$/, "", p); print "P\t" unq(p); next }
141
+ if (line !~ /^[ \t]*$/ && line !~ /^[ \t]*#/) inpaths = 0
142
+ }
143
+ next
144
+ }
145
+ if (line ~ /^[^ \t#]/) { injobs = 0; next }
146
+ if (line ~ /^ +[A-Za-z0-9_-]+:[ \t]*$/ && (jind < 0 || indent(line) == jind)) { jind = indent(line); job = line; sub(/^ +/, "", job); sub(/:.*$/, "", job); jname = ""; sname = ""; next }
147
+ if (line ~ /^[ \t]*-[ \t]/) sname = ""
148
+ if (line ~ /^[ \t]*(-[ \t]+)?name:/) {
149
+ nm = line; sub(/^[ \t]*(-[ \t]+)?name:[ \t]*/, "", nm); sub(/[ \t]+#.*$/, "", nm)
150
+ if (line !~ /^[ \t]*-[ \t]/ && indent(line) == jind + 2) jname = unq(nm); else sname = unq(nm)
151
+ next
152
+ }
153
+ if (line ~ /^[ \t]*(-[ \t]+)?run:/) {
154
+ rest = line; sub(/^[ \t]*(-[ \t]+)?run:[ \t]*/, "", rest); sub(/[ \t]+#.*$/, "", rest)
155
+ # bind = the column of the `run` key: step keys after it (env:, shell:) sit there and end the block
156
+ if (rest ~ /^[|>][-+0-9]*$/) { block = 1; pend = ""; fold = (rest ~ /^>/); bind = match(line, /run:/) - 1; next }
157
+ emit(rest)
158
+ }
159
+ }
160
+ END { if (block && pend != "") emit(pend) }
161
+ ' "$1"
162
+ }
163
+ skip_cmd() {
164
+ case "$1" in
165
+ \#*|echo\ *|echo|cd\ *|export\ *|set\ *|if\ *|then|fi|else|for\ *|do|done|\}*|\{*|*'$GITHUB_'*|*'>> $GITHUB'*) return 0 ;;
166
+ npm\ ci*|npm\ install*|npm\ i|npm\ i\ *|pnpm\ install*|pnpm\ i|yarn|yarn\ install*|pip\ install*|pip3\ install*|python*\ -m\ pip\ install*|uv\ sync*|uv\ pip\ install*|poetry\ install*|go\ mod\ download*|apt-get*|sudo\ *|corepack\ *|brew\ *) return 0 ;;
167
+ esac
168
+ return 1
169
+ }
170
+ # forge#4297: deploy, publish, release, push and secret-using steps are never check commands
171
+ deploy_step() {
172
+ grep -qiE 'deploy|publish|release|push|secret' <<<"$*"
173
+ }
174
+ tool_globs() {
175
+ c=" $1 "
176
+ case "$c" in
177
+ *[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
178
+ *[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
179
+ *[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
180
+ *[\ /]eslint\ *|*[\ /]tsc\ *|*[\ /]prettier\ *|*[\ /]vitest\ *|*[\ /]jest\ *|*\ node\ --test*|*\ npm\ *|*\ pnpm\ *|*\ yarn\ *|*\ npx\ *) ext_globs . "$JS_EXT"; return ;;
181
+ esac
182
+ printf ''
183
+ }
184
+ grep -E '^\.github/workflows/[^/]+\.(yml|yaml)$' "$T/files" | sort | while IFS= read -r wf; do
185
+ [ -f "$wf" ] || continue
186
+ wf_extract "$wf" > "$T/wf"
187
+ paths=$(awk -F'\t' '$1 == "P" { printf "%s\037", $2 }' "$T/wf"); paths=${paths%"$US"}
188
+ awk -F'\t' '$1 != "P"' "$T/wf" | while IFS="$(printf '\037')" read -r job jname sname cmd; do
189
+ skip_cmd "$cmd" && continue
190
+ deploy_step "$job" "$jname" "$sname" "$cmd" && continue
191
+ g=$(tool_globs "$cmd")
192
+ [ -n "$g" ] || g="$paths"
193
+ [ -n "$g" ] || g="**"
194
+ row "$wf#jobs.$job" "." "$cmd" "$g"
195
+ done
196
+ done
197
+
198
+ MAP=$(jq -R -s -c '
199
+ [split("\n")[] | select(length > 0) | split("\t") | {source: .[0], cwd: .[1], command: .[2], globs: (.[3] | split("\u001f") | map(select(length > 0)))}]
200
+ | sort_by(.source, .command)
201
+ | reduce .[] as $e ({order: [], by: {}};
202
+ ([$e.command, $e.cwd] | tojson) as $k
203
+ | if .by[$k] then .by[$k].globs += ($e.globs - .by[$k].globs)
204
+ else .order += [$k] | .by[$k] = {globs: $e.globs, command: $e.command, cwd: $e.cwd, source: $e.source} end)
205
+ | {version: 1, entries: [.by[.order[]]]}' < "$T/rows") || die "jq failed"
206
+
207
+ if [ "$STDOUT" = 1 ]; then printf '%s\n' "$MAP"; exit 0; fi
208
+ [ -n "$OUT" ] || OUT=".forge/test-map.json"
209
+ mkdir -p "$(dirname "$OUT")" || die "cannot create $(dirname "$OUT")"
210
+ printf '%s\n' "$MAP" > "$OUT.tmp.$$" && mv "$OUT.tmp.$$" "$OUT" || die "cannot write $OUT"
211
+ echo "$OUT"
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env bash
2
+ # test-map.test.sh — cases for scripts/test-map.sh over the node, python and go fixtures (forge#4272).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ set -uo pipefail
6
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ S="$HERE/test-map.sh"
8
+ SEL="$HERE/../bin/lean/testmap.mjs"
9
+ FIX="$HERE/fixtures/test-map"
10
+ PASS=0; FAILN=0
11
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/test-map-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
12
+ ok() { PASS=$((PASS+1)); }
13
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
14
+ eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
15
+
16
+ # copy a fixture into a fresh git repo and build its map
17
+ setup() {
18
+ rm -rf "$TMP/$1"; cp -R "$FIX/$1" "$TMP/$1"; cd "$TMP/$1" || exit 1
19
+ git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
20
+ git add -A; git commit -qm init
21
+ OUTP="$(bash "$S")"; RC=$?
22
+ }
23
+ # selected commands for changed files, sorted, joined by |
24
+ sel() { node "$SEL" --map .forge/test-map.json --select "$@" | LC_ALL=C sort | paste -sd'|' -; }
25
+
26
+ # ---- 1. node: root scripts for root files, workspace scripts for package files ----
27
+ setup node
28
+ eq "node rc" "$RC" 0
29
+ eq "node output path" "$OUTP" ".forge/test-map.json"
30
+ eq "schema" "$(jq -c '[.version, (.entries | length > 0), (.entries | all(has("globs") and has("command") and has("cwd") and has("source")))]' .forge/test-map.json)" '[1,true,true]'
31
+ eq "non-check script ignored" "$(jq '[.entries[] | select(.command | test("build"))] | length' .forge/test-map.json)" 0
32
+ eq "root file" "$(sel src/a.ts)" "pnpm run lint|pnpm run test"
33
+ eq "package file" "$(sel packages/ui/src/button.tsx)" "cd packages/ui && pnpm run test|cd packages/ui && pnpm run typecheck|pnpm run lint|pnpm run test"
34
+ eq "vitest config noted" "$(jq -r '.entries[] | select(.cwd == "." and .command == "pnpm run test") | .source' .forge/test-map.json)" "package.json#scripts.test (vitest/jest config)"
35
+ eq "unrelated file" "$(sel README.md)" ""
36
+
37
+ # ---- 2. python: pytest + ruff through uv ----
38
+ setup python
39
+ eq "python rc" "$RC" 0
40
+ eq "python file" "$(sel pkg/mod.py)" "uv run pytest|uv run ruff check ."
41
+ eq "python test file" "$(sel tests/test_mod.py)" "uv run pytest|uv run ruff check ."
42
+ eq "python non-py" "$(sel uv.lock)" ""
43
+
44
+ # ---- 3. go: go.mod, Makefile and CI workflow steps; CI-only lint step; install lines skipped ----
45
+ setup go
46
+ eq "go rc" "$RC" 0
47
+ eq "go file" "$(sel main.go)" "./scripts/check-docs|go test ./...|go vet ./...|golangci-lint run ./...|make test"
48
+ eq "nested go file" "$(sel internal/util/util.go)" "./scripts/check-docs|go test ./...|go vet ./...|golangci-lint run ./...|make test"
49
+ eq "ci-only lint source" "$(jq -r '.entries[] | select(.command == "golangci-lint run ./...") | .source' .forge/test-map.json)" ".github/workflows/ci.yml#jobs.lint"
50
+ eq "install line skipped" "$(jq '[.entries[] | select(.command | test("mod download"))] | length' .forge/test-map.json)" 0
51
+ eq "paths filter fallback" "$(jq -c '.entries[] | select(.command == "./scripts/check-docs") | .globs' .forge/test-map.json)" '["**.go","go.mod"]'
52
+ eq "docs file" "$(sel README.md)" "make test"
53
+ eq "go.sum" "$(sel go.sum)" "go test ./...|go vet ./...|make test"
54
+
55
+ # ---- 4. regenerable: a rerun is byte-identical; --stdout writes nothing; --output ----
56
+ cp .forge/test-map.json "$TMP/first.json"
57
+ bash "$S" >/dev/null
58
+ if cmp -s .forge/test-map.json "$TMP/first.json"; then ok; else bad "rerun not byte-identical"; fi
59
+ rm -rf .forge; OUTS="$(bash "$S" --stdout)"
60
+ eq "stdout same map" "$OUTS" "$(cat "$TMP/first.json")"
61
+ eq "stdout writes nothing" "$([ -e .forge ] && echo yes || echo no)" no
62
+ bash "$S" --repo-path "$TMP/go" --output "$TMP/out/map.json" >/dev/null
63
+ if cmp -s "$TMP/out/map.json" "$TMP/first.json"; then ok; else bad "--output differs"; fi
64
+
65
+ # ---- 5. block runs, continuation lines, setup lines, workflow without paths ----
66
+ mkdir -p "$TMP/wf/.github/workflows"; cd "$TMP/wf" || exit 1
67
+ git init -q -b main
68
+ cat > .github/workflows/lint.yaml <<'EOF'
69
+ on: push
70
+ jobs:
71
+ checks:
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - run: npm ci
75
+ - run: "shellcheck scripts/*.sh"
76
+ - run: >-
77
+ ruff check
78
+ --fix
79
+ - run: |
80
+ echo hi
81
+ bash scripts/verify.sh \
82
+ --strict
83
+ ./custom-check
84
+ EOF
85
+ git add -A
86
+ OUTS="$(bash "$S" --stdout)"
87
+ eq "wf commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "./custom-check|bash scripts/verify.sh --strict|ruff check --fix|shellcheck scripts/*.sh"
88
+ eq "wf globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | {(.command): .globs}] | add')" '{"./custom-check":["**"],"bash scripts/verify.sh --strict":["**/*.sh"],"ruff check --fix":["**/*.py"],"shellcheck scripts/*.sh":["**/*.sh"]}'
89
+
90
+ # ---- 5b. forge#4297: `- run: |` then env:/shell: ends the block; deploy/publish/release/push/secret steps dropped ----
91
+ mkdir -p "$TMP/wf2/.github/workflows"; cd "$TMP/wf2" || exit 1
92
+ git init -q -b main
93
+ cat > .github/workflows/ci.yml <<'EOF'
94
+ on:
95
+ push:
96
+ branches: [main]
97
+ jobs:
98
+ test:
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - run: |
102
+ npm test
103
+ env:
104
+ CI: true
105
+ shell: bash
106
+ - name: Lint
107
+ run: |
108
+ npm run lint
109
+ working-directory: .
110
+ - name: Publish to npm
111
+ run: npm run ship
112
+ - run: ./scripts/release.sh
113
+ - run: git push origin HEAD
114
+ - run: curl -H "token ${{ secrets.TOKEN }}" https://x
115
+ - run: >-
116
+ tsc
117
+ --noEmit
118
+ shell: bash
119
+ deploy:
120
+ name: Ship it
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ - run: ./ship.sh
124
+ site:
125
+ name: Deploy site
126
+ runs-on: ubuntu-latest
127
+ steps:
128
+ - run: ./build-site.sh
129
+ EOF
130
+ git add -A
131
+ OUTS="$(bash "$S" --stdout)"
132
+ eq "wf2 commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "npm run lint|npm test|tsc --noEmit"
133
+
134
+ # ---- 5c. forge#4298: a package dir with a space and a non-ASCII name keeps whole globs; workflow paths with spaces ----
135
+ mkdir -p "$TMP/sp/web app" "$TMP/sp/pkg/ü/src" "$TMP/sp/.github/workflows"; cd "$TMP/sp" || exit 1
136
+ git init -q -b main
137
+ echo '{"name":"root","private":true}' > package.json
138
+ echo '{"name":"web","scripts":{"test":"vitest run"}}' > "web app/package.json"
139
+ echo '{"name":"u","scripts":{"lint":"eslint ."}}' > "pkg/ü/package.json"
140
+ echo 'x' > "pkg/ü/src/a.ts"
141
+ cat > .github/workflows/ci.yml <<'EOF'
142
+ on:
143
+ push:
144
+ paths:
145
+ - "docs site/**"
146
+ jobs:
147
+ docs:
148
+ runs-on: ubuntu-latest
149
+ steps:
150
+ - run: ./build-docs.sh
151
+ EOF
152
+ git add -A
153
+ OUTS="$(bash "$S" --stdout)"
154
+ eq "spaced package globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.cwd == "web app") | .globs[]] | map(select(endswith("package.json") or endswith(".js")))')" '["web app/**/*.js","web app/package.json"]'
155
+ eq "non-ASCII package dir" "$(printf '%s' "$OUTS" | jq -r '[.entries[] | select(.command == "npm run lint") | .cwd] | join("|")')" "pkg/ü"
156
+ eq "spaced workflow path filter" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.command == "./build-docs.sh") | .globs]')" '[["docs site/**"]]'
157
+ printf '%s' "$OUTS" > "$TMP/sp-map.json"
158
+ eq "spaced changed file selects its package test" "$(node "$SEL" --map "$TMP/sp-map.json" --select "web app/src/x.js")" "cd 'web app' && npm run test"
159
+
160
+ # ---- 6. usage ----
161
+ bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
162
+ bash "$S" --nope >/dev/null 2>&1; eq "unknown option" "$?" 2
163
+
164
+ echo "test-map.test.sh: $PASS passed, $FAILN failed"
165
+ [ "$FAILN" -eq 0 ]
@@ -4,7 +4,11 @@
4
4
  #
5
5
  # wait-ci-green.sh — CI gate every autonomous merge must pass first.
6
6
  #
7
- # Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS]
7
+ # Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS] [--ignore-check NAME]...
8
+ #
9
+ # --ignore-check NAME (repeatable): drop checks with exactly that name before deciding. `forgedock/verify` (forge#4270,
10
+ # the lean engine's report-only gate summary) is always ignored, so every caller of this gate treats it the same
11
+ # (forge#4298); requiring it is the repository's branch-protection choice.
8
12
  #
9
13
  # Waits until every check on the PR has finished, then decides. Fails closed: a merge may
10
14
  # proceed ONLY on exit 0. Field test: pipeline agents merged PRs to staging while checks were
@@ -40,17 +44,18 @@ set -uo pipefail
40
44
  _TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
41
45
  type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
42
46
 
43
- PR=""; REPO=""
47
+ PR=""; REPO=""; IGNORE='["forgedock/verify"]'
44
48
  TIMEOUT="${FORGE_CI_TIMEOUT:-540}"; INTERVAL="${FORGE_CI_INTERVAL:-20}"; SETTLE="${FORGE_CI_SETTLE:-45}"
45
49
  GRACE="${FORGE_CI_NO_CHECKS_GRACE:-120}"; REQUIRE="${FORGE_CI_REQUIRE_CHECKS:-0}"
46
- usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S]" >&2; exit 2; }
50
+ usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S] [--ignore-check NAME]..." >&2; exit 2; }
47
51
  while [ $# -gt 0 ]; do
48
52
  case "$1" in
49
53
  -R) [ $# -ge 2 ] || usage; REPO="$2"; shift 2 ;;
50
54
  -R*) REPO="${1#-R}"; REPO="${REPO# }"; shift ;;
51
55
  --timeout) [ $# -ge 2 ] || usage; TIMEOUT="$2"; shift 2 ;;
52
56
  --interval) [ $# -ge 2 ] || usage; INTERVAL="$2"; shift 2 ;;
53
- -h|--help) sed -n '5,32p' "$0"; exit 0 ;;
57
+ --ignore-check) [ $# -ge 2 ] || usage; IGNORE="$(jq -c --arg n "$2" '. + [$n]' <<<"$IGNORE")"; shift 2 ;;
58
+ -h|--help) sed -n '5,37p' "$0"; exit 0 ;;
54
59
  *) [ -z "$PR" ] || usage; PR="$1"; shift ;;
55
60
  esac
56
61
  done
@@ -132,6 +137,7 @@ while :; do
132
137
  fi
133
138
  fi
134
139
  errors=0; limited=0; transient=0
140
+ [ "$IGNORE" = '[]' ] || JSON="$(printf '%s' "$JSON" | jq -c --argjson ig "$IGNORE" '[.[] | select(.name as $n | $ig | index($n) | not)]')"
135
141
  cur="$(head_sha)"
136
142
  if [ -n "$cur" ] && [ "$cur" != "$START_SHA" ]; then emit ERROR; echo "PR head moved during the wait ($START_SHA -> $cur); re-review the new head"; exit 2; fi
137
143
  total=$(printf '%s' "$JSON" | jq 'length')
@@ -55,6 +55,15 @@ expect "pending then pass" "0|CI_GATE: PASS" "$(run "$(mk pend '[{"name":"a","bu
55
55
  expect "pending then fail" "1|CI_GATE: FAIL" "$(run "$(mk pendfail '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"fail"}]')")"
56
56
  expect "pending past timeout" "3|CI_GATE: TIMEOUT" "$(run "$(mk slow '[{"name":"a","bucket":"pending"}]')" --timeout 2)"
57
57
  expect "fail wins over pending" "1|CI_GATE: FAIL" "$(run "$(mk mixed '[{"name":"a","bucket":"pending"},{"name":"b","bucket":"fail"}]')")"
58
+ # forge#4270: --ignore-check drops the named check (the lane's own report-only forgedock/verify) before deciding.
59
+ OWN='[{"name":"a","bucket":"pass"},{"name":"forgedock/verify","bucket":"fail"}]'
60
+ # forge#4298: forgedock/verify is ignored by default, so every caller (legacy engine, review-finalize, spec gates) agrees.
61
+ expect "own verify check failing is ignored by default" "0|CI_GATE: PASS" "$(run "$(mk ownfail "$OWN")")"
62
+ expect "default ignore keeps other failures" "1|CI_GATE: FAIL" "$(run "$(mk owndef '[{"name":"b","bucket":"fail"},{"name":"forgedock/verify","bucket":"fail"}]')")"
63
+ expect "usage text lists --ignore-check" "1" "$(bash "$GATE" 2>&1 >/dev/null | grep -c -- '--ignore-check NAME')"
64
+ expect "--ignore-check drops the named failing check" "0|CI_GATE: PASS" "$(run "$(mk ownign "$OWN")" --ignore-check forgedock/verify)"
65
+ expect "--ignore-check keeps other failures" "1|CI_GATE: FAIL" "$(run "$(mk ownother '[{"name":"b","bucket":"fail"},{"name":"forgedock/verify","bucket":"pass"}]')" --ignore-check forgedock/verify)"
66
+ expect "--ignore-check without a name is a usage error" "2|CI_GATE: ERROR" "$(run "$(mk ownusage "$P")" --ignore-check)"
58
67
  expect "transient outage then pass" "0|CI_GATE: PASS" "$(run "$(mk blip ERR "$P")")"
59
68
  expect "persistent unclassified failure is an ERROR" "2|CI_GATE: ERROR" "$(run "$(mk down ERRX)" --timeout 60)"
60
69
  # Rate-limit / 5xx classification: retried, never an ERROR; a window that only saw them ends RATE_LIMITED (rc 4).
@@ -0,0 +1,34 @@
1
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
+ # /bootstrap interview question bank
4
+
5
+ Ask conversationally and keep it short: skip any aspect the user's free text or docs already answer. Every aspect left
6
+ unanswered gets the **Default** and its rationale, tied to what the user did say, in the "Here's what I assumed" summary.
7
+ Aspects marked **Doubt** change architecture or cost. When the answer is unclear, ask them as multiple-choice questions
8
+ (at most 4 per batch, each with a recommended option). All other aspects default silently.
9
+
10
+ | # | Aspect | Question | Default (rationale pattern) | Doubt? |
11
+ |---|--------|----------|-----------------------------|--------|
12
+ | 1 | Users and segments | Who uses it, and which segments differ in what they need? | One primary segment from the pitch; admins as a second role (most products need an operator view) | no |
13
+ | 2 | Core jobs and features | What must a user be able to do on day one? | The jobs named in the pitch, each as a feature with its happy path | no |
14
+ | 3 | Scope | MVP first, or the full product? | MVP, full product as later milestones (cheapest path to feedback) | yes, when the pitch lists many features |
15
+ | 4 | Platforms | Web, mobile, desktop? | Responsive web (one codebase covers phones) | yes: mobile native vs PWA |
16
+ | 5 | Stack | Any language, framework or hosting preferences? | The closest preset in `stack-profiles.md` | no, unless the user names conflicting stacks |
17
+ | 6 | Auth | How do users sign in? | Email magic link + Google sign-in (lowest friction, no password storage) | no |
18
+ | 7 | Data and tenancy | Is data shared between organisations? | Postgres + row-level security for multi-tenant SaaS; single-tenant otherwise | yes: self-host vs managed DB |
19
+ | 8 | Integrations | Which external services? | A mock provider per credentialed integration so dev and CI stay green | no |
20
+ | 9 | Payments | Do you charge, and how? | Stripe subscriptions behind a mock in dev (most common, test mode is free) | yes, when pricing is unclear |
21
+ | 10 | i18n | Which languages and locales? | English with i18n-ready strings (cheap now, expensive later) | no |
22
+ | 11 | Compliance and residency | Any regulation (GDPR, HIPAA, DPDP) or data-residency rule? | Privacy by default (minimal PII, encrypted at rest); region of the primary market | yes: data residency |
23
+ | 12 | Design ambition and UI libraries | Polished brand or functional? Any UI library? | Accessible component library (e.g. shadcn/ui) plus a design-token theme; hero surfaces go to `/design` | no |
24
+ | 13 | Deployment target | VPS, Cloudflare or a cloud provider? | The stack preset's target with a staging-branch flow (staging -> main) | yes: when the cost profile differs |
25
+ | 14 | Scale | Users and data in year one? | Small (hundreds of users): one region, one DB, a scale path in the spike | no |
26
+ | 15 | Budget | Monthly infra budget? | Free or hobby tiers until launch | no |
27
+ | 16 | Team | Who builds and reviews? | A solo builder with ForgeDock agents | no |
28
+ | 17 | Timeline | Any hard date? | None: milestones in build order | no |
29
+
30
+ Always a doubt: the **product name** and the **repo owner** (user or org; the owner must be covered by the ForgeDock
31
+ GitHub App installation).
32
+
33
+ Guardrails and domain rules (e.g. "no auto-send", "India-only data residency") go into the plan's `guardrails` and
34
+ `domainRules`. bootstrap-create.mjs copies them into every issue's Context.
@@ -0,0 +1,42 @@
1
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
+ <!-- /bootstrap build-issue body (Pipeline Issue Template). Copy from "## Problem" down; drop the UI block for
4
+ non-UI issues and the hero line for non-hero issues. Plan flags: "ui": true needs the full design spec, "hero": true
5
+ needs the /design line (scripts/bootstrap-validate.mjs). Reference other plan issues as #{{KEY}}. -->
6
+ ## Problem
7
+
8
+ {what this issue delivers and for whom, one paragraph}
9
+
10
+ ## Root Cause (if known)
11
+
12
+ Greenfield: not built yet. {spike section this implements}
13
+
14
+ ## Affected Files
15
+
16
+ 1. `{planned/path/file.ext}` — new
17
+ 2. `{planned/path/other.ext}` — new
18
+
19
+ ## Acceptance Criteria
20
+
21
+ - [ ] {observable behaviour} [type:unit]
22
+ - [ ] {API or integration behaviour} [type:api]
23
+ - [ ] {user-visible flow} [type:e2e]
24
+
25
+ ### Design spec (UI issues)
26
+
27
+ - **Breakpoints**: {mobile 360, tablet 768, desktop 1280: layout at each}
28
+ - **States**: empty state {copy and action}; loading state {skeleton}; error state {message and retry}; offline state {cached view or banner}
29
+ - **Motion**: {transitions and durations; respects prefers-reduced-motion}
30
+ - **Accessibility**: {WCAG 2.2 AA: focus order, labels, contrast, keyboard paths}
31
+ - **Dark mode**: {token mapping; verified in both themes}
32
+ - **Hero surface**: route the visual design to `/design` before building this page.
33
+
34
+ ## Context
35
+
36
+ **Parent**: #{{SPIKE}}
37
+ Spike: `docs/engineering/technical-spike.md` § {section}
38
+ {real credentials: see #{{NEEDS_HUMAN_KEY}}; until then the mock provider is used, so this issue never depends on it}
39
+
40
+ ## Dependencies
41
+
42
+ Depends on #{{DEP_KEY}} — {why}
@@ -0,0 +1,45 @@
1
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
+ <!-- /bootstrap needs-human issue: one per credential, account or external input (OAuth client, cloud or DNS account,
4
+ payment keys, API keys, legal review). Label it needs-human and set "needsHuman": true in the plan. Code issues
5
+ reference it (#{{KEY}}) but never list it under Dependencies; their mocks keep dev and CI green. Copy from "## Problem" down. -->
6
+ ## Problem
7
+
8
+ {which credential, account or external input is needed, and which issues use it}
9
+
10
+ ### Setup steps
11
+
12
+ 1. {exact console URL and the clicks to create it}
13
+ 2. {settings to choose: redirect URIs, scopes, region}
14
+ 3. {store the values as deploy secrets (repository or environment secrets)}
15
+
16
+ ### Environment variables
17
+
18
+ - `{SERVICE_CLIENT_ID}` — {what it is}
19
+ - `{SERVICE_CLIENT_SECRET}` — {what it is}
20
+
21
+ ### Secret handling
22
+
23
+ Never commit these values or paste them into issues. Store them only as deploy/CI secrets and a local untracked
24
+ `.env`. Rotate them if exposed. Until they exist, dev and CI use the mock provider.
25
+
26
+ ## Root Cause (if known)
27
+
28
+ Greenfield: an external input agents cannot create.
29
+
30
+ ## Affected Files
31
+
32
+ 1. `docs/setup/{service}.md` — new
33
+
34
+ ## Acceptance Criteria
35
+
36
+ - [ ] Credentials created and stored as deploy secrets [type:manual]
37
+ - [ ] The real provider replaces the mock in staging [type:manual]
38
+
39
+ ## Context
40
+
41
+ **Parent**: #{{SPIKE}}
42
+
43
+ ## Dependencies
44
+
45
+ None.
@@ -0,0 +1,34 @@
1
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
+ # /bootstrap stack-profile presets
4
+
5
+ Pick the preset closest to the interview and record the choice and its rationale in the spike's Stack section. A
6
+ preset only fills blanks: anything the user said wins. Domain labels go into the plan's `labels`.
7
+
8
+ ## Next.js + shadcn SaaS
9
+ - **Stack**: TypeScript, Next.js (App Router), shadcn/ui + Tailwind, Postgres (Prisma or Drizzle) with row-level security, Auth.js.
10
+ - **Repo layout**: `apps/web/`, `packages/db/`, `packages/ui/`, `packages/config/`, `docs/engineering/`.
11
+ - **Deployment**: Vercel or a VPS with Docker; managed Postgres; preview deployments per branch.
12
+ - **Quality gates**: `tsc --noEmit`, ESLint, Vitest, Playwright smoke test, Lighthouse budget on hero pages.
13
+ - **Domain labels**: `frontend`, `backend`, `infra`, `design`.
14
+
15
+ ## Python API
16
+ - **Stack**: Python 3.12, FastAPI, SQLAlchemy + Alembic, Postgres, Pydantic settings, uv.
17
+ - **Repo layout**: `src/<app>/api/`, `src/<app>/models/`, `src/<app>/services/`, `migrations/`, `tests/`, `docs/engineering/`.
18
+ - **Deployment**: a container on a VPS or Cloud Run; managed Postgres.
19
+ - **Quality gates**: ruff, mypy, pytest with coverage, an OpenAPI schema diff check.
20
+ - **Domain labels**: `backend`, `infra`, `api`.
21
+
22
+ ## Mobile (Expo / React Native)
23
+ - **Stack**: TypeScript, Expo + React Native, Expo Router, a backend from one of the presets above (or Supabase).
24
+ - **Repo layout**: `apps/mobile/`, `apps/api/` (when a custom backend exists), `packages/shared/`, `docs/engineering/`.
25
+ - **Deployment**: EAS Build and Submit; OTA updates for JS-only changes.
26
+ - **Quality gates**: `tsc --noEmit`, ESLint, Jest, a Maestro or Detox smoke flow.
27
+ - **Domain labels**: `mobile`, `backend`, `infra`, `design`.
28
+
29
+ ## Go service
30
+ - **Stack**: Go, chi or net/http, sqlc + Postgres, Docker.
31
+ - **Repo layout**: `cmd/<app>/`, `internal/`, `migrations/`, `docs/engineering/`.
32
+ - **Deployment**: a single static binary in a container on a VPS or Fly.io.
33
+ - **Quality gates**: `go vet`, golangci-lint, `go test -race`.
34
+ - **Domain labels**: `backend`, `infra`.