Skip to content

Tell the agent to map the build type to flags, not to a compilation mode - #48

Open
anakanemison wants to merge 17 commits into
mschaller/u12-reference-configurationfrom
mschaller/u13-build-type-to-flags
Open

anakanemison wants to merge 17 commits into
mschaller/u12-reference-configurationfrom
mschaller/u13-build-type-to-flags

Conversation

@anakanemison

@anakanemison anakanemison commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Agents map Release to -c opt and lose the numbers: CMake's Release is -O3 -DNDEBUG and Bazel's opt is -O2, and CMake's Debug -g has no counterpart in fastbuild. A new "Build type" section in docs/REFERENCE-cmake.md gives the mappings that differ, where to read the reference's actual level, the .bazelrc lines that pin it, and that opt's -DNDEBUG turns off assertions the reference may keep.

In any2bazel-lab, eight of nine pono migrations compiled at -O2 against an -O3 reference, and every lensfun migration dropped -g. In later runs with this change, pono kept -O3, lensfun kept -g, and CMake's sources gained neither -O2 nor NDEBUG in three runs.

Stacked on #47 (tracking issue: #35). pytest tests/: 186 passed (132 on main). Lab proposal: P18.

🤖 Generated with Claude Code

anakanemison and others added 17 commits November 2, 2026 12:26
bazel_args is meant for the flags that make aquery mirror the real build
(--config=release, -c opt). Anything that consumes it splices it into a
bazel command line, but nothing checked its shape, and the SKILL.md text
sits next to a literal `bazel aquery ...` line that is easy to paste in.

Measured in any2bazel-lab: on a QGIS migration the agent recorded the whole
aquery command as bazel_args. Its own diff never read the field, so the
migration looked converged, while a consumer that spliced the field into
`bazel aquery` failed on a second query expression and produced no parity
result.

SKILL.md now says the field holds flags, with one right and one wrong
example, and runs `bazel canonicalize-flags --for_command=aquery` on it
just before aquery. Bazel refuses anything that is not an aquery option,
including a target pattern and a stray word after a boolean flag
(`--keep_going true`), which a hand-written check would have to
special-case. The one-liner hands the list to Bazel as argv (os.execvp),
so a value with a space in it is not split. It does not check that a
--config exists; aquery fails on one that does not.

Checked against Bazel 9.3.1: it refused a whole aquery command and
`--keep_going true`, and accepted `--copt=-DMSG="a b"`, `-c opt`,
`--config=release` and a config with no bazel_args. The lab's later QGIS
run, made with an earlier version of this change (a Python check),
recorded only valid flags; this version has not been through a migration
run.

Lab-Proposal: P22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exclude_targets holds CMake names. The diff applies target_map before
deciding participation, so a Bazel target mapped to an excluded CMake name
is skipped. excluded_summary, which reports what was skipped and why,
checked the raw Bazel label instead, so that target appeared nowhere in
`excluded.bazel`: skipped, but not on the record.

A new helper, _bazel_renames, returns each Bazel label with the name the
diff compares it under (today, the target_map entries whose label exists),
and is the one place that decision is made. diff_models renames the Bazel
views with it and, as before, also applies every explicit target_map entry
to dependency references (a map entry can align an external dependency
that has no target node). excluded_summary checks exclusion on the renamed
name while still listing the target under its Bazel label. The per-side
summaries later in this series use the same helper, so they count exactly
the targets the diff compares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_union_tus pooled library TUs into one source-keyed map with
dict.setdefault while iterating a set of target names. When one source is
compiled into several libraries with different flags (a full and a lite
build of the same file, a hardened variant), only one variant per side was
compared, and which one depended on set iteration order, that is, on
PYTHONHASHSEED. The verdict was not reproducible.

Measured in any2bazel-lab: the same MySQL Server models gave 4,715 to 4,817
errors over 10 hash seeds, a different error set each time, and the final
models of a Blender migration converged under 2 of 8 seeds. Projects
without multi-variant sources were unaffected (50 recorded verdicts
identical under 20 seeds).

