mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* fix(retro): keep retro history in the state root, shared across worktrees (#3749) /retro saved snapshots under .context/retros inside the working tree, so a run in a linked worktree saw none of the main checkout's history, reported "First retro recorded", and wrote a snapshot that git worktree remove deletes. gstack-retro-metrics --history [--next <date>] now resolves the history directory as <state root>/projects/<slug>/retros (the slug is keyed on the git common dir, so every worktree shares it), moves any legacy .context/retros from this worktree and the main checkout into it, lists prior snapshots and prints the next snapshot path. Step 12/13 and the gbrain prior-retros query use it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gate-diff): read a bare base as origin/<base> in gate-diff and diff-scope (#3650) gstack-gate-diff and gstack-diff-scope resolved a bare base such as main to the local branch whenever it shared history with HEAD. A local main that is behind origin/main (a linked worktree, or a Capy clone, where local main never moves) then set the merge base, so /ship's gate scan and specialist scope also read commits already upstream. Both now prefer origin/<base> when it resolves, the base /ship fetches and merges against. Fixes #3650 Co-authored-by: exGeni <135786749+exGeni@users.noreply.github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(retro): make history migration safe against symlinks and concurrent runs (#3749 review) Review pass 1 findings: a .context/retros that resolves outside its worktree (or to the shared history itself) is left alone, so migration can no longer delete the history it points at; moves are no-clobber with retry; a copy identical to any same-date snapshot collapses; RETRO_FILE is reserved as {} so overlapping runs in two worktrees never share a path; --next must be a YYYY-MM-DD date; a missing option value no longer loops forever. Step 12 names RETRO_HISTORY_ERROR instead of reading it as a stale helper. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): fragment for the #3749 retro history fix Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (diff-scope reads slash-named bases through origin too) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(retro): trim Step 12/13 prose back under the parity skeleton cap Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: Step 11 review fixes (a fork branch, a colliding tag or another remote never replaces the base) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(retro): keep today="<today>" in Step 12 so the judge excerpt keeps both date inputs Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): describe the fork, tag and remote guards (#3650) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(retro): compare #3749 history paths with forward slashes so the Windows shard passes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review): review-plan and gate-diff plan the same with a stale local main or a shadowing tag (GST-883, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): gate-diff and review-plan resolve a branch base to origin/<base> by full ref name; review-plan scans the merge base its own diff used (GST-883) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-883 fragment Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review-risk): hostile-input time bounds for every risk pattern; the R51 rm and rmSync rules fail (GST-898) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-risk): the R51 rm and rmSync rules run in linear time (GST-898) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review-packets-removed): repin lib/review-risk.ts on the GST-898 verdict-identity replay Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-risk): padding cannot push recursive: true out of the rmSync window; repin on the GST-898 no-loss replay Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-898 fragment wording Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review): planner misreads: SVG chart as runtime, session data as auth, bare base in diff-scope and receipt depth (GST-911, GST-884, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): a drawing-only SVG is data and a scripted one is security; session data files are not auth; diff-scope and the receipt depth read origin/<base> by full ref name (GST-911, GST-884) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): the SVG drawing check refuses CSS escapes and reads zero-padded character references; the security class reads a file's changed SVG lines joined (GST-911 review pass 1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review): a stray quote cannot hide an external href in a drawing SVG (GST-911 review pass 2, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): the SVG drawing check reads every name= without consuming its value, so a stray quote cannot hide an external href (GST-911 review pass 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review): CSS in a drawing SVG holds only declarations and url(#id): image-set(), image(), element() keep it code (GST-911 Step 11, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): a drawing SVG's CSS holds declarations only, no @ rule and no function but url(#id), which closes image-set(), image(), element() and every other CSS load (GST-911 Step 11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(review): a style element's CSS is read past comments and CDATA, unclosed style tags are refused in linear time, and every drawing verdict holds at 1 MB and 8 MB (GST-911, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review): walk a style element's CSS with indexOf, dropping comments and reading CDATA, so no hidden </style> ends it early and unclosed style tags refuse in linear time (GST-911) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(review-policy): the linear-time claim covers R51's rm and rmSync rules (GST-898) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(review-policy): state the rmSync padding rule exactly (GST-898) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-depth): linear url() refusal in svgDrawing (wave review-2, red-team CRITICAL) SVG_REFUSED's url() entry backtracked quadratically on url( followed by a long whitespace run (80k spaces: 820 ms; 1 MB: over 230 s), and svgDrawing runs in collectLive and in collectCommit, which ship-queue ready, train build and land call with no timeout. The rewritten entry refuses exactly what #3838's pattern refused. Repin lib/review-depth.ts under no-loss replay GST-911-redos (verdict identity: 60,000 seeded strings, the edges and every SVG blob in history, zero differences). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (wave review-2, pass 1) - bin/gstack-diff-scope: _resolve_base takes a related local <base> before another remote's twin, the order resolveBase uses, so the two tools pick the same base for a bare name when origin/<base> is unrelated. - lib/review-depth.ts: a drawing's DOCTYPE names no document but W3C's SVG DTD; SYSTEM and other PUBLIC identifiers keep the SVG code. Repin lib/review-depth.ts under no-loss replay GST-911-redos (the DOCTYPE change only keeps more SVGs code, so it drops no reviewer). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: freeze the exported SVG_REFUSED list (wave review-2, Step 11) Exported only so test/review-risk-redos.test.ts can reach the url() entry; a frozen readonly copy keeps any importer from changing verdicts while DEPTH_RULES still reports the original rules. Repin lib/review-depth.ts under no-loss replay GST-911-redos (no pattern changed). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "fix: freeze the exported SVG_REFUSED list (wave review-2, Step 11)" The fix-check (wave review-2) showed Object.freeze is shallow: each RegExp can still be changed in place, so the freeze did not close adversarial-native#1, and it moved the tree past the one the Step 11 voices reviewed. The finding ships as an open note (its reviewer rated it Low, not blocking; no production caller mutates the list). Restores the lib/review-depth.ts pin under no-loss replay GST-911-redos. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): wave review-2 entry for the integrator fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(review-policy): a drawing's DOCTYPE names no outside document (wave review-2 docs audit) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> Co-authored-by: exGeni <135786749+exGeni@users.noreply.github.com>
299 lines
13 KiB
Bash
Executable File
299 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-diff-scope — categorize what changed in the diff against a base branch
|
|
# Usage: source <(gstack-diff-scope main) → sets SCOPE_FRONTEND=true SCOPE_BACKEND=false ...
|
|
# Or: gstack-diff-scope main → prints SCOPE_*=... lines
|
|
# Or: gstack-diff-scope <base> --head <sha> → the committed range <base>...<sha> only
|
|
# (no working tree, no untracked files): what a receipt writer and the
|
|
# merge queue compute for one commit, from any checkout (GSTA-147).
|
|
#
|
|
# Output contract (#2526 — all-false must be distinguishable from "we could
|
|
# not look" and from "nothing matched"):
|
|
# exit 0 changed-file set empty → all false, legitimately nothing
|
|
# exit 0 changed files, >=1 category match → flags
|
|
# exit 2 changed files, ZERO matches → flags + SCOPE_ERROR=unmatched
|
|
# (+ the unmatched paths as comment lines, so a new top-level layout
|
|
# trips loudly instead of silently disabling reviewers)
|
|
# exit 2 base ref unresolvable → all false + SCOPE_ERROR=no_base
|
|
# (shallow CI checkout / missing fetch — a green here would mean
|
|
# "we could not look")
|
|
# Every line is shell-safe for `source <(...)` consumers: assignments or
|
|
# `#`-comments only.
|
|
#
|
|
# The changed-file set is the UNION of committed diff + working tree +
|
|
# untracked files (#2299): /ship detects scope in Step 9, BEFORE it commits in
|
|
# Step 15, so uncommitted work must be visible or every scope-gated reviewer
|
|
# is skipped on the common start-work-then-ship flow.
|
|
set -euo pipefail
|
|
|
|
# Detect the repo's default branch when no arg is given (#703-class
|
|
# platform-agnostic rule): origin/HEAD -> origin/main -> origin/master -> main.
|
|
_default_base() {
|
|
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' && return
|
|
git rev-parse --verify -q origin/main >/dev/null 2>&1 && { echo "main"; return; }
|
|
git rev-parse --verify -q origin/master >/dev/null 2>&1 && { echo "master"; return; }
|
|
echo "main"
|
|
}
|
|
BASE="${1:-$(_default_base)}"
|
|
HEAD_REF=""
|
|
if [ "${2:-}" = "--head" ]; then HEAD_REF="${3:-}"; fi
|
|
# A bare branch name reads origin's tracking branch, by its full ref, when that
|
|
# shares history with HEAD: /ship merges against origin/<base>, and a linked
|
|
# worktree or a Capy clone never moves the local branch, so a stale one widens
|
|
# the scope (#3650). A remote-qualified name is taken as given.
|
|
_NOT_BRANCH='^[0-9a-f]{7,64}$|[~^]|@\{|^[A-Z_]*HEAD$|^refs/'
|
|
_QUALIFIED=false
|
|
for _remote in $(git remote 2>/dev/null); do
|
|
case "$BASE" in "$_remote"/*) _QUALIFIED=true ;; esac
|
|
done
|
|
if [[ ! "$BASE" =~ $_NOT_BRANCH ]] && [ "$_QUALIFIED" = false ] \
|
|
&& git rev-parse --verify -q "refs/remotes/origin/${BASE}^{commit}" >/dev/null 2>&1 \
|
|
&& git merge-base "refs/remotes/origin/${BASE}" "${HEAD_REF:-HEAD}" >/dev/null 2>&1; then
|
|
BASE="refs/remotes/origin/${BASE}"
|
|
fi
|
|
|
|
# The base as a commit (GST-884, mirroring lib/gate-diff/git.ts resolveBase from
|
|
# GST-883): a branch name reads refs/remotes/<base> (origin/main as given), then
|
|
# refs/remotes/origin/<base> that shares history with the head, then refs/heads/<base>
|
|
# that does, then another remote's twin that does, then refs/heads/<base> as it is,
|
|
# all by full ref name and in resolveBase's order, so a stale local main never widens
|
|
# the scope and a tag named main or origin/main cannot stand in for the branch.
|
|
# A sha, a relative ref (HEAD~1, main^2, @{u}) or a full refs/ name resolves as
|
|
# given. Prints nothing when nothing resolves.
|
|
_is_branch_name() {
|
|
case "$1" in refs/*|*~*|*^*|*@\{*|HEAD|*_HEAD) return 1 ;; esac
|
|
[[ ! "$1" =~ ^[0-9a-f]{7,64}$ ]]
|
|
}
|
|
_commit() { git rev-parse --verify -q "$1^{commit}" 2>/dev/null; }
|
|
_resolve_base() {
|
|
local ref="$1" head="$2" sha loc remote
|
|
if ! _is_branch_name "$ref"; then _commit "$ref"; return; fi
|
|
sha=$(_commit "refs/remotes/$ref") && { echo "$sha"; return; }
|
|
loc=$(_commit "refs/heads/$ref")
|
|
for remote in $(git remote 2>/dev/null | grep -x origin) ${loc:+:local} $(git remote 2>/dev/null | grep -vx origin); do
|
|
if [ "$remote" = :local ]; then sha=$loc; else sha=$(_commit "refs/remotes/$remote/$ref") || continue; fi
|
|
git merge-base "$sha" "$head" >/dev/null 2>&1 && { echo "$sha"; return; }
|
|
done
|
|
[ -n "$loc" ] && { echo "$loc"; return; }
|
|
_commit "$ref"
|
|
}
|
|
|
|
FRONTEND=false
|
|
BACKEND=false
|
|
PROMPTS=false
|
|
TESTS=false
|
|
DOCS=false
|
|
CONFIG=false
|
|
MIGRATIONS=false
|
|
API=false
|
|
AUTH=false
|
|
|
|
# Test-file paths, the same rule the test-bootstrap resolver counts as
|
|
# TESTFILES (scripts/resolvers/testing.ts; test/diff-scope.test.ts keeps the two
|
|
# identical). Nested test dirs and Python's test_*.py / tests.py were missed by
|
|
# the root-anchored globs alone (#3335).
|
|
TEST_PATH_RE='(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$'
|
|
|
|
# Data files (lib/review-depth.ts DATA_PATH) outside the config locations its
|
|
# DATA_CONFIG and agent-prose rules keep code: .github/, config/, dotfiles,
|
|
# *.config.*, .gstack/. Case-insensitive, as DATA_PATH is.
|
|
DATA_FILE_RE='\.(json|jsonl|ndjson|csv|tsv|png|jpe?g|gif|webp|bmp|ico|avif)$'
|
|
DATA_CONFIG_RE='(^|/)\.github/|(^|/)config/|(^|/)\.[^/]+$|(^|/)[^/]+\.config\.[^/]+$|^\.gstack/'
|
|
_session_data() {
|
|
local r=1
|
|
shopt -s nocasematch
|
|
if [[ "$1" =~ $DATA_FILE_RE ]] && [[ ! "$1" =~ $DATA_CONFIG_RE ]]; then r=0; fi
|
|
shopt -u nocasematch
|
|
return "$r"
|
|
}
|
|
|
|
_print_flags() {
|
|
echo "SCOPE_FRONTEND=$FRONTEND"
|
|
echo "SCOPE_BACKEND=$BACKEND"
|
|
echo "SCOPE_PROMPTS=$PROMPTS"
|
|
echo "SCOPE_TESTS=$TESTS"
|
|
echo "SCOPE_DOCS=$DOCS"
|
|
echo "SCOPE_CONFIG=$CONFIG"
|
|
echo "SCOPE_MIGRATIONS=$MIGRATIONS"
|
|
echo "SCOPE_API=$API"
|
|
echo "SCOPE_AUTH=$AUTH"
|
|
}
|
|
|
|
# Base reachability (#2526): a shallow CI checkout or an unfetched ref makes
|
|
# `git diff` return an empty list — all-false with exit 0, a green that means
|
|
# "we could not look". Distinguish it before diffing.
|
|
BASE_SHA=$(_resolve_base "$BASE" "${HEAD_REF:-HEAD}" || true)
|
|
if [ -z "$BASE_SHA" ]; then
|
|
_print_flags
|
|
echo "SCOPE_ERROR=no_base"
|
|
echo "# base ref '${BASE}' is not resolvable — shallow checkout or missing fetch. Run: git fetch origin ${BASE}"
|
|
exit 2
|
|
fi
|
|
|
|
# Changed files, NUL-delimited (#2526 minor: `git diff --name-only` octal-quotes
|
|
# non-ASCII paths, and the trailing quote defeats extension globs; -z avoids it).
|
|
FILES_LIST=()
|
|
_collect() {
|
|
local f
|
|
while IFS= read -r -d '' f; do
|
|
[ -n "$f" ] && FILES_LIST+=("$f")
|
|
done
|
|
}
|
|
if [ -n "$HEAD_REF" ] || [ "${2:-}" = "--head" ]; then
|
|
if ! git rev-parse --verify -q "${HEAD_REF}^{commit}" >/dev/null 2>&1; then
|
|
_print_flags
|
|
echo "SCOPE_ERROR=no_head"
|
|
echo "# --head '${HEAD_REF}' is not a commit in this repository"
|
|
exit 2
|
|
fi
|
|
if ! git merge-base "${BASE_SHA}" "${HEAD_REF}" >/dev/null 2>&1; then
|
|
_print_flags
|
|
echo "SCOPE_ERROR=no_merge_base"
|
|
echo "# '${BASE}' and '${HEAD_REF}' share no merge base"
|
|
exit 2
|
|
fi
|
|
_collect < <(git diff -z "${BASE_SHA}...${HEAD_REF}" --name-only 2>/dev/null)
|
|
else
|
|
# Committed diff vs base (merge-base form; two-dot fallback when no merge base).
|
|
_collect < <(git diff -z "${BASE_SHA}...HEAD" --name-only 2>/dev/null || git diff -z "${BASE_SHA}" --name-only 2>/dev/null || true)
|
|
# Working-tree changes (staged + unstaged). `git diff HEAD` fails on a repo
|
|
# with no commits; tolerated.
|
|
_collect < <(git diff -z HEAD --name-only 2>/dev/null || true)
|
|
# Untracked files: a brand-new component/migration/test is exactly what a
|
|
# reviewer should see, and /ship commits it in Step 15 regardless.
|
|
_collect < <(git ls-files -z --others --exclude-standard 2>/dev/null || true)
|
|
fi
|
|
|
|
if [ "${#FILES_LIST[@]}" -eq 0 ]; then
|
|
_print_flags
|
|
exit 0
|
|
fi
|
|
|
|
UNMATCHED=()
|
|
|
|
# Categories are INDEPENDENT booleans (#2299): a single first-match-wins case
|
|
# made them mutually exclusive, so Button.test.jsx set FRONTEND but not TESTS
|
|
# while util.test.ts set TESTS but not BACKEND — same intent, opposite result,
|
|
# purely from arm ordering. Each category now gets its own case; only BACKEND
|
|
# stays deliberately exclusive of frontend component/view files.
|
|
for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
|
|
m_frontend=false; m_prompts=false; m_tests=false; m_docs=false
|
|
m_config=false; m_migrations=false; m_api=false; m_auth=false; m_backend=false
|
|
|
|
# Frontend: CSS, views, components, templates
|
|
case "$f" in
|
|
*.css|*.scss|*.less|*.sass|*.pcss) m_frontend=true ;;
|
|
*.tsx|*.jsx|*.vue|*.svelte|*.astro) m_frontend=true ;;
|
|
*.erb|*.haml|*.slim|*.hbs|*.ejs) m_frontend=true ;;
|
|
*.html) m_frontend=true ;;
|
|
# Dart: the extension cannot tell a Flutter widget from a repository or a
|
|
# pure-Dart server, so it is BOTH frontend and backend (BACKEND set below).
|
|
# A skipped reviewer is invisible; an extra one (design-lite included) is a
|
|
# visible pass that costs a review.
|
|
*.dart) m_frontend=true ;;
|
|
tailwind.config.*|postcss.config.*) m_frontend=true ;;
|
|
app/views/*|*/components/*|styles/*|css/*|app/assets/stylesheets/*) m_frontend=true ;;
|
|
esac
|
|
|
|
# Prompts: prompt builders, system prompts, generation services
|
|
case "$f" in
|
|
*prompt_builder*|*generation_service*|*writer_service*|*designer_service*) m_prompts=true ;;
|
|
*evaluator*|*scorer*|*classifier_service*|*analyzer*) m_prompts=true ;;
|
|
*voice*.rb|*writing*.rb|*prompt*.rb|*token*.rb) m_prompts=true ;;
|
|
app/services/chat_tools/*|app/services/x_thread_tools/*) m_prompts=true ;;
|
|
config/system_prompts/*) m_prompts=true ;;
|
|
esac
|
|
|
|
# Tests
|
|
case "$f" in
|
|
*.test.*|*.spec.*|*_test.*|*_spec.*) m_tests=true ;;
|
|
test/*|tests/*|spec/*|__tests__/*|cypress/*|e2e/*) m_tests=true ;;
|
|
conftest.py|*/conftest.py) m_tests=true ;;
|
|
esac
|
|
if [[ "$f" =~ $TEST_PATH_RE ]]; then m_tests=true; fi
|
|
|
|
# Docs
|
|
case "$f" in
|
|
*.md) m_docs=true ;;
|
|
esac
|
|
|
|
# Config
|
|
case "$f" in
|
|
package.json|package-lock.json|yarn.lock|bun.lock|bun.lockb) m_config=true ;;
|
|
Gemfile|Gemfile.lock) m_config=true ;;
|
|
*.yml|*.yaml) m_config=true ;;
|
|
.github/*) m_config=true ;;
|
|
requirements.txt|pyproject.toml|go.mod|Cargo.toml|composer.json) m_config=true ;;
|
|
pubspec.lock|*/pubspec.lock) m_config=true ;;
|
|
esac
|
|
|
|
# Migrations: database migration files. Bare migrations/* covers a
|
|
# root-level migrations dir (#2526); db/data covers the Rails data_migrate
|
|
# gem's DATA migrations (#2455) — arbitrary Ruby run unattended against
|
|
# production data, strictly higher-risk than a schema migration (they also
|
|
# match BACKEND below via their extension, as ordinary app code should).
|
|
case "$f" in
|
|
db/migrate/*|migrations/*|*/migrations/*|alembic/*|prisma/migrations/*) m_migrations=true ;;
|
|
db/data/*|data_migrations/*|*/data_migrations/*) m_migrations=true ;;
|
|
esac
|
|
|
|
# API: routes, controllers, endpoints, GraphQL/OpenAPI schemas. Bare api/*
|
|
# covers root-level serverless layouts (Vercel functions, Next.js pages/api
|
|
# at root) that */api/* silently missed (#2526).
|
|
case "$f" in
|
|
api/*|*/api/*|*controller*|*route*|*endpoint*) m_api=true ;;
|
|
*.graphql|*.gql|openapi.*|swagger.*) m_api=true ;;
|
|
esac
|
|
|
|
# Auth: authentication, authorization, sessions, permissions. A data file
|
|
# named for sessions (recovered-sessions.jsonl, GST-911) holds rows, not
|
|
# session handling, so it is not auth; session config still is.
|
|
case "$f" in
|
|
*auth*|*jwt*|*oauth*|*permission*|*role*) m_auth=true ;;
|
|
*session*) _session_data "$f" || m_auth=true ;;
|
|
esac
|
|
|
|
# Backend: code that isn't a frontend component/view file. Includes ESM/CJS
|
|
# (.mjs/.cjs) and explicit-module TS (.mts/.cts) — #1810: these matched no
|
|
# category, so an ESM/CJS-only PR skipped the backend reviewer entirely.
|
|
if [ "$m_frontend" = false ]; then
|
|
case "$f" in
|
|
*.rb|*.py|*.go|*.rs|*.java|*.php|*.ex|*.exs) m_backend=true ;;
|
|
*.ts|*.js|*.mjs|*.cjs|*.mts|*.cts) m_backend=true ;;
|
|
esac
|
|
fi
|
|
case "$f" in
|
|
*.dart) m_backend=true ;;
|
|
esac
|
|
|
|
[ "$m_frontend" = true ] && FRONTEND=true
|
|
[ "$m_prompts" = true ] && PROMPTS=true
|
|
[ "$m_tests" = true ] && TESTS=true
|
|
[ "$m_docs" = true ] && DOCS=true
|
|
[ "$m_config" = true ] && CONFIG=true
|
|
[ "$m_migrations" = true ] && MIGRATIONS=true
|
|
[ "$m_api" = true ] && API=true
|
|
[ "$m_auth" = true ] && AUTH=true
|
|
[ "$m_backend" = true ] && BACKEND=true
|
|
|
|
if [ "$m_frontend" = false ] && [ "$m_prompts" = false ] && [ "$m_tests" = false ] \
|
|
&& [ "$m_docs" = false ] && [ "$m_config" = false ] && [ "$m_migrations" = false ] \
|
|
&& [ "$m_api" = false ] && [ "$m_auth" = false ] && [ "$m_backend" = false ]; then
|
|
UNMATCHED+=("$f")
|
|
fi
|
|
done
|
|
|
|
_print_flags
|
|
|
|
# Changed files but ZERO category matches (#2526): a classifier bug, an
|
|
# unrecognised layout, or a new top-level directory would otherwise present
|
|
# as "no reviewers needed" with the skip invisible. Trip loudly instead.
|
|
if [ "$FRONTEND" = false ] && [ "$BACKEND" = false ] && [ "$PROMPTS" = false ] \
|
|
&& [ "$TESTS" = false ] && [ "$DOCS" = false ] && [ "$CONFIG" = false ] \
|
|
&& [ "$MIGRATIONS" = false ] && [ "$API" = false ] && [ "$AUTH" = false ]; then
|
|
echo "SCOPE_ERROR=unmatched"
|
|
printf '%s\n' ${UNMATCHED[@]+"${UNMATCHED[@]}"} | sort -u | head -50 | while IFS= read -r u; do
|
|
[ -n "$u" ] && printf '# unmatched: %s\n' "$u"
|
|
done
|
|
exit 2
|
|
fi
|