@keelcore/standards 0.9.7 → 0.10.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.
@@ -3,15 +3,18 @@
3
3
  These rules govern all bash scripts in this project. They are non-negotiable.
4
4
 
5
5
  **Maturity:** Required
6
- **Version:** 1.0.0
7
- **Last Reviewed:** 2026-03-09
6
+ **Version:** 1.2.0
7
+ **Last Reviewed:** 2026-03-17
8
+
9
+ Each rule has a stable canonical ID (`R1`–`R42`) used when these rules are
10
+ cited elsewhere. Numbers do not renumber across edits.
8
11
 
9
12
  ## Portability and Shell Baseline
10
13
 
11
- 1. Google Bash style unless overridden by rules below.
12
- 2. Darwin-compatible; Bash 3.x compatible — no `mapfile`, no `declare -A`.
13
- 3. Shebang: `#!/usr/bin/env bash` always.
14
- 4. Use the explicit shell options block (not `set -euo pipefail` shorthand).
14
+ - **R1.** Google Bash style unless overridden by rules below.
15
+ - **R2.** Darwin-compatible; Bash 3.x compatible — no `mapfile`, no `declare -A`.
16
+ - **R3.** Shebang: `#!/usr/bin/env bash` always.
17
+ - **R4.** Use the explicit shell options block (not `set -euo pipefail` shorthand).
15
18
 
16
19
  ## Required Shell Options Block
17
20
 
@@ -33,66 +36,111 @@ set -o errexit
33
36
  set -o pipefail
