@intentsolutionsio/contributing-clanker 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/install.sh +115 -0
- package/hooks/uninstall.sh +70 -0
- package/package.json +42 -0
- package/skills/contribute/SKILL.md +457 -0
- package/skills/contribute/agents/draft-writer.md +110 -0
- package/skills/contribute/agents/repo-analyzer.md +68 -0
- package/skills/contribute/agents/researcher.md +246 -0
- package/skills/contribute/agents/scout.md +182 -0
- package/skills/contribute/agents/test-runner.md +70 -0
- package/skills/contribute/assets/claim-template.md +22 -0
- package/skills/contribute/assets/evidence-template.md +32 -0
- package/skills/contribute/assets/pr-template.md +49 -0
- package/skills/contribute/references/candidate-file-format.md +259 -0
- package/skills/contribute/references/workflow-guide.md +153 -0
- package/skills/contribute/scripts/audit-overrides.sh +180 -0
- package/skills/contribute/scripts/catalog-coverage.sh +99 -0
- package/skills/contribute/scripts/gate-runner.sh +191 -0
- package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
- package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
- package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
- package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
- package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
- package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
- package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
- package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
- package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
- package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
- package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
- package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
- package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
- package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
- package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
- package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
- package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
- package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
- package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
- package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
- package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
- package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
- package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
- package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
- package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
- package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
- package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
- package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
- package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
- package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
- package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
- package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
- package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
- package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
- package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
- package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
- package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
- package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
- package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
- package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
- package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
- package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
- package/skills/contribute/scripts/lint-candidate.sh +149 -0
- package/skills/contribute/scripts/researcher-build.sh +456 -0
- package/skills/contribute/scripts/test-known-traps.sh +142 -0
- package/skills/contribute/scripts/test-override-audit.sh +102 -0
- package/skills/contribute/scripts/test-plug-in.sh +113 -0
- package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
- package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
- package/skills/contribute/scripts/transition.sh +260 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: G3 — manual CHANGELOG edits in a repo that auto-generates the changelog
|
|
3
|
+
# Mitigates: clobbers release tooling, gets reverted on next release, looks careless.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
9
|
+
gate_skip "no dossier — cannot determine auto_changelog policy"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
AUTO=$(fm_field "$GATE_DOSSIER_PATH" "auto_changelog")
|
|
13
|
+
if [[ "$AUTO" != "true" ]]; then
|
|
14
|
+
gate_skip "dossier auto_changelog is not true"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
18
|
+
CLONE="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
19
|
+
|
|
20
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
21
|
+
gate_skip "no local clone at $CLONE"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
25
|
+
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
|
|
26
|
+
|
|
27
|
+
CHANGED=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" --name-only 2>/dev/null || /usr/bin/echo "")
|
|
28
|
+
|
|
29
|
+
# Match CHANGELOG.md at root or in any subdir (case-sensitive)
|
|
30
|
+
HITS=$(/usr/bin/printf '%s\n' "$CHANGED" | /usr/bin/grep -E '(^|/)CHANGELOG\.md$' || /usr/bin/echo "")
|
|
31
|
+
|
|
32
|
+
if [[ -z "$HITS" ]]; then
|
|
33
|
+
gate_pass "no CHANGELOG.md edits in diff"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
LIST=$(/usr/bin/printf '%s' "$HITS" | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/,$//')
|
|
37
|
+
gate_warn "diff edits CHANGELOG.md ($LIST) but repo auto-generates changelog" "this repo auto-generates CHANGELOG; let the release tooling write it"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: G4 — manual version bump in a repo that uses semantic-release / changesets
|
|
3
|
+
# Mitigates: clobbers release tooling, breaks semver, signals unfamiliarity with workflow.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
9
|
+
gate_skip "no dossier — cannot determine auto_version policy"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
AUTO=$(fm_field "$GATE_DOSSIER_PATH" "auto_version")
|
|
13
|
+
if [[ "$AUTO" != "true" ]]; then
|
|
14
|
+
gate_skip "dossier auto_version is not true"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
18
|
+
CLONE="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
19
|
+
|
|
20
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
21
|
+
gate_skip "no local clone at $CLONE"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
25
|
+
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
|
|
26
|
+
|
|
27
|
+
DIFF=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" -- package.json Cargo.toml pyproject.toml 2>/dev/null || /usr/bin/echo "")
|
|
28
|
+
|
|
29
|
+
# Look for added lines that look like version field updates
|
|
30
|
+
HITS=$(/usr/bin/printf '%s\n' "$DIFF" | /usr/bin/grep -E '^\+[[:space:]]*("version"[[:space:]]*:|version[[:space:]]*=)' || /usr/bin/echo "")
|
|
31
|
+
|
|
32
|
+
if [[ -z "$HITS" ]]; then
|
|
33
|
+
gate_pass "no manual version field changes detected"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
gate_warn "diff appears to bump a version field (package.json / Cargo.toml / pyproject.toml)" "this repo uses semantic-release / changesets; don't bump versions manually"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: G6 — Override rate limit (anti-habituation)
|
|
3
|
+
# Mitigates: Round-1 security GAP-2 — under "just ship it" pressure, an agent
|
|
4
|
+
# learns to invoke overrides reflexively. Without a rate limit, the override
|
|
5
|
+
# mechanism becomes a slop escape valve. ≥3 overrides at one repo in 30d =
|
|
6
|
+
# the system is telling you something; pause and reflect.
|
|
7
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
8
|
+
|
|
9
|
+
gate_read_input
|
|
10
|
+
|
|
11
|
+
if [[ -z "$GATE_REPO" ]]; then
|
|
12
|
+
gate_skip "no repo in candidate"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
LOG="$HOME/.contribute-system/log.jsonl"
|
|
16
|
+
[[ ! -f "$LOG" ]] && gate_pass "no log.jsonl yet"
|
|
17
|
+
|
|
18
|
+
# Count override events in last 30 days at this repo
|
|
19
|
+
THIRTY_AGO=$(/usr/bin/date -u -d '30 days ago' +%s)
|
|
20
|
+
|
|
21
|
+
OVERRIDE_COUNT=$(jq -r --arg cutoff "$THIRTY_AGO" --arg repo "$GATE_REPO" '
|
|
22
|
+
select(.event == "gate_override")
|
|
23
|
+
| select(.details.repo == $repo)
|
|
24
|
+
| select((.ts | fromdateiso8601) >= ($cutoff | tonumber))
|
|
25
|
+
' "$LOG" 2>/dev/null | jq -s 'length' 2>/dev/null || /usr/bin/echo 0)
|
|
26
|
+
|
|
27
|
+
if [[ "${OVERRIDE_COUNT:-0}" -ge 3 ]]; then
|
|
28
|
+
gate_block "$OVERRIDE_COUNT gate overrides at $GATE_REPO in last 30 days (rate limit: 3)" "the system is telling you something. Either your scope at this repo is wrong, your dossier is out of date, or you're forcing through patterns that will eventually trigger an AI-policy closure. To override THIS rate limit: edit ~/.contribute-system/g06-rate-limit-reset && touch it. Don't just shrug it off."
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
gate_pass "$OVERRIDE_COUNT/3 overrides at $GATE_REPO in last 30 days"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared preamble for every gate script. Sourced as the first line of every
|
|
3
|
+
# gate. Eliminates the per-script bug surface that Slice 1's scout-discover.sh
|
|
4
|
+
# already showed (empty-array failures under set -u, jq parse errors on
|
|
5
|
+
# malformed gh output, etc.).
|
|
6
|
+
#
|
|
7
|
+
# Per-script gate authors: source this file FIRST, then implement the gate.
|
|
8
|
+
# Use the helpers below for output. NEVER `echo "{...}"` your verdict directly
|
|
9
|
+
# — use gate_pass / gate_warn / gate_block / gate_inform.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
# Global error trap — any uncaught error dumps a BLOCK verdict + exits 0.
|
|
14
|
+
# (Per gate contract: exit code is always 0; runner reads stdout JSON.)
|
|
15
|
+
_GATE_ID="${0##*/}"
|
|
16
|
+
_GATE_ID="${_GATE_ID%.sh}"
|
|
17
|
+
|
|
18
|
+
_gate_err_trap() {
|
|
19
|
+
local exit_code=$?
|
|
20
|
+
local line_no=$1
|
|
21
|
+
/usr/bin/cat <<EOF
|
|
22
|
+
{"severity":"BLOCK","gate":"$_GATE_ID","reason":"gate $_GATE_ID crashed at line $line_no (exit $exit_code) — fail-closed","fix_hint":"check ~/.contribute-system/check-runs/gate-debug.log; this is a bug in the gate itself"}
|
|
23
|
+
EOF
|
|
24
|
+
exit 0
|
|
25
|
+
}
|
|
26
|
+
trap '_gate_err_trap $LINENO' ERR
|
|
27
|
+
|
|
28
|
+
# Emit a verdict and exit. Use these — never raw echo.
|
|
29
|
+
#
|
|
30
|
+
# Reason / fix_hint are JSON-escaped via jq -nc. Earlier versions used
|
|
31
|
+
# printf '%s' which dropped raw text into the JSON body and produced
|
|
32
|
+
# invalid output whenever any message contained a literal `"`. Caught by
|
|
33
|
+
# the e02-ai-strike-track unit tests on 2026-05-03.
|
|
34
|
+
gate_pass() { jq -nc --arg g "$_GATE_ID" --arg r "${1:-ok}" '{severity:"PASS",gate:$g,reason:$r}'; exit 0; }
|
|
35
|
+
gate_warn() { jq -nc --arg g "$_GATE_ID" --arg r "${1:-warning}" --arg f "${2:-}" '{severity:"WARN",gate:$g,reason:$r,fix_hint:$f}'; exit 0; }
|
|
36
|
+
gate_block() { jq -nc --arg g "$_GATE_ID" --arg r "${1:-blocked}" --arg f "${2:-}" '{severity:"BLOCK",gate:$g,reason:$r,fix_hint:$f}'; exit 0; }
|
|
37
|
+
gate_inform() { jq -nc --arg g "$_GATE_ID" --arg r "${1:-noted}" '{severity:"INFORM",gate:$g,reason:$r}'; exit 0; }
|
|
38
|
+
gate_skip() { jq -nc --arg g "$_GATE_ID" --arg r "${1:-not applicable}" '{severity:"SKIP",gate:$g,reason:$r}'; exit 0; }
|
|
39
|
+
|
|
40
|
+
# Read stdin JSON contract. Sets:
|
|
41
|
+
# GATE_CANDIDATE_PATH — path to candidate .md file
|
|
42
|
+
# GATE_DOSSIER_PATH — path to dossier .md (or empty if no dossier)
|
|
43
|
+
# GATE_ACTION — transition name (e.g., "shortlist→claimed")
|
|
44
|
+
# GATE_REPO — owner/repo
|
|
45
|
+
# GATE_INPUT_JSON — raw stdin for any extra fields
|
|
46
|
+
gate_read_input() {
|
|
47
|
+
GATE_INPUT_JSON=$(/usr/bin/cat)
|
|
48
|
+
GATE_CANDIDATE_PATH=$(/usr/bin/printf '%s' "$GATE_INPUT_JSON" | jq -r '.candidate // ""')
|
|
49
|
+
GATE_DOSSIER_PATH=$(/usr/bin/printf '%s' "$GATE_INPUT_JSON" | jq -r '.dossier // ""')
|
|
50
|
+
GATE_ACTION=$(/usr/bin/printf '%s' "$GATE_INPUT_JSON" | jq -r '.action // ""')
|
|
51
|
+
GATE_REPO=$(/usr/bin/printf '%s' "$GATE_INPUT_JSON" | jq -r '.env.repo // ""')
|
|
52
|
+
export GATE_INPUT_JSON GATE_CANDIDATE_PATH GATE_DOSSIER_PATH GATE_ACTION GATE_REPO
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Read a frontmatter field from a markdown file. Returns empty if missing.
|
|
56
|
+
# Usage: val=$(fm_field /path/to/file.md "key")
|
|
57
|
+
fm_field() {
|
|
58
|
+
local file="$1" key="$2"
|
|
59
|
+
[[ -f "$file" ]] || { /usr/bin/printf ''; return; }
|
|
60
|
+
/usr/bin/awk -v k="$key" '
|
|
61
|
+
/^---$/ { fm = !fm ? 1 : 2; next }
|
|
62
|
+
fm == 1 && $0 ~ "^"k":" {
|
|
63
|
+
sub("^"k":[[:space:]]*", "")
|
|
64
|
+
gsub(/^"|"$/, "")
|
|
65
|
+
print
|
|
66
|
+
exit
|
|
67
|
+
}
|
|
68
|
+
' "$file"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# gh wrapper with bounded retry.
|
|
72
|
+
#
|
|
73
|
+
# Constraint: gate-runner enforces a 10s wall-clock timeout per gate. gh_safe
|
|
74
|
+
# must finish well under that to leave room for jq/awk downstream.
|
|
75
|
+
# Tuning: 2 attempts, 0.5s sleep between, 4s per-call timeout.
|
|
76
|
+
# Worst case ≈ 4 + 0.5 + 4 = 8.5s — leaves headroom.
|
|
77
|
+
#
|
|
78
|
+
# Permanent 404s (issue/repo doesn't exist) fail on the first call's HTTP error
|
|
79
|
+
# and don't usefully retry. Transient blips get one retry. That's enough for
|
|
80
|
+
# the read-only gh queries gates make.
|
|
81
|
+
#
|
|
82
|
+
# On final failure returns non-zero with empty stdout — caller decides what
|
|
83
|
+
# missing data means (usually `gate_pass`/`gate_inform`/`gate_skip`).
|
|
84
|
+
gh_safe() {
|
|
85
|
+
local attempt=1 max=2
|
|
86
|
+
while (( attempt <= max )); do
|
|
87
|
+
if /usr/bin/timeout 4 gh "$@" 2>/dev/null; then return 0; fi
|
|
88
|
+
/usr/bin/sleep 0.5
|
|
89
|
+
attempt=$(( attempt + 1 ))
|
|
90
|
+
done
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Helper: log a gate run for observability. Append-only.
|
|
95
|
+
gate_log_run() {
|
|
96
|
+
local verdict="$1"
|
|
97
|
+
/usr/bin/printf '%s\n' "$(jq -nc \
|
|
98
|
+
--arg ts "$(/usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
99
|
+
--arg gate "$_GATE_ID" \
|
|
100
|
+
--arg action "$GATE_ACTION" \
|
|
101
|
+
--arg repo "$GATE_REPO" \
|
|
102
|
+
--arg verdict "$verdict" \
|
|
103
|
+
'{ts: $ts, event: "gate_run", details: {gate: $gate, action: $action, repo: $repo, verdict: $verdict}}')" \
|
|
104
|
+
>> ~/.contribute-system/log.jsonl 2>/dev/null || true
|
|
105
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lint-candidate.sh — report missing required body sections in candidate files.
|
|
3
|
+
#
|
|
4
|
+
# Sibling to audit-overrides.sh and catalog-coverage.sh: a read-only reporter
|
|
5
|
+
# that walks ~/.contribute-system/candidates/, reads each candidate's status,
|
|
6
|
+
# and surfaces missing required sections per the matrix in
|
|
7
|
+
# skills/contribute/references/candidate-file-format.md.
|
|
8
|
+
#
|
|
9
|
+
# Required-sections matrix (matches the spec + transition.sh):
|
|
10
|
+
# shortlist → ## Scope, ## Files to touch
|
|
11
|
+
# claimed → ## Scope, ## Files to touch, ## Claim comment draft
|
|
12
|
+
# working → same as claimed
|
|
13
|
+
# submitted → ## PR title, ## PR body, ## Test results
|
|
14
|
+
# merged → same as submitted
|
|
15
|
+
# open → no body requirements
|
|
16
|
+
# dropped → no body requirements
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# lint-candidate.sh # all candidates, table output
|
|
20
|
+
# lint-candidate.sh --status=submitted # filter to one status
|
|
21
|
+
# lint-candidate.sh --missing-only # only candidates with missing sections
|
|
22
|
+
# lint-candidate.sh --json # JSON output (one object per candidate)
|
|
23
|
+
# lint-candidate.sh --candidates-dir=X # override the candidate dir
|
|
24
|
+
#
|
|
25
|
+
# Exit codes:
|
|
26
|
+
# 0 — no missing sections across the filtered set (clean)
|
|
27
|
+
# 1 — at least one candidate has missing required sections
|
|
28
|
+
# 64 — bad arguments
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
CAND_DIR="${HOME}/.contribute-system/candidates"
|
|
33
|
+
STATUS_FILTER=""
|
|
34
|
+
MISSING_ONLY=0
|
|
35
|
+
JSON_OUT=0
|
|
36
|
+
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--status=*) STATUS_FILTER="${1#*=}" ;;
|
|
40
|
+
--missing-only) MISSING_ONLY=1 ;;
|
|
41
|
+
--json) JSON_OUT=1 ;;
|
|
42
|
+
--candidates-dir=*) CAND_DIR="${1#*=}" ;;
|
|
43
|
+
-h|--help) /usr/bin/sed -n '2,28p' "$0" | /usr/bin/sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
44
|
+
*) /usr/bin/echo "unknown arg: $1" >&2; exit 64 ;;
|
|
45
|
+
esac
|
|
46
|
+
shift
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
if [[ ! -d "$CAND_DIR" ]]; then
|
|
50
|
+
/usr/bin/echo "candidate dir not found: $CAND_DIR" >&2
|
|
51
|
+
exit 64
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Required-sections per status. Pipe-delimited for compatibility with the
|
|
55
|
+
# same scheme transition.sh uses.
|
|
56
|
+
required_for() {
|
|
57
|
+
case "$1" in
|
|
58
|
+
shortlist) /usr/bin/echo '## Scope|## Files to touch' ;;
|
|
59
|
+
claimed) /usr/bin/echo '## Scope|## Files to touch|## Claim comment draft' ;;
|
|
60
|
+
working) /usr/bin/echo '## Scope|## Files to touch|## Claim comment draft' ;;
|
|
61
|
+
submitted) /usr/bin/echo '## PR title|## PR body|## Test results' ;;
|
|
62
|
+
merged) /usr/bin/echo '## PR title|## PR body|## Test results' ;;
|
|
63
|
+
*) /usr/bin/echo '' ;; # open, dropped, unknown
|
|
64
|
+
esac
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Per-candidate evaluation. Echoes a JSON object per candidate so the table
|
|
68
|
+
# pass and the JSON pass can both consume it.
|
|
69
|
+
evaluate_one() {
|
|
70
|
+
local file="$1"
|
|
71
|
+
local basename
|
|
72
|
+
basename=$(/usr/bin/basename "$file")
|
|
73
|
+
local status
|
|
74
|
+
status=$(/usr/bin/awk '/^---$/{fm=!fm?1:2;next} fm==1 && /^status:/{sub(/^status:[[:space:]]*/,""); print; exit}' "$file" 2>/dev/null)
|
|
75
|
+
status="${status:-unknown}"
|
|
76
|
+
|
|
77
|
+
local req
|
|
78
|
+
req=$(required_for "$status")
|
|
79
|
+
|
|
80
|
+
local missing=()
|
|
81
|
+
if [[ -n "$req" ]]; then
|
|
82
|
+
IFS='|' read -ra SECTIONS <<< "$req"
|
|
83
|
+
for sec in "${SECTIONS[@]}"; do
|
|
84
|
+
if ! /usr/bin/grep -qE "^${sec}\b" "$file" 2>/dev/null; then
|
|
85
|
+
missing+=("$sec")
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
local missing_json
|
|
91
|
+
if [[ "${#missing[@]}" -eq 0 ]]; then
|
|
92
|
+
missing_json='[]'
|
|
93
|
+
else
|
|
94
|
+
missing_json=$(/usr/bin/printf '%s\n' "${missing[@]}" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
jq -nc --arg cand "$basename" --arg status "$status" --argjson missing "$missing_json" \
|
|
98
|
+
'{candidate: $cand, status: $status, missing: $missing, missing_count: ($missing | length)}'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Walk all candidates, evaluate each, capture rows.
|
|
102
|
+
ROWS=()
|
|
103
|
+
EXIT_CODE=0
|
|
104
|
+
shopt -s nullglob
|
|
105
|
+
for f in "$CAND_DIR"/*.md; do
|
|
106
|
+
row=$(evaluate_one "$f")
|
|
107
|
+
STATUS=$(/usr/bin/echo "$row" | jq -r .status)
|
|
108
|
+
MISSING_COUNT=$(/usr/bin/echo "$row" | jq -r .missing_count)
|
|
109
|
+
|
|
110
|
+
# Apply --status filter
|
|
111
|
+
if [[ -n "$STATUS_FILTER" && "$STATUS" != "$STATUS_FILTER" ]]; then
|
|
112
|
+
continue
|
|
113
|
+
fi
|
|
114
|
+
# Apply --missing-only filter
|
|
115
|
+
if [[ "$MISSING_ONLY" -eq 1 && "$MISSING_COUNT" -eq 0 ]]; then
|
|
116
|
+
continue
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
ROWS+=("$row")
|
|
120
|
+
if [[ "$MISSING_COUNT" -gt 0 ]]; then
|
|
121
|
+
EXIT_CODE=1
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
shopt -u nullglob
|
|
125
|
+
|
|
126
|
+
# Emit
|
|
127
|
+
if [[ "$JSON_OUT" -eq 1 ]]; then
|
|
128
|
+
/usr/bin/printf '%s\n' "${ROWS[@]}" | jq -s '.'
|
|
129
|
+
else
|
|
130
|
+
/usr/bin/printf '%-55s %-10s %s\n' 'candidate' 'status' 'missing'
|
|
131
|
+
/usr/bin/printf '%-55s %-10s %s\n' '---------' '------' '-------'
|
|
132
|
+
if [[ "${#ROWS[@]}" -eq 0 ]]; then
|
|
133
|
+
/usr/bin/printf '(no candidates matched filter)\n'
|
|
134
|
+
else
|
|
135
|
+
for row in "${ROWS[@]}"; do
|
|
136
|
+
cand=$(/usr/bin/echo "$row" | jq -r .candidate)
|
|
137
|
+
status=$(/usr/bin/echo "$row" | jq -r .status)
|
|
138
|
+
missing_str=$(/usr/bin/echo "$row" | jq -r '.missing | if length == 0 then "(none)" else join(", ") end')
|
|
139
|
+
/usr/bin/printf '%-55s %-10s %s\n' "$cand" "$status" "$missing_str"
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
# Summary footer
|
|
143
|
+
TOTAL=${#ROWS[@]}
|
|
144
|
+
DIRTY=$(/usr/bin/printf '%s\n' "${ROWS[@]}" | jq -s '[.[] | select(.missing_count > 0)] | length')
|
|
145
|
+
/usr/bin/printf '\n%d candidate(s) shown · %d with missing sections\n' "$TOTAL" "$DIRTY"
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
exit "$EXIT_CODE"
|