Pooling now keeps every distinct variant per source, visiting targets in
sorted order. Each CMake variant is compared with its best-matching Bazel
variant: fewest errors, then warnings, then the smallest symmetric
difference of the two TUs' define, flag and include sets, then a stable
text order. Every CMake variant must be matched by some Bazel variant and
nothing is suppressed; an extra Bazel-only variant is tolerated, like an
extra Bazel flag. The distance term keeps a tie from choosing a variant
that only adds flags (-w, -Wno-error=...) over an exact one. A source with
one variant per side takes exactly the old path, and on every
single-variant verdict the lab has recorded, the result is unchanged.
Executables and the opt-in test union use the same pairing, and the Java
source union is visited in sorted order too.

One consequence for source_map users: a token that collapses several CMake
TUs (an AUTOMOC bundle, say) now yields one comparison, and so possibly one
discrepancy, per distinct CMake TU, so the worklist can grow. The
source_map comment in config.py says so.

In the lab's later QGIS run with this change, the differ gave the same
verdict under three seeds.

Lab-Proposal: P14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flag comparison is an asymmetric subset: a CMake flag missing on the
Bazel side is an error, and a flag only Bazel has is tolerated. That is the
right policy, since Bazel's toolchain injects flags no CMake build has, but
nothing in the output said so. "0 errors" reads as "both builds compile the
same way" when it means "no CMake flag is missing on the Bazel side", and a
flag the Bazel side added appeared nowhere unless that TU also had a real
discrepancy.

Measured in any2bazel-lab: one migration's Bazel side carried 15 distinct
compiler warning flags against CMake's one, including ten -Wno-error= demotions the agent
added to get past -Werror, and the diff reported 0 errors and 0 warnings
with an empty ignore block. Those demotions were justified; an unjustified
-w would have looked the same.

diff_models takes an optional DiffNotes collector. For every compared TU
pair (the chosen variant pair) it counts the compile flags only Bazel has,
after the ignore lists and minus the toolchain defaults already hidden from
bazel_only; for every aligned executable, it does the same for link flags.
summarize() reports them as "bazel_only_flags" (flag -> compared TU pairs,
or executables) with a note that they are not compared; triage.py prints a
one-line summary with the top entries; SKILL.md says so where diff.json is
described. Severity and `converged` are unchanged.

Not done: raising warning-control flags the Bazel side adds (-w, -Wno-*,
-Wno-error=*) to WARN. That changes what the tool counts, so it is left for
the maintainers to decide.

In the lab's later graphicsgems run with this change, the agent's report
named its Bazel-only flags; the earlier run's report had not.

Lab-Proposal: P17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
canonicalize.py drops -O* and -g* from both sides before comparing. That is
the right rule for a per-TU diff, since the level is a build-wide setting,
but nothing else looked at it, so the tool reported parity for builds
compiled at a different optimization level than the reference, and SKILL.md
told the agent the flags were "never your concern".

Measured in any2bazel-lab across its first 35 judged migrations, with and
without the skill: seven are of pono, whose reference compiles all 94 TUs at
-O3, and six of those seven compile at -O2, each reported as 0 errors.
lensfun's reference sets -g on all 40 TUs, and every migration lost it. On
MySQL Server one migration flattened the reference's 201 -O3 TUs to -O2.

summarize() now adds "levels": per side and per language (C, CXX, OBJC,
OBJCXX, by source extension), histograms of the optimization level, the
debug level and the debug format each compared compile action carries. The
last flag of each kind wins, and "(none)" means absent. Level and format are
kept apart because they vary independently: `-g3 -gdwarf-4` and
`-g1 -gdwarf-4` share a format, and only -g3 emits macro information. A
-gdwarf-N with no level flag counts as -g, since it enables debug info.
"differs" is set when, for a language both sides compile, any of the three
differs. It reads the raw argv the models already hold, over exactly the
targets the diff compares (participating roles, minus exclude_targets on
the name the diff compares under, minus Bazel header-processing actions),
so no new extraction is needed. It never affects errors or `converged`.
triage.py prints the levels, and SKILL.md says where to look instead of
calling the flags no concern.

