@windyroad/itil 0.47.0-preview.516 → 0.47.1-preview.521
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/.claude-plugin/plugin.json +1 -1
- package/bin/wr-itil-check-locale-discipline +51 -0
- package/package.json +1 -1
- package/scripts/check-locale-discipline.sh +185 -0
- package/scripts/test/check-locale-discipline.bats +291 -0
- package/scripts/test/no-type-regression-guard.bats +3 -2
- package/skills/capture-problem/test/capture-problem-step-1-5b-jtbd-trace.bats +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generated by scripts/sync-shim-wrappers.sh from
|
|
3
|
+
# packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
|
|
4
|
+
# shim files in packages/*/bin/wr-* directly; edit the template + run
|
|
5
|
+
# `npm run sync:shim-wrappers` to regenerate.
|
|
6
|
+
#
|
|
7
|
+
# Resolution (ADR-080):
|
|
8
|
+
# 1. If the wrapper's parent dir is semver-shaped, treat as installed-
|
|
9
|
+
# cache execution and resolve to the highest-version sibling's
|
|
10
|
+
# scripts/ entry below.
|
|
11
|
+
# 2. Otherwise (parent dir is e.g. `architect`), treat as source-
|
|
12
|
+
# monorepo execution and dispatch to own scripts/. The source-repo-
|
|
13
|
+
# guard `exec` is the anchor parsed by
|
|
14
|
+
# packages/retrospective/scripts/check-tarball-shipped-shims.sh.
|
|
15
|
+
# 3. If the cache parent contains zero semver-shaped siblings, exit
|
|
16
|
+
# 127 with a stderr message naming the cache parent (per SQ-080-2).
|
|
17
|
+
#
|
|
18
|
+
# @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
|
|
19
|
+
# @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
|
|
20
|
+
# @problem P343 (mid-session staleness window)
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
|
|
26
|
+
OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
|
|
27
|
+
CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
|
|
28
|
+
|
|
29
|
+
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
|
|
30
|
+
|
|
31
|
+
# Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
|
|
32
|
+
if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
|
|
33
|
+
exec "$SHIM_DIR/../scripts/check-locale-discipline.sh" "$@"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Cache execution: pick the highest-semver sibling under CACHE_PARENT.
|
|
37
|
+
HIGHEST=""
|
|
38
|
+
while IFS= read -r dir; do
|
|
39
|
+
name="$(basename "$dir")"
|
|
40
|
+
[[ "$name" =~ $SEMVER_RE ]] || continue
|
|
41
|
+
if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
|
|
42
|
+
HIGHEST="$name"
|
|
43
|
+
fi
|
|
44
|
+
done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
45
|
+
|
|
46
|
+
if [[ -z "$HIGHEST" ]]; then
|
|
47
|
+
printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
|
|
48
|
+
exit 127
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exec "$CACHE_PARENT/$HIGHEST/scripts/check-locale-discipline.sh" "$@"
|
package/package.json
CHANGED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-locale-discipline.sh — advisory lint for P328 (BSD locale UTF-8
|
|
3
|
+
# friction on macOS).
|
|
4
|
+
#
|
|
5
|
+
# BSD `grep` / `sed` / `awk` on macOS silently fail (or emit
|
|
6
|
+
# `multibyte conversion failure` / `illegal byte sequence`) when processing
|
|
7
|
+
# UTF-8 multi-byte characters (em-dash `—`, smart quotes, en-dash) without
|
|
8
|
+
# `LC_ALL=en_US.UTF-8` set. Our prose surfaces (ADRs, problem-ticket bodies,
|
|
9
|
+
# briefing entries, SKILL.md files) are pervasively em-dash / smart-quote
|
|
10
|
+
# rich, so any script that grep / sed / awks those surfaces is exposed.
|
|
11
|
+
# P328 captures three distinct incident classes from the 2026-05-30
|
|
12
|
+
# ADR-077 compendium session.
|
|
13
|
+
#
|
|
14
|
+
# This lint walks `packages/*/scripts/*.sh`, `packages/*/hooks/*.sh`, and
|
|
15
|
+
# `packages/*/lib/*.sh` (including `packages/*/hooks/lib/*.sh`) and reports
|
|
16
|
+
# any line invoking `grep` / `sed` / `awk` that is NOT preceded — in the
|
|
17
|
+
# same script — by an `export LC_ALL=` statement OR an inline `LC_ALL=`
|
|
18
|
+
# prefix on the same line. `git grep` is skipped (different binary).
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# check-locale-discipline.sh [<repo-root>]
|
|
22
|
+
# <repo-root> defaults to the current working directory.
|
|
23
|
+
#
|
|
24
|
+
# Environment:
|
|
25
|
+
# WR_LOCALE_DISCIPLINE_WARN_ONLY=1 Phase 1 advisory (default) — exit 0
|
|
26
|
+
# even when violations exist.
|
|
27
|
+
# WR_LOCALE_DISCIPLINE_WARN_ONLY=0 Phase 2 load-bearing — exit 1 when
|
|
28
|
+
# violations exist.
|
|
29
|
+
#
|
|
30
|
+
# Exit codes:
|
|
31
|
+
# 0 = clean OR Phase 1 advisory with violations
|
|
32
|
+
# 1 = Phase 2 load-bearing with violations
|
|
33
|
+
# 2 = usage / path error
|
|
34
|
+
#
|
|
35
|
+
# Output format (one line per violation, to stderr):
|
|
36
|
+
# WARN <relpath>:<line> <tool> without preceding LC_ALL=en_US.UTF-8
|
|
37
|
+
#
|
|
38
|
+
# Promotion criteria (Phase 1 → Phase 2):
|
|
39
|
+
# Promote `WR_LOCALE_DISCIPLINE_WARN_ONLY=0` once existing scripts have
|
|
40
|
+
# been migrated. Until then, the warnings are signal-only — they identify
|
|
41
|
+
# scripts that may silently mis-process UTF-8 input on macOS.
|
|
42
|
+
#
|
|
43
|
+
# @adr ADR-040 (advisory-then-load-bearing reusable pattern)
|
|
44
|
+
# @adr ADR-049 (plugin-bundled scripts; PATH shim)
|
|
45
|
+
# @adr ADR-052 (behavioural-tests-default)
|
|
46
|
+
# @adr ADR-080 (highest-version-wins shim wrapper)
|
|
47
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory exit 0)
|
|
48
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
49
|
+
# @jtbd JTBD-001 (Enforce Governance Without Slowing Down)
|
|
50
|
+
# @jtbd JTBD-101 (Extend the Suite with New Plugins)
|
|
51
|
+
# @problem P328 (BSD grep/sed/awk on macOS friction with UTF-8)
|
|
52
|
+
|
|
53
|
+
set -uo pipefail
|
|
54
|
+
|
|
55
|
+
# Self-application: this lint itself grep / sed / awks script content.
|
|
56
|
+
export LC_ALL=en_US.UTF-8
|
|
57
|
+
|
|
58
|
+
REPO_ROOT="${1:-$(pwd)}"
|
|
59
|
+
WARN_ONLY="${WR_LOCALE_DISCIPLINE_WARN_ONLY:-1}"
|
|
60
|
+
|
|
61
|
+
if [ ! -d "$REPO_ROOT" ]; then
|
|
62
|
+
echo "check-locale-discipline: not a directory: $REPO_ROOT" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [ ! -d "$REPO_ROOT/packages" ]; then
|
|
67
|
+
echo "check-locale-discipline: no packages/ subdir under $REPO_ROOT" >&2
|
|
68
|
+
exit 2
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Enumerate target scripts: any *.sh under packages/<pkg>/scripts/,
|
|
72
|
+
# packages/<pkg>/hooks/ (incl. nested lib/), packages/<pkg>/lib/.
|
|
73
|
+
# Depth 3-5 covers packages/<pkg>/scripts/foo.sh and
|
|
74
|
+
# packages/<pkg>/hooks/lib/foo.sh.
|
|
75
|
+
mapfile -t TARGETS < <(
|
|
76
|
+
find "$REPO_ROOT/packages" \
|
|
77
|
+
-mindepth 3 -maxdepth 5 \
|
|
78
|
+
-type f -name '*.sh' \
|
|
79
|
+
\( -path '*/scripts/*' -o -path '*/hooks/*' -o -path '*/lib/*' \) \
|
|
80
|
+
2>/dev/null | sort
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
violations=0
|
|
84
|
+
scanned=0
|
|
85
|
+
|
|
86
|
+
scan_one() {
|
|
87
|
+
local file="$1"
|
|
88
|
+
local rel="${file#"$REPO_ROOT"/}"
|
|
89
|
+
local line_no=0
|
|
90
|
+
local lc_all_set=0
|
|
91
|
+
local in_heredoc=0
|
|
92
|
+
local heredoc_token=''
|
|
93
|
+
local line
|
|
94
|
+
|
|
95
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
96
|
+
line_no=$((line_no + 1))
|
|
97
|
+
|
|
98
|
+
# Heredoc body — skip until the closing token. The closing token
|
|
99
|
+
# appears on a line of its own (optionally leading whitespace when
|
|
100
|
+
# opened with `<<-`).
|
|
101
|
+
if [ "$in_heredoc" -eq 1 ]; then
|
|
102
|
+
local trimmed="${line#"${line%%[![:space:]]*}"}"
|
|
103
|
+
if [ "$trimmed" = "$heredoc_token" ]; then
|
|
104
|
+
in_heredoc=0
|
|
105
|
+
heredoc_token=''
|
|
106
|
+
fi
|
|
107
|
+
continue
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Comment-only line — skip.
|
|
111
|
+
if [[ "$line" =~ ^[[:space:]]*\# ]]; then
|
|
112
|
+
continue
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# LC_ALL set state. An `export LC_ALL=...` line flips the file-wide
|
|
116
|
+
# protection on. An inline `LC_ALL=...` prefix on the same line as a
|
|
117
|
+
# grep / sed / awk invocation protects only that line.
|
|
118
|
+
if [[ "$line" =~ ^[[:space:]]*export[[:space:]]+LC_ALL= ]]; then
|
|
119
|
+
lc_all_set=1
|
|
120
|
+
continue
|
|
121
|
+
fi
|
|
122
|
+
if [[ "$line" =~ ^[[:space:]]*LC_ALL= ]]; then
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Detect a heredoc-open ON THIS LINE (before deciding it's a violation
|
|
127
|
+
# — heredoc-open lines may carry a grep/sed/awk invocation that runs).
|
|
128
|
+
# Grammar: `<<` or `<<-` followed by optional single/double quote then
|
|
129
|
+
# an identifier-shaped token. Set state for subsequent lines.
|
|
130
|
+
local opened_heredoc=0
|
|
131
|
+
if [[ "$line" =~ \<\<-?[\'\"]?([A-Za-z_][A-Za-z0-9_]*)[\'\"]? ]]; then
|
|
132
|
+
heredoc_token="${BASH_REMATCH[1]}"
|
|
133
|
+
opened_heredoc=1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# If file-wide LC_ALL is set above this point, no violation possible.
|
|
137
|
+
if [ "$lc_all_set" -eq 1 ]; then
|
|
138
|
+
[ "$opened_heredoc" -eq 1 ] && in_heredoc=1
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Tool-invocation detector. Word-boundary match on grep / sed / awk
|
|
143
|
+
# at a command position: start of line, after a pipe, semicolon,
|
|
144
|
+
# ampersand, open-paren, backtick, or whitespace. Followed by
|
|
145
|
+
# whitespace (so we skip `grep_helper`, `$grep`, etc.). We scan
|
|
146
|
+
# for ALL three tools by checking each in turn so multiple
|
|
147
|
+
# invocations on one line still flag the line once per distinct
|
|
148
|
+
# tool — keeps output deterministic and bounded.
|
|
149
|
+
local matched_tool=''
|
|
150
|
+
if [[ "$line" =~ (^|[[:space:]\|\;\&\(\`\$\{])(grep|sed|awk)([[:space:]]|$) ]]; then
|
|
151
|
+
matched_tool="${BASH_REMATCH[2]}"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [ -n "$matched_tool" ]; then
|
|
155
|
+
# Skip `git grep` (different binary; uses git's own pattern engine).
|
|
156
|
+
if [ "$matched_tool" = "grep" ] && [[ "$line" =~ git[[:space:]]+grep ]]; then
|
|
157
|
+
[ "$opened_heredoc" -eq 1 ] && in_heredoc=1
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
printf 'WARN %s:%d %s without preceding LC_ALL=en_US.UTF-8\n' \
|
|
161
|
+
"$rel" "$line_no" "$matched_tool" >&2
|
|
162
|
+
violations=$((violations + 1))
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
[ "$opened_heredoc" -eq 1 ] && in_heredoc=1
|
|
166
|
+
done < "$file"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for f in "${TARGETS[@]}"; do
|
|
170
|
+
scanned=$((scanned + 1))
|
|
171
|
+
scan_one "$f"
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
if [ "$violations" -gt 0 ]; then
|
|
175
|
+
printf 'check-locale-discipline: %d violation(s) across %d script(s) — add `export LC_ALL=en_US.UTF-8` at the top of each script that calls grep/sed/awk on UTF-8 content, or use an inline `LC_ALL=...` prefix per invocation (P328).\n' \
|
|
176
|
+
"$violations" "$scanned" >&2
|
|
177
|
+
if [ "$WARN_ONLY" = "1" ]; then
|
|
178
|
+
exit 0
|
|
179
|
+
else
|
|
180
|
+
exit 1
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
printf 'check-locale-discipline: clean (%d script(s) scanned; no unprotected grep/sed/awk invocations).\n' "$scanned"
|
|
185
|
+
exit 0
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P328 — BSD `grep` / `sed` / `awk` on macOS silently mis-process
|
|
4
|
+
# UTF-8 multi-byte characters (em-dash, smart quotes) without
|
|
5
|
+
# `LC_ALL=en_US.UTF-8` set. Lint detects unprotected invocations so the
|
|
6
|
+
# class of silent-wrong-result bugs can't reach the codebase undetected.
|
|
7
|
+
#
|
|
8
|
+
# Contract: `check-locale-discipline.sh [<repo-root>]` walks scripts under
|
|
9
|
+
# `packages/*/scripts/`, `packages/*/hooks/` (incl. nested `lib/`), and
|
|
10
|
+
# `packages/*/lib/`, then emits one WARN line per unprotected grep/sed/awk
|
|
11
|
+
# invocation. Default Phase 1 advisory exits 0; `WR_LOCALE_DISCIPLINE_WARN_ONLY=0`
|
|
12
|
+
# promotes to Phase 2 load-bearing (exit 1 on any violation).
|
|
13
|
+
#
|
|
14
|
+
# Behavioural fixture (ADR-052 default): every test exercises the script
|
|
15
|
+
# against a synthesised packages/ fixture tree containing known-state
|
|
16
|
+
# scripts. No grep of SKILL.md / agent prose / source content.
|
|
17
|
+
#
|
|
18
|
+
# @adr ADR-040 (advisory-then-load-bearing reusable pattern)
|
|
19
|
+
# @adr ADR-049 (plugin-bundled scripts; PATH shim)
|
|
20
|
+
# @adr ADR-052 (behavioural-tests-default — temp script with known violations)
|
|
21
|
+
# @adr ADR-080 (highest-version-wins shim wrapper)
|
|
22
|
+
# @adr ADR-005 (Plugin testing strategy — bats coverage)
|
|
23
|
+
# @jtbd JTBD-001 (Enforce Governance Without Slowing Down)
|
|
24
|
+
# @jtbd JTBD-101 (Extend the Suite with New Plugins)
|
|
25
|
+
|
|
26
|
+
setup() {
|
|
27
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
28
|
+
SCRIPT="$SCRIPTS_DIR/check-locale-discipline.sh"
|
|
29
|
+
FIXTURE_ROOT="$(mktemp -d)"
|
|
30
|
+
mkdir -p "$FIXTURE_ROOT/packages/demo/scripts"
|
|
31
|
+
mkdir -p "$FIXTURE_ROOT/packages/demo/hooks"
|
|
32
|
+
mkdir -p "$FIXTURE_ROOT/packages/demo/hooks/lib"
|
|
33
|
+
mkdir -p "$FIXTURE_ROOT/packages/demo/lib"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
teardown() {
|
|
37
|
+
rm -rf "$FIXTURE_ROOT"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ── Existence + executable ──────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
@test "check-locale-discipline: script exists" {
|
|
43
|
+
[ -f "$SCRIPT" ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "check-locale-discipline: script is executable" {
|
|
47
|
+
[ -x "$SCRIPT" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ── Negative cases (violations expected) ────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
@test "check-locale-discipline: grep without LC_ALL emits WARN and counts violation" {
|
|
53
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-grep.sh" <<'EOF'
|
|
54
|
+
#!/usr/bin/env bash
|
|
55
|
+
grep -c '^### ' README.md
|
|
56
|
+
EOF
|
|
57
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
58
|
+
[ "$status" -eq 0 ] # Phase 1 advisory
|
|
59
|
+
echo "$output" | grep -E "WARN.*raw-grep.sh:2.*grep without preceding LC_ALL"
|
|
60
|
+
echo "$output" | grep -E "1 violation"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "check-locale-discipline: sed without LC_ALL emits WARN" {
|
|
64
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-sed.sh" <<'EOF'
|
|
65
|
+
#!/usr/bin/env bash
|
|
66
|
+
sed -n '/^### /p' README.md
|
|
67
|
+
EOF
|
|
68
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
69
|
+
[ "$status" -eq 0 ]
|
|
70
|
+
echo "$output" | grep -E "WARN.*raw-sed.sh:2.*sed without preceding LC_ALL"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "check-locale-discipline: awk without LC_ALL emits WARN" {
|
|
74
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/raw-awk.sh" <<'EOF'
|
|
75
|
+
#!/usr/bin/env bash
|
|
76
|
+
awk '/^### /' README.md
|
|
77
|
+
EOF
|
|
78
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
79
|
+
[ "$status" -eq 0 ]
|
|
80
|
+
echo "$output" | grep -E "WARN.*raw-awk.sh:2.*awk without preceding LC_ALL"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ── Positive cases (file-wide protection — silent pass) ─────────────────────
|
|
84
|
+
|
|
85
|
+
@test "check-locale-discipline: file-wide export LC_ALL above grep — silent pass" {
|
|
86
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/protected.sh" <<'EOF'
|
|
87
|
+
#!/usr/bin/env bash
|
|
88
|
+
export LC_ALL=en_US.UTF-8
|
|
89
|
+
grep -c '^### ' README.md
|
|
90
|
+
sed -n '/^### /p' README.md
|
|
91
|
+
awk '/^### /' README.md
|
|
92
|
+
EOF
|
|
93
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
94
|
+
[ "$status" -eq 0 ]
|
|
95
|
+
! echo "$output" | grep -qE "protected.sh.*WARN"
|
|
96
|
+
echo "$output" | grep -E "clean"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "check-locale-discipline: inline LC_ALL= prefix on same line — silent pass" {
|
|
100
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/inline.sh" <<'EOF'
|
|
101
|
+
#!/usr/bin/env bash
|
|
102
|
+
LC_ALL=en_US.UTF-8 grep -c '^### ' README.md
|
|
103
|
+
LC_ALL=en_US.UTF-8 sed -n '/^### /p' README.md
|
|
104
|
+
LC_ALL=en_US.UTF-8 awk '/^### /' README.md
|
|
105
|
+
EOF
|
|
106
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
107
|
+
[ "$status" -eq 0 ]
|
|
108
|
+
! echo "$output" | grep -qE "inline.sh.*WARN"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# ── Edge cases ──────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
@test "check-locale-discipline: multiple grep/sed/awk on one line — line is flagged" {
|
|
114
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/multi.sh" <<'EOF'
|
|
115
|
+
#!/usr/bin/env bash
|
|
116
|
+
grep -c '^### ' README.md | sed 's/ //g' | awk '{print $1}'
|
|
117
|
+
EOF
|
|
118
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
119
|
+
[ "$status" -eq 0 ]
|
|
120
|
+
# At least one tool is reported; we don't over-specify the count.
|
|
121
|
+
echo "$output" | grep -E "WARN.*multi.sh:2.*(grep|sed|awk) without preceding LC_ALL"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "check-locale-discipline: git grep is skipped" {
|
|
125
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/git-grep.sh" <<'EOF'
|
|
126
|
+
#!/usr/bin/env bash
|
|
127
|
+
git grep -nE 'pattern' -- '*.md'
|
|
128
|
+
EOF
|
|
129
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
130
|
+
[ "$status" -eq 0 ]
|
|
131
|
+
! echo "$output" | grep -qE "git-grep.sh.*WARN"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@test "check-locale-discipline: heredoc body containing grep is not flagged" {
|
|
135
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/heredoc.sh" <<'EOF'
|
|
136
|
+
#!/usr/bin/env bash
|
|
137
|
+
cat <<'INNER'
|
|
138
|
+
The way to do this is to run: grep '^### ' README.md
|
|
139
|
+
And then: sed -n '/^### /p'
|
|
140
|
+
INNER
|
|
141
|
+
echo "done"
|
|
142
|
+
EOF
|
|
143
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
# Heredoc body lines must NOT produce WARNs.
|
|
146
|
+
! echo "$output" | grep -qE "heredoc.sh:[34].*WARN"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@test "check-locale-discipline: comment-only line is not flagged" {
|
|
150
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/commented.sh" <<'EOF'
|
|
151
|
+
#!/usr/bin/env bash
|
|
152
|
+
# Future work: replace this awk with a sed | grep pipeline.
|
|
153
|
+
echo "no grep here"
|
|
154
|
+
EOF
|
|
155
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
156
|
+
[ "$status" -eq 0 ]
|
|
157
|
+
! echo "$output" | grep -qE "commented.sh.*WARN"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@test "check-locale-discipline: identifiers containing grep/sed/awk substrings are not flagged" {
|
|
161
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/identifiers.sh" <<'EOF'
|
|
162
|
+
#!/usr/bin/env bash
|
|
163
|
+
grep_helper="something"
|
|
164
|
+
result_from_sed=42
|
|
165
|
+
my_awkward_var="x"
|
|
166
|
+
echo "$grep_helper $result_from_sed $my_awkward_var"
|
|
167
|
+
EOF
|
|
168
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
169
|
+
[ "$status" -eq 0 ]
|
|
170
|
+
! echo "$output" | grep -qE "identifiers.sh.*WARN"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ── Scope: hooks/ + lib/ + nested hooks/lib/ ────────────────────────────────
|
|
174
|
+
|
|
175
|
+
@test "check-locale-discipline: scans packages/*/hooks/ scripts" {
|
|
176
|
+
cat > "$FIXTURE_ROOT/packages/demo/hooks/raw-grep-hook.sh" <<'EOF'
|
|
177
|
+
#!/usr/bin/env bash
|
|
178
|
+
grep -c '^### ' README.md
|
|
179
|
+
EOF
|
|
180
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
181
|
+
[ "$status" -eq 0 ]
|
|
182
|
+
echo "$output" | grep -E "WARN.*hooks/raw-grep-hook.sh:2"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@test "check-locale-discipline: scans packages/*/hooks/lib/ scripts" {
|
|
186
|
+
cat > "$FIXTURE_ROOT/packages/demo/hooks/lib/nested.sh" <<'EOF'
|
|
187
|
+
#!/usr/bin/env bash
|
|
188
|
+
awk '/^### /' README.md
|
|
189
|
+
EOF
|
|
190
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
191
|
+
[ "$status" -eq 0 ]
|
|
192
|
+
echo "$output" | grep -E "WARN.*hooks/lib/nested.sh:2"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@test "check-locale-discipline: scans packages/*/lib/ scripts" {
|
|
196
|
+
cat > "$FIXTURE_ROOT/packages/demo/lib/raw-sed-lib.sh" <<'EOF'
|
|
197
|
+
#!/usr/bin/env bash
|
|
198
|
+
sed -n '/^### /p' README.md
|
|
199
|
+
EOF
|
|
200
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
201
|
+
[ "$status" -eq 0 ]
|
|
202
|
+
echo "$output" | grep -E "WARN.*lib/raw-sed-lib.sh:2"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# ── Phase 1 advisory vs Phase 2 load-bearing ────────────────────────────────
|
|
206
|
+
|
|
207
|
+
@test "check-locale-discipline: Phase 1 default exits 0 even with violations" {
|
|
208
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
|
|
209
|
+
#!/usr/bin/env bash
|
|
210
|
+
grep -c '^### ' README.md
|
|
211
|
+
EOF
|
|
212
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
213
|
+
[ "$status" -eq 0 ]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "check-locale-discipline: Phase 2 load-bearing exits 1 on violations" {
|
|
217
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
|
|
218
|
+
#!/usr/bin/env bash
|
|
219
|
+
grep -c '^### ' README.md
|
|
220
|
+
EOF
|
|
221
|
+
WR_LOCALE_DISCIPLINE_WARN_ONLY=0 run "$SCRIPT" "$FIXTURE_ROOT"
|
|
222
|
+
[ "$status" -eq 1 ]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@test "check-locale-discipline: Phase 2 load-bearing exits 0 on clean tree" {
|
|
226
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/clean.sh" <<'EOF'
|
|
227
|
+
#!/usr/bin/env bash
|
|
228
|
+
export LC_ALL=en_US.UTF-8
|
|
229
|
+
grep -c '^### ' README.md
|
|
230
|
+
EOF
|
|
231
|
+
WR_LOCALE_DISCIPLINE_WARN_ONLY=0 run "$SCRIPT" "$FIXTURE_ROOT"
|
|
232
|
+
[ "$status" -eq 0 ]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# ── Argument and error handling ─────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
@test "check-locale-discipline: missing repo-root directory exits 2" {
|
|
238
|
+
run "$SCRIPT" "$FIXTURE_ROOT/does-not-exist"
|
|
239
|
+
[ "$status" -eq 2 ]
|
|
240
|
+
echo "$output" | grep -iE "not a directory"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test "check-locale-discipline: repo-root without packages/ subdir exits 2" {
|
|
244
|
+
mkdir -p "$FIXTURE_ROOT/empty"
|
|
245
|
+
run "$SCRIPT" "$FIXTURE_ROOT/empty"
|
|
246
|
+
[ "$status" -eq 2 ]
|
|
247
|
+
echo "$output" | grep -iE "no packages/"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@test "check-locale-discipline: defaults to current working directory when no arg" {
|
|
251
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
|
|
252
|
+
#!/usr/bin/env bash
|
|
253
|
+
grep -c '^### ' README.md
|
|
254
|
+
EOF
|
|
255
|
+
cd "$FIXTURE_ROOT"
|
|
256
|
+
run "$SCRIPT"
|
|
257
|
+
[ "$status" -eq 0 ]
|
|
258
|
+
echo "$output" | grep -E "WARN.*scripts/violation.sh:2"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ── Output shape ────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
@test "check-locale-discipline: clean tree emits summary on stdout" {
|
|
264
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/clean.sh" <<'EOF'
|
|
265
|
+
#!/usr/bin/env bash
|
|
266
|
+
export LC_ALL=en_US.UTF-8
|
|
267
|
+
grep -c '^### ' README.md
|
|
268
|
+
EOF
|
|
269
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
270
|
+
[ "$status" -eq 0 ]
|
|
271
|
+
echo "$output" | grep -E "^check-locale-discipline: clean"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@test "check-locale-discipline: violation summary cites P328" {
|
|
275
|
+
cat > "$FIXTURE_ROOT/packages/demo/scripts/violation.sh" <<'EOF'
|
|
276
|
+
#!/usr/bin/env bash
|
|
277
|
+
grep -c '^### ' README.md
|
|
278
|
+
EOF
|
|
279
|
+
run "$SCRIPT" "$FIXTURE_ROOT"
|
|
280
|
+
[ "$status" -eq 0 ]
|
|
281
|
+
echo "$output" | grep -E "P328"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# ── Self-application: lint script itself is locale-discipline-compliant ─────
|
|
285
|
+
|
|
286
|
+
@test "check-locale-discipline: own script declares export LC_ALL at top" {
|
|
287
|
+
# The lint walks UTF-8 prose; it must lead by example. This guards
|
|
288
|
+
# against a future edit that strips the export and reintroduces P328
|
|
289
|
+
# in the lint itself.
|
|
290
|
+
head -80 "$SCRIPT" | grep -qE "^export LC_ALL=en_US.UTF-8$"
|
|
291
|
+
}
|
|
@@ -65,9 +65,10 @@ setup() {
|
|
|
65
65
|
diff -q "$HELPER" "$REPO_ROOT/packages/architect/lib/derive-first-dispatch.sh"
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
@test "P287: capture-problem SKILL.md does not carry --type=
|
|
68
|
+
@test "P287: capture-problem SKILL.md does not carry --type= flag rows (--no-prompt REVIVED per ADR-060 Amendment 2026-06-02 as AFK marker)" {
|
|
69
69
|
# The retired Step 1.5 dispatch flags must not appear in the flag table.
|
|
70
|
+
# NB: --no-prompt was REVIVED per ADR-060 Amendment 2026-06-02 — see
|
|
71
|
+
# capture-problem-step-1-5b-jtbd-trace.bats line 327 (positive assertion).
|
|
70
72
|
! grep -E '^\| `--type=technical`' "$SKILL_FILE"
|
|
71
73
|
! grep -E '^\| `--type=user-business`' "$SKILL_FILE"
|
|
72
|
-
! grep -E '^\| `--no-prompt`' "$SKILL_FILE"
|
|
73
74
|
}
|
|
@@ -317,7 +317,7 @@ parse_no_prompt_flag() {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
@test "SKILL.md: Step 1.5b preserves JTBD-301 firewall on plugin-user-side intake" {
|
|
320
|
-
grep -qE '
|
|
320
|
+
grep -qE '[Pp]lugin-user-side.*MUST NOT (prompt|carry)' "$SKILL_FILE"
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
@test "SKILL.md: Step 1.5b names derive-then-ratify contract" {
|