From 35c9cc657e516d0f14a909f9a0d83268be14c239 Mon Sep 17 00:00:00 2001 From: Omer Kocaoglu Date: Wed, 19 Aug 2026 21:17:18 -0400 Subject: [PATCH] fix(hack): make audit-gate lint scripts run on stock macOS lint-drift.sh aborted on empty-array expansion under set -u (bash 3.2), lint-docstrings.sh broke bash 3.2's command-substitution parser with an apostrophe in a comment, then false-positived MISSING_FIELDS on every exported struct because BSD grep has no -P/PCRE. make audit could not run at all on a default macOS toolchain. Behavior-preserving POSIX/bash-3.2-safe substitutions; findings identical under GNU. Spec: specs/hack-script-portability.md Signed-off-by: Omer Kocaoglu --- .context/LEARNINGS.md | 21 ++++++++++++++ hack/lint-docstrings.sh | 8 ++++-- hack/lint-drift.sh | 5 +++- specs/hack-script-portability.md | 47 ++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 specs/hack-script-portability.md diff --git a/.context/LEARNINGS.md b/.context/LEARNINGS.md index a64dcc584..533e7670c 100644 --- a/.context/LEARNINGS.md +++ b/.context/LEARNINGS.md @@ -1,5 +1,16 @@ # Learnings + +| Date | Learning | +|----|--------| +| 2026-08-19 | hack/ lint scripts assumed GNU bash 4+ and GNU grep; stock macOS breaks all three ways | +| 2026-07-25 | Using the proprietary sibling repo as design evidence leaks its internals into tracked files | +| 2026-07-25 | Skill and doc examples of a serialized structure must round-trip through the real parser | +| 2026-07-25 | A guard derived from a capability accessor silently lifts when the accessor is extended | +| 2026-07-19 | The disclosure parser is a deliberately dumb line-scanner (skips comments, not code fences) | +| 2026-07-19 | Measurement gates surface a real bug in every disclosure milestone | + + +## [2026-08-19-211547] hack/ lint scripts assumed GNU bash 4+ and GNU grep; stock macOS breaks all three ways + +**Context**: make audit had never passed on this stock macOS machine: lint-drift.sh died on empty-array expansion under set -u (bash 3.2), lint-docstrings.sh died on an apostrophe in a comment inside $( ) (bash 3.2 substitution parser), then false-positived MISSING_FIELDS on every exported struct because BSD grep silently lacks -P/PCRE. + +**Lesson**: Three distinct GNU-isms bite on macOS defaults: empty-array ${arr[@]} under set -u (bash 3.2), apostrophes in comments inside command substitution (bash 3.2), and grep -P (BSD grep). The -P failure is the worst: it fails per-line inside || true pipelines, so counts come back empty and checks false-positive rather than erroring. + +**Application**: In hack/ scripts use ${arr[@]+"${arr[@]}"} guards, no apostrophes in comments inside $( ), and POSIX patterns ($'\t' + [[:space:]]) instead of grep -P. Per specs/hack-script-portability.md; long-term fix is the Go rewrite tracked in TASKS. + +--- + ## [2026-07-25-124457] Using the proprietary sibling repo as design evidence leaks its internals into tracked files **Context**: While deciding the pd-m4 add-path shape, I read the sibling repo's convention file to settle the question, then quoted its guide text and attributed the decision to it in a tracked plan file. An unrelated build warning prompted the sweep that caught it. diff --git a/hack/lint-docstrings.sh b/hack/lint-docstrings.sh index ef72cd470..822531f13 100755 --- a/hack/lint-docstrings.sh +++ b/hack/lint-docstrings.sh @@ -83,7 +83,8 @@ find internal/ cmd/ -name '*.go' ! -name '*_test.go' ! -name 'doc.go' | sort | w continue fi returnpart=$(echo "$rest" | sed 's/^func [A-Za-z0-9_]*([^)]*) //') - # Guard: if sed didn't match (returnpart unchanged), skip + # Guard: if sed did not match (returnpart unchanged), skip. + # (No apostrophes in comments inside $(...): bash 3.2 mis-parses them.) if [ "$returnpart" = "$rest" ]; then continue fi @@ -105,8 +106,9 @@ find internal/ cmd/ -name '*.go' ! -name '*_test.go' ! -name 'doc.go' | sort | w if [ -z "$closing" ]; then continue fi + # $'…' expands \t to a real tab: BSD grep (macOS) has no -P/PCRE. fieldcount=$(sed -n "$((lineno+1)),$((closing-1))p" "$file" \ - | grep -cP '^\t[A-Z]' || true) + | grep -c $'^\t[A-Z]' || true) if [ "$fieldcount" -lt 2 ]; then continue fi @@ -138,7 +140,7 @@ find internal/ cmd/ -name '*.go' ! -name '*_test.go' ! -name 'doc.go' | sort | w # Accept inline field comments as alternative to Fields: section. # Count fields with a preceding or same-line comment. inlinecount=$(sed -n "$((lineno+1)),$((closing-1))p" "$file" \ - | grep -cP '^\t// [A-Z]|^\t[A-Z].*//\s' || true) + | grep -cE $'^\t// [A-Z]|^\t[A-Z].*//[[:space:]]' || true) if [ "$inlinecount" -ge "$fieldcount" ]; then continue fi diff --git a/hack/lint-drift.sh b/hack/lint-drift.sh index d01b85b5e..99138b692 100755 --- a/hack/lint-drift.sh +++ b/hack/lint-drift.sh @@ -36,7 +36,10 @@ drift_grep() { for ex in "$@"; do exclude_args+=(--exclude="$ex") done - grep -rn --include='*.go' --exclude='*_test.go' "${exclude_args[@]}" \ + # ${arr[@]+...} guard: bash 3.2 (macOS default) treats an empty + # array expansion as unbound under `set -u`. + grep -rn --include='*.go' --exclude='*_test.go' \ + ${exclude_args[@]+"${exclude_args[@]}"} \ -E "$pattern" internal/ 2>/dev/null || true } diff --git a/specs/hack-script-portability.md b/specs/hack-script-portability.md new file mode 100644 index 000000000..b7edfe3de --- /dev/null +++ b/specs/hack-script-portability.md @@ -0,0 +1,47 @@ +# Spec: hack/ Script Portability — macOS Default Toolchain + +## Problem + +`make audit` fails before running a single real check on a stock +macOS machine (bash 3.2, BSD grep). The lint scripts in `hack/` +assumed GNU bash ≥ 4 and GNU grep: + +1. `hack/lint-drift.sh` — `"${exclude_args[@]}"` on an empty array + aborts under `set -u` on bash 3.2 ("unbound variable"; bash 4.4+ + treats it as empty). +2. `hack/lint-docstrings.sh` — an apostrophe in a comment inside a + `$( … )` command substitution breaks bash 3.2's substitution + parser ("unexpected EOF while looking for matching `'`"). +3. `hack/lint-docstrings.sh` — `grep -cP` (PCRE) is GNU-only; BSD + grep has no `-P`, so every field count came back empty and every + exported struct false-positived as `MISSING_FIELDS`. + +These failures predate any feature work and mask real findings: the +audit gate cannot run at all on contributor machines with the default +macOS toolchain. + +## Fix + +Minimal, behavior-preserving substitutions that run identically under +GNU and BSD toolchains: + +- `${arr[@]+"${arr[@]}"}` guard for empty-array expansion. +- Reword the offending comment (no apostrophes inside `$( … )`). +- Replace `-P` patterns with POSIX equivalents: `$'^\t…'` (real tab + via ANSI-C quoting, supported by bash 3.2) and `[[:space:]]` for + `\s`. + +## Non-Goals + +- Rewriting the lint scripts in Go (tracked in TASKS.md: "Replace + hack/lint-drift.sh with AST-based Go tests"; "Rewrite lint-style + scripts in Go as ctxctl subcommands"). This spec only unblocks the + gate until that lands. +- A full portability audit of every script under `hack/` (only the + `make audit` chain is in scope). + +## Verification + +`make audit` completes on macOS (bash 3.2.57, BSD grep) with the same +findings as on a GNU toolchain: `lint-drift: clean`, no docstring +false-positives, all checks green.