In the lab's later runs with this series, pono kept -O3 (8 of 9 earlier
pono migrations were at -O2), lensfun and openttd kept -g (no earlier
lensfun migration had), and xbmc kept its reference's -O3.

Lab-Proposal: P16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
canonicalize_flags collected every -D into the define set and passed -U
through as an ordinary flag, so an undefine never removed anything. A
Bazel build that keeps assertions on with `build --copt=-UNDEBUG` after
-c opt's -DNDEBUG was reported as defining NDEBUG (a WARN on every TU),
and one that turned assertions back on against a reference compiled with
-DNDEBUG converged with 0 errors. Later commits in this series report
NDEBUG state and recommend exactly that -UNDEBUG, so it has to be read
correctly.

- canonicalize_flags applies `-U NAME` / `-UNAME` as the compiler does: it
  removes every NAME define collected so far, and a later -D defines it
  again. A -D likewise replaces every earlier definition of its name, so
  each side ends with the last word per macro: `-DF(x)=1 -DF(x)=2` and
  `-DF(x)=1 -UF -DF(x)=2` both leave F(x)=2 and compare equal. `-UNAME` is
  also kept as a comparable flag iff the -U is the last word on NAME (a
  later -D clears it), so whether it survives never depends on what either
  side's toolchain passed before it. Bazel's -c opt preamble
  (-D_FORTIFY_SOURCE=1 -DNDEBUG) followed by a project's
  `-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2` now compares equal to the
  reference (before this change it produced a discrepancy, in either
  direction). A last-word -U still compares a -U of a compiler-predefined
  macro (-U_FORTIFY_SOURCE alone). A define is matched by its macro name,
  the identifier before any `(` or `=`, so `-UF` cancels a function-like
  `-DF(x)=x`, as `gcc -E` shows.
- A define with no value means value 1 to the compiler, so `-DX` is
  spelled `X=1` (and `-DF(x)` is `F(x)=1`); `-DX -DX=1` and `-DX=1 -DX`
  compare equal. Define values in diff.json now show that spelling.
- extract_cmake.py synthesizes each compile argv in the order CMake's
  generators emit it ($DEFINES $INCLUDES $FLAGS, as in CMake 3.28's
  Makefile and Ninja output) instead of flags first, so a -UNDEBUG in
  CMAKE_CXX_FLAGS lands after, and cancels, an NDEBUG compile definition,
  as it does in the real command. It does not cancel Release's -DNDEBUG,
  which comes later in the same flags fragment; only a compile definition
  is cancelled. Include order changes too: structured include directories
  now precede any -I from the flags fragments. That has no verdict impact,
  since includes are compared for presence, not order.

Checked against the preprocessor: a differential test generates fixed-seed
random -D/-U sequences over object-like and function-like names (joined
and split forms, with and without values, repeats) and requires two
sequences to canonicalize to the same define set exactly when `cc -E`
expands a probe source identically; it is skipped when no C compiler is
installed. A larger run of the same check (3,000 sequences, 4.5 million
pairs, three seeds) found no disagreement in either direction. One class
is documented instead of fixed: a function-like macro that differs only by
parameter names (F(x)=x vs F(y)=y) preprocesses the same but reports a
define difference.

Trade-off: a reference compiled with `-DNDEBUG -UNDEBUG` against a Bazel
side with neither now shows a cmake-only `-UNDEBUG` flag error. It is
benign (both sides compile without NDEBUG); copy the flag into copts, or
add it to ignore.flags (ignore.defines does not reach it: it is a flag).

Tests cover valueless defines, last-word -U in both orders, redefinition
with and without an intervening -U, function-like macros in both orders,
the predefined-macro case, the fortify override against Bazel's opt
preamble in both directions, both NDEBUG directions through the diff, and
the CMake argv order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defines are compared both ways, but a define only the Bazel side has is a
WARN on each TU, and no summary line showed it. The one that matters most is
NDEBUG: Bazel's `-c opt` adds -DNDEBUG, so a migration that pins
--compilation_mode=opt against a reference that keeps assertions turns them
off everywhere and still converges.

