Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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
- fix(canvas): **`mf-canvas-facts.py` reads a note line's sides by the model's Mendix version, so on Mendix 11 it no longer reports NOTE-SIDE for lines that face their object.** A note numbers its sides top 0, bottom 1, left 2, right 3 on Mendix 10, but like every other object (top 0, right 1, bottom 2, left 3) on Mendix 11; the script knew only the Mendix 10 table. It now reads `_ProductVersion` from the `.mpr`'s `_MetaData` table (major 11 or higher: the activity numbering; unreadable: Mendix 11) and prints `# note sides: …` as its first line. One app's 305 hand-drawn note lines, matched across its 10.24 and 11.15 versions: the upgrade renumbered the note's side on every line (0 to 0, 1 to 2, 2 to 3, 3 to 1) and left the object's side alone, and the script's fact counts for the whole app are now identical on both. mxcli v0.28.0 (mendixlabs/mxcli#1620) writes the activity numbering whatever the project's version. Fixture section V (renumbered copy of the golden model). — 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 <project> 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
Expand Down
10 changes: 8 additions & 2 deletions bin/harvest-learnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand All @@ -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/)"

Expand Down
40 changes: 38 additions & 2 deletions tests/wave2/test-harvest-learnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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" \
Expand Down Expand Up @@ -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
Expand Down
Loading