34
37
  ```
35
38
 
39
+ ## Locale Determinism
40
+
41
+ Bash tooling is locale-sensitive and MUST run under a pinned C locale so output is reproducible
42
+ across machines and CI (the generic policy: see coding.md, "Locale and Reproducibility"):
43
+
44
+ - **L1.** `sort` collates by `LC_COLLATE` — order differs across locales, and `sort -u` can even
45
+ dedup differently. `awk`/`printf` format numbers by `LC_NUMERIC` — a comma-radix locale (e.g.
46
+ `de_DE`) turns `95.00` into `95,00`, breaking numeric comparisons and file compares. `tr [:upper:]`,
47
+ `[[ a < b ]]`, and `grep` character classes are likewise locale-dependent.
48
+ - **L2.** The build entrypoint (the `Makefile`) MUST `export LC_ALL := C`, so every recipe shell and
49
+ its subprocesses — including canonical scripts a consumer does not edit — run under C.
50
+ - **L3.** A script that sorts or formats numbers and may be run OUTSIDE `make` MUST source a locale
51
+ guard (`scripts/lib/locale.sh`) that exports `LC_ALL=C`, so direct `bash scripts/x.sh` runs are
52
+ deterministic too.
53
+ - **L4.** The lock is testable: a BATS case runs a recipe under a hostile `LC_ALL`/`LC_NUMERIC` and
54
+ asserts `LC_ALL=C` wins.
55
+
36
56
  ## Script Structure
37
57
 
38
- 5. `main` is the FIRST function defined in the file.
39
- 6. `main "${@:-}"` is the LAST line of the file.
40
- 7. Always provide the complete script, never a patch or diff.
41
- 8. Never omit any referenced function.
58
+ - **R5.** `main` is the FIRST function defined in the file.
59
+ - **R6.** `main "${@:-}"` is the LAST line of the file.
60
+ - **R7.** Always provide the complete script, never a patch or diff.
61
+ - **R8.** Never omit any referenced function.
42
62
 
43
63
  ## Functions
44
64
 
45
- 9. All function declarations preceded by keyword `function`.
46
- 10. No `function_` prefix on function names.
47
- 11. Function names: lowercase, simple, reflect intent.
48
- 12. One statement per line.
49
- 13. No function body > 10 lines unless reduction is genuinely impossible.
50
- 14. Prefer decomposition into small, reusable helpers.
51
- 15. Load-bearing functions: log intent first, then log result.
65
+ - **R9.** All function declarations preceded by keyword `function`.
66
+ - **R10.** No `function_` prefix on function names.
67
+ - **R11.** Function names: lowercase, simple, reflect intent.
68
+ - **R12.** One statement per line.
69
+ - **R13.** No function body > 10 lines unless reduction is genuinely impossible.
70
+ - **R14.** Prefer decomposition into small, reusable helpers.
71
+ - **R15.** Load-bearing functions: log intent first, then log result.
52
72
 
53
73
  ## Arguments and Validation
54
74
 
55
- 16. Validate argument count.
56
- 17. Validate required arguments are non-null.
57
- 18. Support empty argument lists under `set -u` using `"${@:-}"`.
58
- 19. `validate_args` must allow an empty first argument when zero arguments are valid:
75
+ - **R16.** Validate argument count.
76
+ - **R17.** Validate required arguments are non-null.
77
+ - **R18.** Support empty argument lists under `set -u` using `"${@:-}"`.
78
+ - **R19.** `validate_args` must allow an empty first argument when zero arguments are valid:
59
79
 
60
- ```bash
61
- function validate_args() {
62
- if [ "${#}" -gt 0 ] && [ -z "${1:-}" ]; then
63
- log '❌ Error: Unexpected empty argument'
64
- exit 1
65
- fi
66
- }
67
- ```
80
+ ```bash
81
+ function validate_args() {
82
+ if [ "${#}" -gt 0 ] && [ -z "${1:-}" ]; then
83
+ log '❌ Error: Unexpected empty argument'
84
+ exit 1
85
+ fi
86
+ }
87
+ ```
68
88
 
69
89
  ## Variables
70
90
 
71
- 20. Always `"${var}"` style for variable expansion.
72
- 21. Quote expansions unless unquoted behavior is explicitly required.
73
- 22. `local -r` for immutable locals NOT assigned from subshell.
74
- 23. Mutable locals or subshell-assigned: declare on one line, assign on next:
91
+ - **R20.** Always `"${var}"` style for variable expansion.
92
+ - **R21.** Quote expansions unless unquoted behavior is explicitly required.
93
+ - **R22.** `local -r` for immutable locals NOT assigned from subshell.
94
+ - **R23.** Mutable locals or subshell-assigned: declare on one line, assign on next:
75
95
 
76
- ```bash
77
- local result
78
- result="$(some_command)"
79
- ```
96
+ ```bash
97
+ local result
98
+ result="$(some_command)"
99
+ ```
80
100
 
81
- 24. Single quotes for literal strings with no expansion needed.
101
+ - **R24.** Single quotes for literal strings with no expansion needed.
82
102
 
83
103
  ## Logging and Output
84
104
 
85
- 25. Route all messaging through a `log` function.
86
- 26. `log` must tee to a well-known log file.
87
- 27. Capture original stdout in `main` with `exec 5>&1`; `log` writes to FD 5.
88
- 28. Use emoji for key accomplishments.
89
- 29. Load-bearing functions log both intent and result.
105
+ - **R25.** Route all messaging through a `log` function.
106
+ - **R26.** `log` must tee to a well-known log file.
107
+ - **R27.** Capture original stdout in `main` with `exec 5>&1`; `log` writes to FD 5.
108
+ - **R28.** Use emoji for key accomplishments.
109
+ - **R29.** Load-bearing functions log both intent and result.
90
110
 
91
111
  ## Printing
92
112
 
93
- 30. Prefer `printf` over `echo`.
94
- 31. `printf` format string must be single-quoted with explicit format specifiers.
95
- 32. Use `echo` only with no parameters and no formatting concerns.
113
+ - **R30.** Prefer `printf` over `echo`.
114
+ - **R31.** `printf` format string must be single-quoted with explicit format specifiers.
115
+ - **R32.** Use `echo` only with no parameters and no formatting concerns.
116
+
117
+ ## Globals
118
+
119
+ - **R33.** Never use the keyword `global`.
120
+ - **R34.** All globals shall be declared after the header comment block and before `function main`.
121
+ - **R35.** All globals shall be immutable after initialization.
122
+ - **R36.** Immutability shall be enforced with `declare -r`; the one exception is a global whose
123
+ initialization requires multi-statement evaluation — in that case declare on one line,
124
+ assign on the next (the variable remains effectively immutable by convention):
125
+
126
+ ```bash
127
+ # simple: single-expression initialization
128
+ declare -r DEPLOY_ENV="${DEPLOY_ENV:-staging}"
129
+
130
+ # multi-statement: declare first, then assign
131
+ declare LOG_FILE
132
+ LOG_FILE="$(mktemp /tmp/script.XXXXXX)"
133
+ ```
134
+
135
+ - **R37.** Environment variables shall be declared as globals with `declare -rx`, using the
136
+ default-value idiom. This applies to both imported and exported environment variables:
137
+
138
+ ```bash
139
+ declare -rx SOME_VAR="${SOME_VAR:-default_value}"
140
+ ```
141
+
142
+ - **R38.** Global names SHALL be uppercase.
143
+ - **R39.** Never declare a global inside a function scope.
96
144
 
97
145
  ## Standard `log` Function Pattern
98
146
 
@@ -145,3 +193,26 @@ function validate_args() {
145
193
  ```
146
194
 
147
195
  Do NOT change the call site from `"${@:-}"` to `"$@"` — fix the body only.
196
+
197
+ ## Embedded Code (Heredocs)
198
+
199
+ - **R40.** Never embed code blocks (PHP, TOML, ini, shell, conf, etc.) as heredocs inside a bash
200
+ script. Code in heredocs cannot be linted by its native tooling, is invisible to static
201
+ analysis, and causes bash quoting edge cases that are impossible to debug.
202
+
203
+ - **R41.** Extract every embedded code block to a **sibling file** in the same directory as the
204
+ script, with the correct source extension for linting (`.php`, `.toml`, `.sh`, `.conf`,
205
+ etc.). Load it at runtime with `cat`:
206
+
207
+ ```bash
208
+ php_script=$(cat "${SCRIPT_DIR}/myscript-step.php")
209
+ ```
210
+
211
+ - **R42.** Templates that require runtime variable substitution use `@PLACEHOLDER@` notation and
212
+ `sed` for substitution. Never use an unquoted heredoc (`<< EOF`) for variable injection:
213
+
214
+ ```bash
215
+ php_script=$(sed \
216
+ -e "s|@MY_VAR@|${MY_VAR}|g" \
217
+ "${SCRIPT_DIR}/myscript-template.php")
218
+ ```