Measured in any2bazel-lab: four judged migrations compile with NDEBUG
against a reference that passes none, and nothing in the differ's verdict
or summary said so. Three migrations of CMake's own sources got it from
`-c opt`, on every compile action; a GDAL migration passed -DNDEBUG itself,
on 1,534 of 1,534 actions, at no optimization level.

- bazel_only_flags gains "defines": each define only the Bazel side has,
  after ignore.defines, with the compared TU pairs it appears on. Its note
  says these are a WARN, not an error; triage prints the count and the top
  entries.
- A new "ndebug" block sits next to "levels". Per side, it counts the
  compared compile actions that leave NDEBUG defined. With the diff's
  notes, "bazel_added" and "bazel_missing" count the compared TU pairs
  where only Bazel, or only CMake, defines it, and "differs" is true when
  either count is nonzero; a Bazel-only TU that is never compared cannot
  flip it. Both read the canonicalized defines, which apply -U in order (the
  previous commit), so a `build --copt=-UNDEBUG` after -c opt counts as
  assertions on. The counts ignore ignore.defines on purpose, so an accepted
  suppression cannot hide assertion state. triage.py prints one NDEBUG line
  after the levels.
- SKILL.md mentions ndebug under "Diff against reference build". The
  guidance on opt's -DNDEBUG is in the build-type commit.

Severity and `converged` are unchanged: a Bazel-only NDEBUG is still the
existing WARN. Whether it should be an error, since it changes what the
built program does, is a decision for the maintainers.

In the lab's later CMake runs with this series, none added NDEBUG against
the reference's none (three runs; two earlier runs had added it).

Lab-Proposal: P17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A configure_file() output never reaches a command line, so a value that
differs only inside one (a config.h define, a version string) is invisible
to the diff. The skill said so in a note beside the CMake extraction
commands, but diff.json, the artifact people act on, said nothing, and
`converged: true` read as covering them.

Measured in any2bazel-lab: lensfun's generated config.h differed from the
reference in 3 of 7 migrations with the skill, each with a converged diff.
configured_files does populate (the lab saw it on another project); the
empty lists on lensfun, which spells the command CONFIGURE_FILE, come from
the uppercase bug fixed earlier in this series.

diff_models now appends one WARN discrepancy, kind
configured_files_not_compared, target "<configure>", when the CMake model
records configured files: cmake_only lists them, and the detail gives the
count and how many land on a compile include path. It never blocks
`converged`. One aggregated entry rather than one per file keeps a large
project's worklist readable (MySQL Server has about 200). All project-owned
configured files are listed, not only those flagged as compile inputs,
because that flag depends on the output sitting under a known include
directory and can miss a header reached another way. A new end-to-end test
covers the path from a trace through extract_cmake and the model to the
diff.

In the lab's later runs with this series, agents byte-compared the listed
files on lensfun, CMake and QGIS; the QGIS agent wrote eleven Bazel tests
that do it, and CMake's three configure headers that had differed from the
reference came out identical in all three CMake runs.

Lab-Proposal: P2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SKILL.md said that configure_file outputs are "recorded but not yet
diffed" only in a note beside the CMake extraction commands. The paragraph
that defines done ("Done when both stages converge") carried no such
caveat, so a reader who lands there reads converged: true as covering the
whole build.

Measured in any2bazel-lab: a lensfun migration converged with its generated
config.h differing from the reference (a GLIB_VERSION_MIN_REQUIRED value
that appears in no command line), and the same divergence recurred in later
runs, each alongside a converged diff.

Adds a short "what converged does not cover" list directly under the
definition of done, starting with generated file contents and the
instruction to byte-compare them. Later commits add the other boundaries
to the same list.

Lab-Proposal: P1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SKILL.md described link consistency as covering "every name-aligned
executable/shared-lib". diff.py does not do that: shared libraries are in
the library TU union, only executables reach the link-flag comparison, and
the external-dependency check is a project-wide union, so a dependency one
.so is missing passes if any other target links it. A clean diff says
nothing about how a shared library links.

Measured in any2bazel-lab: a pono migration converged while libpono.so
lacked its MPFR link, and the Bazel-built binary crashed at load. The agent
found it only by running the binary.

