Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
861be36
fix(check): count joins inside a REST call's error handler (MDL-FLOW02)
ako Oct 8, 2026
65a9fb1
fix(check): a void microflow/nanoflow call's output name declares not…
ako Oct 8, 2026
ca7562b
fix(check): MDL004 on a stored void microflow's return value is a war…
ako Oct 8, 2026
230dd5c
fix(xpath): store an underscore-led enumeration value as a string lit…
ako Oct 8, 2026
d11a40f
fix(exprcheck): lex `:` as Mendix division so comparisons over it typ…
ako Oct 8, 2026
32693c7
perf(check): read the project settings only for a script that writes …
ako Oct 8, 2026
569a2c9
perf(check): list only the document kinds the script's statements ask…
ako Oct 8, 2026
aea08ee
Merge origin/main into fix/check-fp-rest-handler-joins
ako Oct 8, 2026
3246b52
Merge origin/main into fix/check-fp-void-microflow-output
ako Oct 8, 2026
5d4c53b
Merge origin/main into fix/check-fp-void-return-value
ako Oct 8, 2026
8fd7f9d
fix(describe): join a crossed merge wherever a branch reaches it
ako Oct 8, 2026
3f23d3f
fix(describe): print a region two case arms share once, not once per arm
ako Oct 8, 2026
b51bce0
fix(describe): keep a loop's back-edge when its header sits in a branch
ako Oct 8, 2026
7e7cec7
Merge origin/main into fix/check-fp-xpath-underscore-enum
ako Oct 8, 2026
41ce03c
Let MDL set module documentation and the admin user name (#1314)
claude Oct 8, 2026
9c122dd
test: pin #1182 — SET Caption and snippet restate with nl_NL stored f…
claude Oct 8, 2026
a1f5102
docs(wiki): the rebuild comparison is describe's missing oracle
ako Oct 8, 2026
02194b0
chore(findings): one file per finding instead of shared .jsonl shards
ako Oct 8, 2026
ef36637
docs(findings): point the skills, commands, CLAUDE.md and wiki at the…
ako Oct 8, 2026
cf534de
docs(claude-md): keep the findings wording within the context budget
ako Oct 8, 2026
d57737a
Merge origin/main into chore/findings-one-file-per-record
ako Oct 8, 2026
0c332e7
Merge origin/main into fix/describe-irreducible-graphs
ako Oct 8, 2026
0f8a0c2
fix: alter page/snippet can add and drop parameters (mendixlabs/mxcli…
claude Oct 8, 2026
1102636
fix(describe): print an error handler two activities share, once
ako Oct 8, 2026
15804ae
fix(describe): end the iteration where a loop-body arm stops dead
ako Oct 8, 2026
f645b1c
fix(describe): walk an error handler through a merge with one way in
ako Oct 8, 2026
3e8ef44
fix(describe): keep a loop's back-edge when its header is a split's join
ako Oct 8, 2026
041d6f7
fix(describe): join every crossing of an interleaved if whose entries…
ako Oct 8, 2026
a7abd49
docs(wiki): a flow describer is several walks that have to agree
ako Oct 8, 2026
0037b0c
Merge origin/main into fix/check-fp-rest-handler-joins
ako Oct 8, 2026
ddf39aa
Merge pull request #1045 from ako/fix/check-fp-rest-handler-joins
ako Oct 8, 2026
9027b56
Merge origin/main into fix/check-fp-void-microflow-output
ako Oct 8, 2026
746fa7c
Merge origin/main into fix/check-fp-void-return-value
ako Oct 8, 2026
436b945
Merge origin/main into fix/check-fp-xpath-underscore-enum
ako Oct 8, 2026
f5215d1
Merge origin/main into fix/check-conflict-scan-speed
ako Oct 8, 2026
588a2f1
Merge origin/main into chore/findings-one-file-per-record
ako Oct 8, 2026
cda5b5c
Merge pull request #1046 from ako/fix/check-fp-void-microflow-output
ako Oct 8, 2026
0a11394
Merge origin/main into fix/check-fp-void-return-value
ako Oct 8, 2026
76ebb89
Merge pull request #1047 from ako/fix/check-fp-void-return-value
ako Oct 8, 2026
203bc36
Merge origin/main into fix/check-fp-xpath-underscore-enum
ako Oct 8, 2026
0416cbc
Merge origin/main into chore/findings-one-file-per-record
ako Oct 8, 2026
7e44674
Merge pull request #1054 from ako/chore/findings-one-file-per-record
ako Oct 8, 2026
49a2f57
Merge origin/main into fix/check-fp-xpath-underscore-enum
ako Oct 8, 2026
4eb5c06
Merge origin/main into fix/exprcheck-colon-division
ako Oct 8, 2026
3e04f84
Merge origin/main into fix/check-conflict-scan-speed
ako Oct 8, 2026
f860eef
Merge pull request #1048 from ako/fix/check-fp-xpath-underscore-enum
ako Oct 8, 2026
79aca4a
Merge origin/main into fix/describe-irreducible-graphs
ako Oct 8, 2026
d73e2a8
Merge origin/fix/describe-irreducible-graphs into fix/describe-graph-…
ako Oct 8, 2026
90440a1
Merge remote-tracking branch 'origin/main' into claude/mxcli-issue-11…
claude Oct 8, 2026
539152e
Merge remote-tracking branch 'origin/main' into claude/mxcli-issue-12…
claude Oct 8, 2026
220fb6d
Merge pull request #1049 from ako/fix/exprcheck-colon-division
ako Oct 8, 2026
ee35adc
Merge pull request #1050 from ako/fix/check-conflict-scan-speed
ako Oct 8, 2026
371387e
Merge pull request #1055 from ako/fix/describe-irreducible-graphs
ako Oct 8, 2026
09da639
Merge remote-tracking branch 'origin/main' into claude/mxcli-issue-13…
claude Oct 8, 2026
e2e985d
Merge origin/main into fix/describe-graph-invariant-remaining
ako Oct 8, 2026
3d557be
Merge pull request #1058 from ako/claude/mxcli-issue-1314-5to4h3
ako Oct 8, 2026
0ae279d
Merge pull request #1059 from ako/claude/mxcli-issue-1182-xlma7z
ako Oct 8, 2026
9fcef62
Merge pull request #1057 from ako/claude/mxcli-issue-1234-2z9io3
ako Oct 8, 2026
b0ec519
fix: keep an empty decision caption through describe -> exec (#1254) …
ako Oct 8, 2026
1f1ef87
Merge pull request #1061 from ako/fix/describe-graph-invariant-remaining
ako Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .claude/commands/mxcli-dev/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ is the order to do things in.

2. **Match the failure class**, then the instance:
- `docs-wiki/bug-patterns/` for the class (small, read it)
- `grep -il '<CE code or keyword>' .claude/skills/fix-issue/findings/*.jsonl`
- `grep -ril '<CE code or keyword>' .claude/skills/fix-issue/findings/`
for the instance

A pattern-page miss means "not yet digested", never "not seen before".
Expand Down Expand Up @@ -59,7 +59,7 @@ is the order to do things in.
7. **Add the regression case**: `mdl-examples/bug-tests/<issue>-<description>.mdl`,
and check it parses (`mxcli check`) and passes `make check-mdl`.

8. **Append one finding** to `.claude/skills/fix-issue/findings/<area>.jsonl` and run
8. **Add one finding file**, `.claude/skills/fix-issue/findings/<area>/<date>-<slug>.json` (one JSON object on one line; never append to an existing file), and run
`make check-findings`. Write the insight — what would have made this cheaper to
find, and which plausible wrong turn to skip — not the changelog.

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/mxcli-dev/wiki-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This command is the trigger; the skill is the contract.
For `bug-patterns/` specifically, run **`make digest-status`** first: it reports
how many findings have landed since the last bug-pattern sync and which areas
no page mentions. That is the scope question for this category — the pages
digest `.claude/skills/fix-issue/findings/*.jsonl`, and an area with many
digest `.claude/skills/fix-issue/findings/*/*.json`, and an area with many
findings and no page is an undigested failure class, not a missing file.

If invoked with no arguments, ask the user which page(s) to sync. List the
Expand Down
57 changes: 30 additions & 27 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

A fast-path workflow for diagnosing and fixing bugs in mxcli.

Each fix appends one finding to `findings/*.jsonl`. Those are **evidence, not
reading material**: 630 findings at ~1.7 KB each. The entry point for a diagnosis
Each fix adds one finding file under `findings/<area>/`. Those are **evidence, not
reading material**: over 1,500 findings at ~1.7 KB each. The entry point for a diagnosis
is [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/), which digests them
into failure classes; the findings are where you drill down for the instance.

Expand Down Expand Up @@ -45,39 +45,35 @@ into failure classes; the findings are where you drill down for the instance.
> `.gitattributes` gave `merge=union` to the whole file, which kept two concurrent
> appends instead of conflicting. That was the right driver on the wrong unit: union
> applies file-wide, so two branches editing the same *prose* line would silently
> keep both. The note that shipped with it called this exact shot — "if that starts
> happening, split the table into its own file so `union` covers only append-only
> content" — and the union driver now sits on `findings/*.jsonl`, where every line is
> an independent record and keeping both sides is always correct.
>
> It never solved the conflicts anyway: GitHub's server-side merge does not run merge
> drivers, so every PR still had to merge `main` locally first. Sharding by area is
> what actually reduces them — two fixes now collide only when they touch the same
> area.
> keep both. The table moved to `findings/<area>.jsonl` shards with the union driver
> on them, which still left every fix-PR conflicting on GitHub: its server-side
> merge does not run merge drivers, and nearly every fix appends to the same
> `mdl-executor` shard. The findings are now one file per finding, so two fixes add
> two different files and nothing conflicts at all. History in
> [`findings/README.md`](fix-issue/findings/README.md).
---

## Finding a prior fix

The findings live in `findings/*.jsonl`, one JSON object per line, sharded by
area. They are **data, not reading material**: 630 findings at ~1.7 KB each do
not fit in a context window, and the table they used to live in had grown past
the point where GitHub's web editor would open it.
The findings live in `findings/<area>/<date>-<slug>.json`, one finding per
file, each a single JSON object on a single line. They are **data, not reading
material**: over 1,500 findings at ~1.7 KB each do not fit in a context window.

Grep is the fast path — the shard name narrows it, and every line is
self-contained:
Grep is the fast path. The directory narrows it by area, and every file is one
self-contained line:

```bash
grep -il 'CE0463' .claude/skills/fix-issue/findings/*.jsonl # which areas
grep -h 'CE0463' .claude/skills/fix-issue/findings/mdl-executor.jsonl | jq .
grep -ril 'CE0463' .claude/skills/fix-issue/findings/ # which findings
grep -rh 'CE0463' .claude/skills/fix-issue/findings/mdl-executor/ | jq .
```

For anything with a shape to it, the records have fields — `area`, `symptom`,
`cause`, `file`, `insight`, plus `refs` / `ce` / `rules` where the text carried
them — and DuckDB reads the files in place, no import step:

```bash
duckdb -c "select symptom, file from '.claude/skills/fix-issue/findings/*.jsonl'
duckdb -c "select symptom, file from read_json_auto('.claude/skills/fix-issue/findings/*/*.json')
where list_contains(ce, 'CE0463')"
```

Expand All @@ -90,18 +86,25 @@ small enough to read. Start there, come here for the instance.

## Recording a fix

Append one line to the shard for the area you touched — a new shard is fine if
none fits. Keep it one line: `merge=union` in `.gitattributes` resolves two
concurrent appends by keeping both, which is correct for a file of independent
records and is not correct for prose.
Add one new file in the directory for the area you touched (a new directory is
fine if none fits), named `<YYYY-MM-DD>-<slug>.json`, with the slug a few words
of the symptom. Keep the record on **one line**, so `grep -h` returns it whole.
Never append to another finding's file: one file per finding is what keeps two
fix-PRs from conflicting, since GitHub's merge cannot resolve two appends to
the same file.

```bash
cat >> .claude/skills/fix-issue/findings/mdl-executor.jsonl <<'JSON'
cat > .claude/skills/fix-issue/findings/mdl-executor/2026-08-31-describe-drops-sort-order.json <<'JSON'
{"area":"mdl/executor","date":"2026-08-31","symptom":"...","cause":"...","file":"...","insight":"...","refs":["#123"]}
JSON
make check-findings
```

If a merge with `main` brings back a `findings/<area>.jsonl` file (a branch from
before the split), run `scripts/split-findings.py`. It turns those lines into
files and skips any that already exist; `check-findings` refuses the stray
`.jsonl` until you do.

`check-findings` prints one line saying how far `docs-wiki/bug-patterns/` has
fallen behind; `make digest-status` breaks it down by area. **If the class of
failure keeps recurring, sync its pattern page** (`/mxcli-dev:wiki-sync
Expand All @@ -128,7 +131,7 @@ Step 1: write a failing test at the layer the symptom lives in
Step 2: confirm it fails — and that it fails with the REPORTED symptom
Step 3: implement the minimum code to make it pass
Step 4: go test ./... (or the packages you touched)
Step 5: append a finding to findings/<area>.jsonl
Step 5: add a finding file under findings/<area>/
```

Where the test goes, by layer:
Expand Down Expand Up @@ -216,5 +219,5 @@ Before writing "cannot":
- [ ] Any "cannot be verified" claim carries its evidence, and a control that could have falsified it
- [ ] `make test && make lint` pass
- [ ] `mdl-examples/bug-tests/<issue>-<description>.mdl` added for the regression case
- [ ] New finding appended to `findings/<area>.jsonl` (if not already covered), and `make check-findings` passes
- [ ] New finding file added under `findings/<area>/` (if not already covered), and `make check-findings` passes
- [ ] PR title: `fix: <one-line description matching the symptom>`
80 changes: 47 additions & 33 deletions .claude/skills/fix-issue/findings/README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,73 @@
# Findings

One JSON object per line, sharded by area. 630 records, extracted from the
symptom table that used to live inside `fix-issue.md` — see that file for how to
search them and how to add one.
One file per finding, in a directory per area:
`<area>/<YYYY-MM-DD>-<slug>.json`, holding one JSON object on one line. The
records were extracted from the symptom table that used to live inside
`fix-issue.md` — see that file for how to search them and how to add one.

## Why JSONL, and why sharded
## Why one file per finding

The table had grown to 1.05 MB. That is past a context window, past what
GitHub's web editor will open, and past what the wiki's own `wiki-sync` can
consume (its Phase 2 requires reading a source in full) — so the digest step that
turns findings into `docs-wiki/bug-patterns/` pages had been blocked since the
day it was created.
The findings started as a Markdown table inside `fix-issue.md`, which grew to
1.05 MB: past a context window, past what GitHub's web editor will open, and
past what `wiki-sync` can consume. They then became nine `<area>.jsonl` shards,
one record per line, with `merge=union` in `.gitattributes` so two fixes
appending to the same shard kept both lines.

Line-oriented records fix all three at once. A shard is small enough to open and
grep; `merge=union` is *correct* on a file of independent records, where on the
old mixed prose-and-table file it was a hazard; and a query can pull one area at
a time instead of the whole corpus.
That fixed the size and did not fix the conflicts. GitHub's server-side merge
does not run merge drivers, so every fix-PR that appended to a shard conflicted
with every other open one the moment one of them merged — measured on
2026-10-08, eight open PRs re-conflicting after each merge, each needing a
local merge of `main`, a rebuild and a re-test. Sharding only lowered the odds;
almost every fix lands in `mdl-executor`. And `union` had quietly done damage:
six records were in that shard twice, byte for byte, from merges that kept both
sides of the same line. The split dropped the duplicates.

Nine shards rather than one file per finding: 630 files would trade a
too-large file for a directory nobody can scan, and the conflicts that motivated
sharding only happen between fixes touching the *same* area.
With one file per finding, two fixes add two different files. There is nothing
to merge, so nothing conflicts, on GitHub or anywhere else. The cost the shard
design was avoiding, "a directory nobody can scan", is not one: nobody scans it.
The findings are looked up by `grep` or DuckDB, both of which take a glob.

The split (`scripts/split-findings.py`) wrote each original line verbatim as a
file, so it is lossless by construction: the multiset of file contents equals
the multiset of unique original lines, checked byte for byte.

## Fields

| field | |
|---|---|
| `area` | package the fix landed in, e.g. `mdl/executor`. Decides the shard |
| `date` | when the finding was last touched, `YYYY-MM-DD`. Backfilled by `git blame` over the table this came from, so it is *last touched*, not *first written* — a row that was later corrected carries the correction's date. Used by `make digest-status` to measure how far the wiki digest has fallen behind |
| `area` | package the fix landed in, e.g. `mdl/executor`. The directory is the coarse shard (`mdl-executor/`); `area` is the precise package |
| `date` | when the finding was last touched, `YYYY-MM-DD`. Also the file name's prefix when the file was written. The older records were backfilled by `git blame` over the table they came from, so for those it is *last touched*, not *first written*. Used by `make digest-status` to measure how far the wiki digest has fallen behind |
| `symptom` | what the user saw — the thing you match against |
| `cause` | the mechanism |
| `file` | where to look first |
| `insight` | what would have made it cheaper to find; the part worth reading |
| `refs`, `ce`, `rules` | issue/PR refs, Mendix `CE####` codes, MDL rule ids — extracted from the text, present only where the text carried them |
| `raw` | the original table row, for the 68 records whose row could not be split into four columns (an unescaped `\|` in the text, or a row that never had four cells). `symptom`/`cause`/`file`/`insight` are absent on these |
| `raw` | the original table row, for the records whose row could not be split into four columns (an unescaped `\|` in the text, or a row that never had four cells). `symptom`/`cause`/`file`/`insight` are absent on these |

A record has **either** the four structured fields **or** `raw`. Queries that
must cover everything need to account for both.

## Guard
## Naming

`make check-findings` (`scripts/check-findings.sh`) validates every line: one
JSON object, an `area`, a `date`, and either the four fields or `raw`. It prints
one line saying how far `docs-wiki/bug-patterns/` has fallen behind these
findings — see `make digest-status` for the breakdown.
`<date>-<slug>.json`: the record's `date`, then a slug of its symptom
(lowercase words joined by hyphens, about 60 characters at most). The name is
for a human skimming `ls`; nothing parses it beyond the pattern check. If the
name is taken, add `-2`.

It runs in CI as of the change that added this sentence. It did not before,
for the two weeks this file claimed it did — which is the same shape as every
other finding here, so it is recorded rather than quietly corrected.
## Guard

The extraction was verified lossless by regenerating every table row from the
records and diffing against the original 630 — byte-identical as a multiset.
Order carries no meaning: these are looked up by matching a symptom.
`make check-findings` (`scripts/check-findings.sh`, run in CI) validates every
file: the name pattern, exactly one line, one JSON object, an `area`, a `date`,
and either the four fields or `raw`. It also **refuses any `*.jsonl` left in
this directory**. A branch written before the split brings its shard file back
on its next merge with `main`; `scripts/split-findings.py` converts the lines
into files, skipping those that already exist. It prints one line saying how
far `docs-wiki/bug-patterns/` has fallen behind these findings; see `make
digest-status` for the breakdown.

## On DuckDB

The files are newline-delimited JSON, which DuckDB reads in place over a glob —
no import, no schema, no build step. It is not vendored and not required: `grep`
plus `jq` covers the common lookup, and the guard above only needs `python3`.
DuckDB reads the files in place over a glob, with no import, schema or build
step: `select … from read_json_auto('.claude/skills/fix-issue/findings/*/*.json')`.
It is not vendored and not required: `grep` plus `jq` covers the common lookup,
and the guard above only needs `python3`.
Loading
Loading