From 9dfffcdf609cf2b9573bf70b850f0c60ad4cd7d1 Mon Sep 17 00:00:00 2001 From: saingge <262679532+saingge@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:50:55 +0100 Subject: [PATCH] fix(harvest-learnings): a bug-log heading the toolkit log carries is not drafted again when it has ~ * or ` Section 1 strips ~ * ` from a project heading, takes the first 30 characters after a BUG-nnn tag and searches bug-logs/mxcli-bugs.md for them. The log was searched as written, so the stripped text was never a substring of a toolkit heading that has those characters (BUG-01 starts with a backtick pair), and the heading was drafted again. The log is now stripped the same way before the search. The search also takes -e: stripping leaves BUG-26 and BUG-54 starting with "--", which grep read as an option. A project whose bug log held copies of all 223 headings in the toolkit log drafted 103 of them at 95e1b30 and 0 now. Fixture: tests/wave2/test-harvest-learnings.sh T6, with four headings taken from the log itself; it fails on 95e1b30. Ported from #594 onto current master; the only conflict was the CHANGELOG insertion point, and both lines are kept. Re-verified on master 0db1c0e2. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018LeNWZV2sVH62vkgfYh5u5 --- CHANGELOG.md | 1 + bin/harvest-learnings.sh | 10 +++++-- tests/wave2/test-harvest-learnings.sh | 40 +++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e72b6d..e5496a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m - fix(page-audit, design-audit): **the audits produce their own `.claude/loop/page-scope.json`**: when it is missing or older than the model they run `bin/page-scope.sh` (`scope-ensure.js`, 2 s) instead of FAULTing on 0 pages; a failed regeneration never blanks an existing file and names the failure. Field: scope deleted -> page-audit ran on 9 pages, `touch` of the .mpr -> regenerated once — speed experiment E2 - new(look-ledger, journey-runner, module-review): **a journey run's screenshots are LOOK evidence**: `bin/look-ledger.sh shots` (run by journey-runner after every run) maps each reached step's PNG to the one page whose widgets (`DESCRIBE PAGE`) it landed on, a URL pattern breaking ties, and lists every owed page as `OPEN` (open that PNG; the Read hook then marks the page SEEN) or `UNREACHED` (short gap-fill journey, or `page-audit.js` as before) with the line `owed N = A OPEN + B UNREACHED`; a shot older than the page's last build is not evidence and nothing is claimed when unsure. Field: 8 owed pages = 5 OPEN + 3 UNREACHED from two journey runs, no separate screenshot pass; baseline words 64,004 -> 64,193 — speed experiment E2 - fix(affected-tests): **a page, flow or navigation change no longer selects 0 journeys because the journeys name widgets, not `Module.Name`**: the affected scope now reads the widget names of every changed page/snippet and of each calling page (`DESCRIBE PAGE`, one extra warm-catalog `mxcli` call, 0.07 s) and selects each journey whose JSON holds one; a navigation or layout change selects every journey; when callers or widgets cannot be measured (`widgets NOT MEASURED` row) it selects every journey (unsure means include). Field: 067 (Request_Edit page) 0 -> J-REQ-01, 090 (navigation) 0 -> all 5 journeys — speed experiment E2 +- 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