Corrects the two places in SKILL.md and the four comments in
scripts/diff.py (the module docstring, LINK_FLAGS_DIFF, _diff_link_flags
and _diff_tests) that claimed shared-library link parity, and adds the
boundary to the "what converged does not cover" list with the remedy: run a
binary that loads each shared library. Comparing shared libraries' link
lines is a larger design change (they would need identity alignment like
executables) and is left to the maintainers.

No later run exercised this change on the project that motivated it. On
lensfun, an agent following it loaded the built shared library, found it
missing a GLib symbol, and fixed the link.

Lab-Proposal: P8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMake passes CMAKE_<LANG>_FLAGS to the link line as well as the compile
line, so warning flags, -pedantic and -D/-U defines (including the -DNDEBUG
of Release builds) appear in the File API's link flags. The driver ignores
them when linking, and Bazel's link lines do not carry them, so each one was
a link_flags_diff error that every migration had to suppress by hand in
ignore.link_flags.

Measured in any2bazel-lab: every converged migration of three small
projects suppressed the same kinds of flag, each agent rediscovering and
justifying them in its own words (-Wall -Wextra -Wformat-security -pedantic
on one, -Wno-format-truncation on another, -DNDEBUG on a third). Agents also
suppressed front-end flags by hand: -fexceptions on lensfun, -w and
-fno-common on CMake's sources, -fvisibility=hidden on GDAL and QGIS. A hand
suppression is also where a reviewer can wave a real difference through.

_diff_link_flags now sends these missing CMake link flags to a new WARN
kind, link_compile_only_flags, which does not block `converged`:
- warnings and -Wp, preprocessor pass-throughs (-W..., but not -Wl, or
  -Wa,, the latter because an LTO link runs the assembler), -w and
  -pedantic*;
- -D and -U;
- front-end -f flags that steer only the compiler, listed one by one:
  -fexceptions, -fno-exceptions, -fcommon, -fno-common,
  -fvisibility-inlines-hidden and -fvisibility=.
Everything else stays a link_flags_diff error. Most -f flags matter at link
time, so the list is explicit rather than a prefix, and -fopenmp, -pthread,
-static*, -fsanitize* and -flto* are not in it. When an -flto* flag is on
either side of a link, warning controls and the front-end flags stay errors,
because LTO runs code generation, and diagnostics such as
-Werror=lto-type-mismatch, at link time. ignore.link_flags still silences
any of them entirely. SKILL.md lists the new kind, and the front-end flags,
with the other no-action WARNs. The upstream test that
used -fvisibility=hidden as its example of a link error now uses
-Wl,--no-undefined.

A built-in list is a design choice the maintainers may prefer to own; this
one is reported rather than dropped, so nothing disappears silently. Not
done: raising an unreferenced <target>_EXPORTS define only when a source
reads it, which would need the differ to read source files.

In the lab's runs with this change, the first version of the list (without
the front-end flags) still left agents suppressing -fexceptions on lensfun
and -w and -fno-common on CMake. With the list above, neither project's
next run suppressed a link flag by hand.

Lab-Proposal: P11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CMake File API reports a target's link line in fragments by role.
Flags that a dependency carries on its link interface (Python's
-Xlinker -export-dynamic, OpenMP's -fopenmp, Threads' -pthread) arrive in
the "libraries" fragments, not "flags". extract_cmake.py read "libraries"
only for dependency names, so these flags reached neither the dep closure
nor the link-flag comparison, and a Bazel binary missing one converged.

Observed in any2bazel-lab: on a Blender migration, the lab
verified that -Xlinker -export-dynamic sits only in the blender target's
libraries fragment. By the migrating agent's own report, the diff
converged without it and Python extension modules then failed to resolve
CPython symbols at run time, caught only by its functional test; the lab
did not reproduce the failure, because that tree was not kept.

