diff --git a/CHANGELOG.md b/CHANGELOG.md index c7eacbd4..71de35b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(harvest-learnings): **a bug-log heading the toolkit log already carries is no longer drafted again when it has `~`, `*` or a backtick in its first 30 characters after the `BUG-nn:` tag.** Section 1 strips those characters from a project heading before it matches the heading's first 30 characters after the tag against `bug-logs/mxcli-bugs.md`, but searched the log as written, so the stripped text was never a substring of a heading like ``BUG-01: `alter entity drop attribute` causes …``. The log is now stripped the same way before the search, and the search takes `-e`, because stripping can leave a heading that starts with `--` (BUG-26, BUG-54), which `grep` read as an option. Reproduced with the script at 95e1b30 on a project whose bug log holds copies of all 223 headings in the toolkit log: it drafts 103 of them, and 0 after the fix. Pinned by `tests/wave2/test-harvest-learnings.sh` T6 (4 headings taken from the log itself; T6 fails on 95e1b30) — saingge - process(design-artifacts, conversion-runbook): **Stage 3 runs in parallel by default, from one brief and a mechanical task list**: the lead fixes modules, canonical names and design tokens, writes `docs/progress/stage3-brief.md`, and takes the tasks from the stage-3 lines of `bin/lib/artifact-check.sh` (so no artifact is forgotten); one subagent per 1–2 screens or artifact group, a shared opening line so the start is cached, mechanical tasks on the cheap tier, 5-line replies, the lead the only writer of `PROJECT.md`. The runbook's Stage 3 fan-out row points there, and its Stage 2 row names `facts-lock.sh resolve`. Measured on one project (single runs, Sonnet, 250k auto-compact): serial 13 min / $3.60, parallel 10 min / $4.44, parallel with a brief 6 min / $4.32; 0 name mismatches in the parallel runs (serial dropped 3 entities); both parallel runs forgot the process map and swimlanes, which the task list now covers. Baseline words 64,004 before and after — Stage 3 experiment 2026-10-28 - fix(facts-lock, gate-check): **parallel BRDs keep one spelling per identifier**: `bin/facts-lock.sh resolve` freezes each casing conflict (`WIPStatus`/`WipStatus`) whose majority spelling is used by strictly more BRD sources than any other and records the count in the lock (`resolvedBy`); a tie stays contested, and `build` still never picks on its own. Stage 2 of `gate-check.sh` now reads the lock once there are 2+ BRDs: a BRD that contradicts a frozen identifier FAILs and is quoted; no lock yet is a note, never a FAIL. Measured cause: 20 parallel BRDs on PROJECT-C carried 10 casing conflicts into Stage 3 as open questions, and facts-lock.sh, documented only as advice, reached 1 of 6 projects. Fixtures: `test-facts-lock.sh` resolve cases (4), `test-bug03-gates.sh` T19 (2 of 4 fail on the previous gate-check) — speed/parallel experiments 2026-10-28 - fix(row-groups): **a build plan whose number and Kind cells are formatted as code (`` `BUILD` ``, `` `12` ``) is read like a plain one**: before, it read 0 BUILD rows, so with `Row groups: on` grouping silently did nothing (the fast-lane agent in the 2026-10-28 speed experiment had to strip the backticks by hand). Fixture: `test-row-groups.sh` G5 (both cases fail on the previous row-groups.sh) — speed experiment 2026-10-28 diff --git a/bin/harvest-learnings.sh b/bin/harvest-learnings.sh index d54c4c24..2442423e 100755 --- a/bin/harvest-learnings.sh +++ b/bin/harvest-learnings.sh @@ -70,6 +70,11 @@ BUGS_OUT="$INBOX/$STAMP-$PROJECT_NAME-bugs.md" FOUND_BUGS=0 if [ -d "$PROJECT_DIR/bug-logs" ]; then TMP="$(mktemp)" + # A heading is stripped of ~ * ` before it is matched (below), so the log it is matched against is + # stripped the same way. Without that, a heading the toolkit log carries with a backtick in its + # first 30 characters never matched, and was drafted again. + BUGLOG_PLAIN="$(mktemp)" + tr -d '~*`' < "$TOOLKIT_BUGLOG" > "$BUGLOG_PLAIN" 2>/dev/null find "$PROJECT_DIR/bug-logs" -name '*.md' -type f | sort | while IFS= read -r f; do # Split the file on '## ' headings; test each heading's distinctive text against the # toolkit log. awk emits candidate sections whole, delimited for the outer loop. @@ -86,7 +91,8 @@ if [ -d "$PROJECT_DIR/bug-logs" ]; then # measured: personal-toolkit and the toolkit disagree from 82 up), then take 30 chars. slice="$(printf '%s' "$heading" | sed -E 's/^BUG-[0-9]+[: ]*//' | cut -c1-30)" [ -n "$slice" ] || continue - if ! grep -qiF "$slice" "$TOOLKIT_BUGLOG" 2>/dev/null; then + # -e: stripping can leave a slice that starts with "-", which grep would read as an option. + if ! grep -qiF -e "$slice" "$BUGLOG_PLAIN" 2>/dev/null; then { printf '## [candidate — from %s] %s\n' "${f#$PROJECT_DIR/}" "$heading" printf '%s' "$section" | tail -n +2 printf '\n'; } >> "$TMP" @@ -99,7 +105,7 @@ if [ -d "$PROJECT_DIR/bug-logs" ]; then FOUND_BUGS=1; WROTE=$((WROTE + 1)) echo " wrote ${BUGS_OUT#$TOOLKIT_ROOT/}" fi - rm -f "$TMP" + rm -f "$TMP" "$BUGLOG_PLAIN" fi [ "$FOUND_BUGS" -eq 1 ] || echo " bug-logs: nothing the toolkit log doesn't already carry (or no bug-logs/)" diff --git a/tests/wave2/test-harvest-learnings.sh b/tests/wave2/test-harvest-learnings.sh index 4752af14..23c27ee6 100755 --- a/tests/wave2/test-harvest-learnings.sh +++ b/tests/wave2/test-harvest-learnings.sh @@ -25,6 +25,8 @@ # T5 section 1 drafts every '## ' section of every bug-log file, not only the first of each, # and still skips a later section whose heading the toolkit log carries. Fixture: # fixtures/harvest-learnings/ (see its CAPTURE.md). +# T6 a heading the toolkit log carries is not drafted again when it has ~ * or ` in its first +# 30 characters, or starts with a dash once those are stripped. # # Usage: bash tests/wave2/test-harvest-learnings.sh /path/to/harvest-learnings.sh @@ -196,8 +198,7 @@ else EXPECTED5="$(cat "$FIX5"/bug-logs/*.md | grep '^## ' | sed 's/^## *//; s/[~*`]//g')" N5="$(printf '%s\n' "$EXPECTED5" | grep -c .)" # A later section whose heading the toolkit log already carries must still be filtered out. - # Taken with no ~ * or ` in it: the harvester strips those from a heading before matching it - # against the log, so a heading that has them never matches and is drafted every time. + # A plain heading is enough here; T6 covers the ones with ~ * or `. KNOWN5="$(grep -m1 -E '^## BUG-[0-9]+: [^~*`]+$' "$CLONE/bug-logs/mxcli-bugs.md")" if [ -n "$KNOWN5" ]; then printf '\n%s\n\nA section the toolkit log already carries; never drafted.\n' "$KNOWN5" \ @@ -235,6 +236,41 @@ else fi fi +# --------------------------------------------------------------------------------------------- +# T6: a heading the toolkit log carries is not drafted again when it has ~ * or ` (or starts with +# "-" once stripped). The harvester strips ~ * ` from a project heading before matching it, but searched the raw +# log, which keeps them, so the stripped slice never matched: 103 of the log's own 223 headings were +# drafted again from a project that held nothing but copies of them. A heading that starts with a +# backtick and then "--" starts with "--" once stripped; grep read the slice as an option, which +# also counted as "not found". The headings come from the +# clone's own log, so the check follows the log instead of a copy of it. +# --------------------------------------------------------------------------------------------- +echo "== T6: headings with ~ * \` or a dash left first are recognised as known ==" + +if [ ! -f "$CLONE/bin/harvest-learnings.sh" ] || [ ! -f "$CLONE/bug-logs/mxcli-bugs.md" ]; then + bad "control: no isolated clone with bug-logs/mxcli-bugs.md (see T1) — fixture cannot run" +else + P6="$WORK/knownproj"; mkdir -p "$P6/bug-logs" + { grep -m3 -E '^## BUG-[0-9]+: .{0,26}[~*`]' "$CLONE/bug-logs/mxcli-bugs.md" + grep -m1 -E '^## BUG-[0-9]+: [~*`]-' "$CLONE/bug-logs/mxcli-bugs.md"; } \ + | awk '{print; print ""; print "Copy of a heading the toolkit log carries."; print ""}' > "$P6/bug-logs/known.md" + N6="$(grep -c '^## ' "$P6/bug-logs/known.md")" + if [ "$N6" -lt 4 ]; then + bad "control: the clone's log gave $N6 of the 4 headings this check needs (3 with ~ * \` in their first 30 characters, 1 that starts with a dash once stripped)" + else + printf '\n## BUG-900: a heading no toolkit log has\n\nGenuinely new.\n' >> "$P6/bug-logs/known.md" + OUT6="$(bash "$CLONE/bin/harvest-learnings.sh" "$P6" --to "$WORK/inbox6" 2>&1)"; RC6=$? + DRAFT6="$WORK/inbox6/$STAMP-knownproj-bugs.md" + [ "$RC6" -eq 0 ] && ok "exit 0" || bad "exit $RC6, expected 0" "$OUT6" + GOT6="$(grep '^## \[candidate' "$DRAFT6" 2>/dev/null)" + if [ "$(printf '%s\n' "$GOT6" | grep -c .)" -eq 1 ] && printf '%s' "$GOT6" | grep -qF 'BUG-900: a heading no toolkit log has'; then + ok "$N6 known headings (with ~ * \` in them, or a dash left first) not drafted; the new one is" + else + bad "expected only BUG-900 drafted; got:" "$GOT6" + fi + fi +fi + echo "" echo "PASS=$PASS FAIL=$FAIL ($WORK)" # Every draft this fixture produced lives under $WORK (the clone's contrib/inbox, or the no-git