Files
Bo 10db732740 fix(evolve): guard jq|wc pipefail so a malformed line cannot abort the script (ag-1j1 #jq-wc-guards) (#584)
## What

Under `set -euo pipefail`, a `jq ... 2>/dev/null | wc -l` substitution
aborts the entire script when `jq` hits a malformed JSON line:
`2>/dev/null` suppresses jq's stderr but not its non-zero exit code,
`pipefail` propagates that through the pipeline, and `set -e` then kills
the script mid-run (leaving partial state).

## Fix

Append `|| echo 0` to the unguarded substitutions:

- **`scripts/bootstrap-maturity.sh` line 65** — reads the raw `.jsonl`
file directly, so a malformed line genuinely aborts the migration before
the summary prints. Verified empirically and caught by the regression
test (test fails against the unguarded copy with `status != 0`).
- **`scripts/evolve-capture-daily-learning.sh` lines 59-62 (4x)** —
matches the already-guarded `grep -c` on line 58. Verification note: in
normal operation `TODAY_CYCLES` is pre-sanitized by the line-55 `jq ...
|| true` filter, so these lines don't see malformed input in practice.
The guard is defense-in-depth that pins abort-proof behavior if
malformed content ever reaches them (e.g., a future refactor drops the
line-55 sanitization).

`halt-check.sh` is intentionally out of scope (`set -uo`, no `-e` —
degrades, doesn't abort).

## Test

`tests/scripts/jq-wc-pipefail-guards.bats` (5 tests):
- Malformed-input fixtures for both scripts prove neither aborts and the
consolidated artifacts still get written.
- A guarded/unguarded expression pair (positive + negative control) pins
the `|| echo 0` mechanism so a future edit can't silently drop it.

## Local validation

- `bats tests/scripts/jq-wc-pipefail-guards.bats` — 5/5 green
- `shellcheck scripts/evolve-capture-daily-learning.sh
scripts/bootstrap-maturity.sh` — clean
- Existing `tests/scripts/bootstrap-maturity.bats` — still 4/4 green

Closes-scenario: ag-1j1#jq-wc-guards
Bounded-context: BC5-Runtime
Evidence: bats tests/scripts/jq-wc-pipefail-guards.bats
2026-05-28 17:19:15 -04:00

90 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# bootstrap-maturity.sh — One-time migration: add maturity: provisional to
# existing .md learnings that lack a maturity field.
#
# Usage: bash scripts/bootstrap-maturity.sh [learnings-dir]
# Default learnings dir: .agents/learnings
#
# Safe to run multiple times — skips files that already have maturity field.
set -euo pipefail
LEARNINGS_DIR="${1:-.agents/learnings}"
if [[ ! -d "$LEARNINGS_DIR" ]]; then
echo "Directory not found: $LEARNINGS_DIR"
exit 1
fi
total=0
updated=0
skipped=0
for file in "$LEARNINGS_DIR"/*.md; do
[[ -f "$file" ]] || continue
total=$((total + 1))
# Check if file has YAML frontmatter
head_line=$(head -1 "$file")
if [[ "$head_line" != "---" ]]; then
skipped=$((skipped + 1))
continue
fi
# Check if maturity field already exists in frontmatter
if grep -q "^maturity:" "$file"; then
skipped=$((skipped + 1))
continue
fi
# Add maturity: provisional after the opening ---
# Use a temp file to avoid in-place edit portability issues
tmpfile=$(mktemp)
{
echo "---"
echo "maturity: provisional"
tail -n +2 "$file"
} > "$tmpfile"
mv "$tmpfile" "$file"
updated=$((updated + 1))
done
# Process .jsonl files: add "maturity":"provisional" if missing
jsonl_total=0
jsonl_updated=0
jsonl_skipped=0
if command -v jq >/dev/null 2>&1; then
for file in "$LEARNINGS_DIR"/*.jsonl; do
[[ -f "$file" ]] || continue
jsonl_total=$((jsonl_total + 1))
# Count lines missing the maturity field. JSONL is one object per
# line, so we must treat each line independently — a single -e check
# only inspects the first object.
missing=$(jq -c 'select(has("maturity") | not)' "$file" 2>/dev/null | wc -l | tr -d ' ' || echo 0)
if [[ "${missing:-0}" -eq 0 ]]; then
jsonl_skipped=$((jsonl_skipped + 1))
continue
fi
# Transform each object: add maturity only when missing, preserving
# existing values. Use -c so output stays valid JSONL (compact, one
# object per line) instead of jq's default pretty-printed stream.
tmpfile=$(mktemp)
if jq -c 'if has("maturity") then . else . + {"maturity": "provisional"} end' "$file" > "$tmpfile" 2>/dev/null; then
mv "$tmpfile" "$file"
jsonl_updated=$((jsonl_updated + 1))
else
rm -f "$tmpfile"
jsonl_skipped=$((jsonl_skipped + 1))
fi
done
else
echo "Warning: jq not found — skipping .jsonl files" >&2
fi
echo "Bootstrap maturity complete:"
echo " .md files: $total total, $updated updated, $skipped skipped"
echo " .jsonl files: $jsonl_total total, $jsonl_updated updated, $jsonl_skipped skipped"