A "libraries" fragment that is not a library and starts with "-" is now
added to the target's link argv, so it is compared like any link flag.
"-Xlinker X" pairs are folded into "-Wl,X" first, so the fragment is
classified by what it passes to the linker. Excluded, because they position
or locate the libraries around them or name the CMake build tree: -L,
-framework, rpaths in every spelling (-Wl,-rpath, -Wl,--rpath, -Wl,-R, and
-Xlinker -rpath -Xlinker <dir>), the whole-archive, push/pop-state, group,
-Bstatic/-Bdynamic and as-needed toggles, and -force_load.
canonicalize_link_flags also folds "-Xlinker X" into "-Wl,X" on both sides,
before its noise filters, so the pair compares as one flag, and then joins
an rpath given as two linker arguments (`-Wl,-rpath` then `-Wl,DIR`) into
`-Wl,-rpath,DIR`, which is what the linker receives. So these three
spellings of one rpath compare equal: `-Xlinker -rpath -Xlinker DIR` (what
CMake's LINKER:-rpath,DIR becomes under Clang), `-Wl,-rpath -Wl,DIR` and
`-Wl,-rpath,DIR`. `-Wl,-rpath=DIR`, `-Wl,--rpath,DIR` and `-Wl,-R,DIR`
are not unified with them, as before. Bazel spells its own rpaths the
first way, as $ORIGIN/, $EXEC_ORIGIN/ or @loader_path/ paths into its
_solib_<cpu> directories; on the Bazel side exactly those (an anchored
match) are toolchain link noise, otherwise they would appear as Bazel-only
link flags on every binary. A project's own rpath
(-Wl,-rpath,$ORIGIN/../lib, -Wl,-rpath=/opt/lib, -Wl,-rpath-link,...) is
kept and compared.

One limitation: any non-library "libraries" fragment becomes a link
flag the Bazel side must have, including one that carries a CMake
build-tree path Bazel cannot reproduce (-Wl,--version-script,<build
dir>/sym.map; this holds when the map file's basename does not start with
"lib", since a fragment ending in lib<name>.<ext> is still read as the
external dependency <name>, as before). A version script decides which
symbols are exported, so such a flag is not benign: its exact CMake
spelling may go in ignore.link_flags only after confirming the Bazel link
passes an equivalent script with the same content, and SKILL.md's
link_flags_diff row now says so. A multi-token fragment is classified as a
whole, as before, so `-Wl,-Bstatic -lfoo -Wl,-Bdynamic` in one fragment
still loses its -lfoo; splitting fragments per token is left out because
rpath pairs would then need their own pairing logic.

A behavior change: a migration that converged before may now show a
link_flags_diff for such a flag, most often -pthread (Threads::Threads with
THREADS_PREFER_PTHREAD_FLAG) or -fopenmp. That is a real difference in the
link command; add the flag to linkopts or record it in ignore.link_flags.
SKILL.md says these flags are compared.

No migration run exercised this change; it was checked on the stored
Blender models.

Lab-Proposal: P25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The differ checks external link dependencies (-lz, -lhdf5, ...) as a
project-wide union over PRODUCTION targets. A target that compiles nothing
of its own is an AGGREGATE and is left out, and in a Bazel migration nearly
every cc_shared_library is one: its sources live in the cc_library targets
it wraps, and the external libraries it links appear only on its own link
line. So a library linked only by a shared library read as missing although
it was there.

Found by an agent migrating QGIS in any2bazel-lab: its own diff ended with
seven missing_dep errors (crypto, curl, dl, hdf5, netcdf, pthread, sz),
which it traced to provider_mdal, a cc_shared_library classified as an
aggregate. The lab confirmed the seven are on that target's Bazel link line.

Link-only targets now take part in the external-dependency union on both
sides, unless exclude_targets drops them. Their translation units are still
compared through the libraries they wrap.

Run over the stored models of the 42 migrations the lab has judged, each
with its agent's own config, this removes exactly those seven errors and
changes nothing else. (One earlier QGIS model is unreadable to both
versions.) The next QGIS migration, with this change, converged.

Lab-Proposal: P26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SKILL.md says to always call aquery with --features=-linker_param_file. The
toolchain that rules_cc 0.2.22 autodetects on Linux implies that feature
unconditionally, so analysis fails. In any2bazel-lab's migration runs, 8 of
10 agents hit the error and retried without the flag, one to three calls
each, and none of the aquery outputs then held a param file reference (four
runs checked).

The text now names the error, says to drop the flag, and gives the check
that keeps the instruction's purpose: no @....params entry in any action's
arguments. Written after the runs, so no run has used it yet.

Lab-Proposal: P29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bazel_args records the extra args aquery runs with to mirror the real
build. Nothing said where settings the migration itself introduces should
live, so parity could be established for a named --config that a plain
`bazel build //...` never applies: the claim then holds for a command no
user runs.

Measured in any2bazel-lab: one pono migration put the
reference's Release settings behind --config=release, recorded it in
bazel_args, and converged. A plain `bazel build //...` produced an
assertion-enabled binary that crashes on an input where the reference
prints an error and exits. The four other runs on the same project put the
settings in .bazelrc's build lines and were equivalent.

Adds one bullet to the "invoke aquery the way the project is built"
list: settings the migration adds go in `.bazelrc` build lines, and parity
must hold for the project's documented build command, which is a plain
`bazel build //...` unless the project documents flags; only those
documented flags go in bazel_args. The bazel_args field
description now says the same, and that the example config (a project
config plus the top-level -fno-exceptions BoringSSL expects) is that kind of
entry.

In the lab's later pono and lensfun runs with this change, bazel_args was
empty and the settings were in .bazelrc build lines.

Lab-Proposal: P10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing in the procedure said which CMake configuration the reference
should use, or that choosing one is a decision. An agent can turn options
off before extracting the reference and then reach parity with that
narrower build; both sides of the comparison move together, so no error
count shows it.

Measured in any2bazel-lab: a MySQL Server migration
configured the reference with Router, NDB, the unit tests and the
distribution's package compiler flags all turned off, recorded only in a
comment in any2bazel.json. Against the project's default configuration the
result showed 4,928 differences; against its own, 13. The reason given for
the package flags was defensible; the problem was that the choice was
silent. Agents redefined the reference with and without the skill, on small
projects as well as large.

"Extract the reference model" now says to configure with the project's
documented default options, to surface any change as a scope decision, and
to record
the exact command in any2bazel.json as reference.configure_command (added
to the example and the field list as record-only; config.load ignores it,
like any unknown key) and never change it after the first diff. The
optional test step reconfigures with tests on; that command is recorded
separately, as reference.test_configure_command, so it does not count as
changing the reference.

In the lab's later runs with this change, every migrating agent recorded
its configure command.

Lab-Proposal: P13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill never mentioned CMAKE_BUILD_TYPE or --compilation_mode, and its
canonicalization text called -O*/-g* "never your concern" (reworded in the
levels commit). Agents therefore map the concept, Release to `-c opt`,
and lose the number: CMake's Release is -O3 -DNDEBUG, Bazel's opt is -O2;
CMake's Debug is -g, Bazel's default fastbuild has neither.

Measured in any2bazel-lab: all nine migrations of pono, with and without
the skill, wrote
`build --compilation_mode=opt` as the counterpart of Release, several
saying so in a comment on the line above, and eight of the nine compile at
-O2 against a reference at -O3; the ninth added `build --copt=-O3` and
matched. lensfun's CMakeLists.txt defaults the build type to Debug when
none is given; no lensfun migration set a compilation mode, so every one
dropped -g from all 40 TUs.

Adds a short "Build type" section to docs/REFERENCE-cmake.md, under a new
"Writing the BUILD files" heading that "Create initial BUILD.bazel files"
points to: the two mappings that differ, the reminder that a project can set its own default build type,
where to read the reference's actual level (diff.json's levels, or the
cache), and the two .bazelrc lines that pin it. It also says that opt adds
-DNDEBUG and so turns off assertions the reference may keep (diff.json's
ndebug shows it), and that
`build --copt=-UNDEBUG` undoes only the toolchain's -DNDEBUG, not one a
rule passes in its copts, which come later on the command line.

In the lab's later runs with this change, pono kept -O3, lensfun kept -g,
and CMake's sources, whose reference sets no build type, gained neither -O2
nor NDEBUG in three runs (two earlier runs had added both).

Lab-Proposal: P18

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant