Imported from nolanvenhola/zeliard (
.claude/skills/symbol-sweep/SKILL.md). Install upstream withnpx skills add nolanvenhola/zeliard --skill symbol-sweep. Copyright stays with the author.
You are running a symbol-sweep across the Zeliard tasm source. This is the multi-file companion to two earlier skills:
/asm-cleanupcleans one file at a time — Sourcer artefacts, db decoding, label renaming inside a chunk./label-auditresolves one address at a time — finds the canonical name, alias EQUs, and rewrites use sites for that single address.
Symbol-sweep handles work that neither of those covers cleanly:
- Cross-file symbol drift — same address referenced by 4 different speculative names in 4 different chunks. No single label-audit run catches them all because they're spread across files.
- Bulk hardcoded-reference replacement — a chunk has 200+ raw
ds:[NNh]andcs:[NNNh]operands and you want to replace them all with symbolic EQUs in one pass, across many chunks at once. - Driver dispatch tables —
cs:[10Ch],cs:[110h], ...cs:[120h]are stick.bin INT 60h game-state slots;cs:[2000h]..cs:[2044h]are graphics-driver function pointers. Mapping every slot at once is more efficient than auditing each address individually. - Cross-include propagation — when an EQU is added to one shared
.inc, deciding which other shared includes it also needs to land in. - Conflicting evidence resolution — when two chunks operate on the
same byte and imply different semantics (e.g. 200FIGHT
damage-absorption vs 201SELCT icon-draw both reading
[93h]), pick the canonical interpretation by ranking evidence weight.
Verification is the same two-tier system as the prior skills:
- Per-edit —
python verify1.py <changed-file.asm>after every batch. - End-of-batch —
python build_all.py --verifyonce at the end. Required output: all 3 SARs BIT-PERFECT.
Step 0 — Decide the sweep scope
Symbol-sweeps come in five common shapes. Identify which one you're running before doing anything else — the workflow differs.
| Shape | Trigger | Example |
|---|---|---|
| Player-record sweep | Want every byte of player record (game_seg:0x80..0xE8) to have a canonical name across all chunks that touch it | The shield_type/shield_HP/shield_max_HP pass at 0x93/94/95 |
| gvar_ sweep* | Want to confirm every game-segment global (FF00..FFFF) name is consistent against the canonical .inc |
The "audit all gvars in game.asm" pass |
| Driver-dispatch sweep | Want every slot in a function-pointer table named after the proc it points to | The stick.bin cs:[10Ch..120h] slot mapping |
| Hardcoded-ref sweep | A chunk has many raw ds:[NNh]/cs:[NNNh] operands → want them all symbolic |
The 200FIGHT ~250 hardcoded refs cleanup |
| Placeholder-proc sweep | A chunk has many auto-generated loc_NN, proc_NN, game_func_NN labels |
The 200FIGHT 51-proc rename |
For each shape, the inventory step is different (Step 1). The remaining steps (evidence, rename, propagate, verify) are mostly the same.
State the shape explicitly to the user when you start a sweep, and quote the inventory size: "Player-record sweep: 14 unresolved bytes across 0x80..0xE8" or "Hardcoded-ref sweep: 247 raw operands in 200FIGHT.asm". This sets expectations on how big the change will be.
Step 1 — Inventory
Run the right inventory tool for the sweep shape.
1a — Player-record / gvar sweep
cd 3_Assembly/tasm
python find_missing_equs.py
Open working/MISSING_EQUS.md. The "MISSING" section lists raw-hex
operands without an EQU; the "UNUSED" section lists EQUs that exist but
nothing references symbolically. Both are leverage points.
For specifically the gvar range (FF00..FFFF):
grep -rEn '\b0?FF[0-9A-Fa-f]{2}h\b' 3_Assembly/tasm/working \
| grep -vE '\bequ\b' \
| grep -vE '^\S*\.(inc|asm):\s*(db|dw|dd)\b' \
> /tmp/raw_gvar_refs.txt
Count distinct addresses:
grep -oE '0?FF[0-9A-Fa-f]{2}h' /tmp/raw_gvar_refs.txt | sort -u | wc -l
1b — Driver-dispatch sweep
The function-pointer table lives in the driver's source (stick.asm for
INT 60h slots, gm*.asm for graphics slots). Find the table:
grep -nE '^\s*driver_init_data|^\s*dispatch_tbl|^\s*fn_table' \
3_Assembly/tasm/working/drivers/*.asm
Read the table's dw entries — each entry is a near-pointer to a proc
in the same segment. The slot index → proc mapping is what you'll use
to rename cs:[NNh] references in the chunks that call into the driver.
1c — Hardcoded-ref sweep (single chunk)
Pick the busiest patterns first — ranking by frequency tells you where the readability win is biggest:
grep -oE '(ds|cs|es|ss):\[0?[0-9A-Fa-f]+h\]' \
3_Assembly/tasm/working/zelres2/code/200FIGHT.asm \
| sort | uniq -c | sort -rn | head -40
The output gives you a ranked list of (count, operand) pairs. Plan
the rename in descending-count order — high-count operands have the
largest readability impact and the smallest risk because their semantic
role is over-determined by the volume of evidence.
1d — Placeholder-proc sweep
grep -nE '^(loc|proc|sub|game_func)_[0-9]+\s+(proc|:|\bnear\b)' \
3_Assembly/tasm/working/<chunk>.asm \
| head -100
Tabulate them with their callers (one grep per name):
for n in $(grep -oE '^(loc|proc|sub|game_func)_[0-9]+' <chunk>.asm | sort -u); do
count=$(grep -cE "\b$n\b" <chunk>.asm)
echo "$count $n"
done | sort -rn
Procs called from many sites are higher value to rename — their name shows up in many places once renamed. Procs called from one site are lower priority but should still be done if their behaviour is clear.
Step 2 — Detect symbol drift
For each address in the inventory, list every name that resolves to it across the codebase.
HEX=93 # the address you're investigating
grep -rnE "^\s*\w+\s+equ\s+0?${HEX}h\b" 3_Assembly/tasm/working
Tabulate the hits. Classic drift looks like:
0x93 names found:
shield_type stdply.inc:142 ; canonical (matches DOS overlay)
equipped_magic zr2com.inc:88 ; misnomer — only set in 201SELCT
player_ability_a zr1com.inc:54 ; misnomer — alias from a wrong functest probe
stat_X93 zr2com.inc:171 ; placeholder
When 3+ names resolve to the same address, you have drift. Don't rename anything yet — go to Step 3 to weigh evidence.
A symptom of recent drift: a name that exists in only one .inc
and is referenced by zero .asm files outside that include's scope.
That name is almost certainly a guess from a single chunk's narrow
view; the canonical name lives elsewhere.
Step 3 — Weight the evidence
Not all evidence is equal. When two chunks imply different semantics for the same byte, rank the evidence types:
| Evidence type | Weight | Example |
|---|---|---|
| Damage / arithmetic / state-write that drives gameplay | Highest | apply_combat_damage_with_absorb reads [93h], conditionally subtracts from [94h], writes back |
| FSM transitions in main game loop | Highest | [E7h] switches on cmp [0E7h], 80h across 5 driver chunks |
| Set/test/clear cycle inside one function | High | bg_save sets [FF44h], bg_restore tests/clears it |
| Address used as table index / dispatch key | High | mov bx, [9Ah]; jmp [bx*2 + dispatch_tbl] |
| Counter / wrap-around comparison | Medium | inc [80h]; cmp [80h], map_width; jne … |
| Icon-draw / text-format / display read | Low | 201SELCT reads [93h] to pick which icon to draw — only proves it's some index, not which one |
| Single zero-init in a "clear all flags" sequence | None | A reset path that writes 0 to dozens of bytes proves nothing |
| User testimony alone | None | "I think this is the magic stat" — still needs an asm trace |
The decisive case from this codebase: at byte 0x93, both
apply_combat_damage_with_absorb (200FIGHT) and draw_abilities
(201SELCT) read the byte. The combat path modifies [94h] based
on the value at [93h] — that's load-bearing semantics. The selcт
path uses [93h] to pick an icon — that just proves it's an index.
Combat path wins; the byte is shield_type, not equipped_magic.
When you adopt the winning name, retain the loser as a deprecated alias EQU with a comment explaining why it was wrong — so a future reader doesn't re-litigate the same conflict.
shield_type equ 93h ; type of equipped shield (drives damage absorb in 200FIGHT)
equipped_magic equ 93h ; alias — deprecated; was a misread of 201SELCT's icon-draw use
Step 4 — Validate against indirect access
Static analyzers can miss a use site if the code reaches the byte
indirectly — through SI/DI/BX after a lea or arithmetic, or
through a structure offset. Before declaring a byte vestigial, run a
broader probe.
4a — Direct grep (catches mov al, ds:[NNh] etc.)
grep -rEn "(ds|cs|es|ss):\[0?<HEX>h\]" 3_Assembly/tasm/working
4b — lea / pointer-load grep
grep -rEn "\b(lea|mov)\s+\w+\s*,\s*(\[)?<HEX>h" 3_Assembly/tasm/working
If the address is the target of a lea bx, [93h] followed by indirect
reads through [bx], the direct grep misses it. Also check for
mov si, OFFSET <known_label_at_or_near_addr> — scrolling past via SI
also escapes the direct grep.
4c — Range-of-bytes probe via Unicorn
Use functest/harness.py to set up a representative call into a
suspect proc with controlled register/memory state, then read which
bytes it touches. This is the gold standard for "does anything in
this function actually touch byte X?"
The point: "VESTIGIAL — no reader" claims are wrong about 30% of
the time in this codebase because the reader uses lodsb or
mov al, [si+offset] after SI was set elsewhere. Always run 4a/4b
before claiming vestigial; run 4c if the static evidence is split.
Step 5 — Apply the renames
The order of operations matters because of replace_all prefix
collisions. This is the most common source of broken builds during
a sweep.
5a — Reverse-numeric order for prefix-overlapping labels
If you're renaming loc_3 and also loc_30, loc_31, ..., loc_39,
rename loc_3 LAST. Otherwise the replace_all for loc_3
clobbers references to loc_30..loc_39 because the substring loc_3
appears inside them.
# WRONG — clobbers loc_30..loc_39 references
for n in [3, 30, 31, 32, 35]:
edit.replace_all(f"loc_{n}", new_names[n])
# RIGHT — descending numeric order
for n in [35, 32, 31, 30, 3]:
edit.replace_all(f"loc_{n}", new_names[n])
The same pattern hits any prefix_N family where N spans single and
multi-digit values: game_func_7 collides with game_func_70..79,
proc_1 collides with proc_10..19 and proc_100..199.
If the names span multiple orders of magnitude (proc_1 and
proc_100), consider using a regex with a word-boundary or
end-of-line anchor instead of replace_all:
# Match loc_3 only when followed by non-digit (end of token)
re.sub(r'\bloc_3(?!\d)', new_name, content)
5b — Bracket sensitivity in TASM
These two are NOT equivalent in TASM 2.01 emitted bytes:
mov word ptr ds:boss_entry_tbl, ax ; (no brackets)
mov word ptr ds:[boss_entry_tbl], ax ; (with brackets)
The first form sometimes emits a different ModR/M byte than the second. Always preserve the original brackets when rewriting raw-hex operands to symbolic operands:
; Original
mov word ptr ds:[0EB15h], ax
; Correct rename (brackets preserved)
mov word ptr ds:[boss_entry_tbl], ax
; INCORRECT — drops brackets, may change emitted bytes
mov word ptr ds:boss_entry_tbl, ax
If verify1.py reports a 1-2 byte mismatch right after a sweep, this
is the first thing to check.
5c — Cross-include propagation
Adding an EQU to one .inc doesn't make it visible to chunks that
include a different .inc. Map the include relationships before you
edit:
working/drivers/stdply.asm → includes stdply.inc, zeliard.inc
working/drivers/gm*.asm → includes stdply.inc, srmacros.inc
working/zelres1/code/100OPDMO.asm → includes zr1com.inc
working/zelres1/code/106TOWN.asm → includes zr1com.inc
working/zelres2/code/200FIGHT.asm → includes zr2com.inc
working/zelres2/code/213BANKP.asm → includes zr2com.inc
working/zelres3/code/3*.asm → includes zr3com.inc
working/core/game.asm → has its own EQU section + zeliard.inc
working/core/zeliad.asm → has its own EQU section + zeliard.inc
If a canonical EQU lives in stdply.inc but 200FIGHT.asm uses the
symbolic name, TASM raises "undefined symbol" because 200FIGHT doesn't
include stdply.inc. Add the EQU as a duplicate in zr2com.inc:
; canonical home is stdply.inc; redeclared here so chunks in zelres2
; can reference shield_type symbolically. TASM accepts duplicate
; EQUs with matching values.
shield_type equ 93h
The shared .inc files for player-record and gvar EQUs are:
| Include | Used by | Canonical home for |
|---|---|---|
drivers/stdply.inc |
stdply.asm + gm*.asm | player record bytes (0x80..0xE8) |
core/zeliard.inc |
zeliad.asm + game.asm | gvar_* (FF00..FFFF) |
zelres1/code/zr1com.inc |
zelres1 chunks | duplicates of stdply.inc + gvar_* names referenced by zelres1 chunks |
zelres2/code/zr2com.inc |
zelres2 chunks | duplicates of stdply.inc + gvar_* names referenced by zelres2 chunks |
zelres3/code/zr3com.inc |
zelres3 chunks | duplicates of stdply.inc + gvar_* names referenced by zelres3 chunks |
When in doubt: add the EQU to every common include the address is referenced from. TASM accepts duplicates with matching values, and duplication beats compile failures.
5d — Multi-purpose byte aliases
Some bytes are reused for unrelated purposes by different code paths. Both names can coexist if they EQU to the same value:
drv_color_lut equ 78h ; gm*.asm: color lookup base in graphics dispatch
weap_dur_cur equ 78h ; alias — same byte, holds current weapon durability
; in 200FIGHT. Treat as separate logical fields
; that happen to share storage; rename each call
; site to whichever name fits its role.
The rule: rename each use site to the name that matches its semantic role, not to a single canonical name. Multi-purpose bytes in this codebase are often genuine — RAM was scarce, the original authors aliased fields whose lifetimes didn't overlap.
5e — Comment maintenance
After a rename, the surrounding comments often reference the old name.
If your replace_all was scoped to just the symbol, the comment is
now stale and misleading. Two failure modes:
- Stale reference comment:
; sets gvar_volume_a (FF44)survives aftergvar_volume_awas renamed togvar_input_lock. Hunt these down with a grep for the old name and rewrite. - Self-referential rename bug: a comment like
; renamed from gvar_volume_a to gvar_input_lock — was a misnomergets clobbered by a globalreplace_all gvar_volume_a → gvar_input_lock, becoming; renamed from gvar_input_lock to gvar_input_lock.
Avoid both by:
- Doing the rename FIRST.
- Then writing the explanatory comment.
- Never the other way around.
If you wrote the comment first, scope the replace_all with a regex
that excludes comment lines (^[^;]*\bold_name\b matches only outside
comments — but check for inline ; ... after the symbol, those are
inside comments too).
5f — Re-grep for missed sites
After a batch of renames, both forms must come up empty:
# Did any old placeholder symbols survive?
grep -rEn "\b(stat_X<HEX>|gvar_flag_FF<HEX>|loc_<N>)\b" 3_Assembly/tasm/working
# Did any raw-hex operands survive at the renamed addresses?
grep -rEn "(ds|cs|es|ss):\[0?<HEX>h\]" 3_Assembly/tasm/working
The first should return only the alias-EQU line(s). The second should
return only the EQU declarations themselves. Anything in mov/cmp/
test/or/and instructions is a missed site.
Step 6 — Verify
6.1 — Per-batch fast verify
Run after every batch of related renames:
cd 3_Assembly/tasm
python verify1.py drivers/stdply.asm
python verify1.py zelres2/code/200FIGHT.asm
python verify1.py zelres2/code/201SELCT.asm
# ... one per changed .asm
Each takes <2 seconds. Required output per file:
BIT-PERFECT — drivers/stdply.bin matches expected (3344 bytes)
If a file fails, the most likely causes are:
- Bracket sensitivity (5b above) — restore
[]around the operand. - Prefix collision (5a above) — a
replace_allclobbered a longer symbol; revert and redo in reverse-numeric order. - Missing EQU in scope (5c above) — TASM raises "undefined symbol" because the EQU is in stdply.inc but the file includes zr2com.inc only. Add the duplicate EQU.
- Stale
.lstconfusion — a previous failed build left a.lstfile showing unrelated errors. Delete.lstand.binfor the changed file before re-running:rm -f 3_Assembly/tasm/working/<path>/<file>.lst \ 3_Assembly/tasm/working/<path>/<file>.bin
6.2 — End-of-batch full verify
Once all verify1.py runs are bit-perfect, run the full SAR rebuild:
cd 3_Assembly/tasm
python build_all.py --verify
Required output:
zelres1.sar: BIT-PERFECT (256,952 bytes)
zelres2.sar: BIT-PERFECT (345,218 bytes)
zelres3.sar: BIT-PERFECT (342,434 bytes)
If a SAR fails despite per-file verifies passing, the issue is at the SAR-pack level (chunk ordering, archive header, padding). Renames rarely cause this — but a delete or insert of a line can shift something. Bisect by reverting half the changes and rebuilding.
Step 7 — Document the sweep
Update one or more of:
working/IDA_NAME_DELTA.md— record renames where the IDA name differed from the cleaned-source name and which one won.working/AUDIT_TODO.md— list any names you renamed to a generic placeholder (likeplayer_ability_1/2/3) where the specific role is still TBD.working/EVIDENCE_REPORT.md— append a paragraph for each significant rename with the evidence weight (Step 3 table) and the procs/files cited.
In the user-facing reply, state:
- The sweep shape (player-record / gvar / driver-dispatch / hardcoded / placeholder-proc) and inventory size.
- The renames applied, grouped by canonical home.
- SAR verification status (line per archive).
- Any audit-todo items left for follow-up.
Common pitfalls
These are the failure modes that bit me during this codebase's sweeps. Watch for them.
Prefix collision in replace_all
The single most common build-breaker. If you're renaming loc_3 and
loc_3X exists, loc_3 will clobber loc_3X references. Always
rename in reverse numeric order, or use word-boundary regex.
Bracket sensitivity
mov ds:label, ax and mov ds:[label], ax can emit different bytes.
Preserve original brackets.
Self-referential comment clobber
replace_all old_name → new_name will rewrite a comment that mentions
both names: ; renamed from old_name to new_name becomes
; renamed from new_name to new_name. Write the comment after the
rename, not before.
Global rename of a multi-purpose byte
If [78h] is drv_color_lut in graphics drivers and weap_dur_cur in
combat code, a global [78h] → drv_color_lut rename makes the combat
code unreadable. Multi-purpose bytes need site-by-site decisions,
not a global sweep.
Trusting "no readers" claims from a static probe
functest/harness.py and analyze_stat_layout.py see direct
addressing modes only. They miss lodsb and mov al, [si+N] accesses
where SI was loaded with the address in a different proc. Run 4a/4b/4c
before annotating a byte as vestigial.
Trusting one chunk's narrow interpretation
201SELCT reads [93h] to pick a shield icon. That proves [93h] is
some kind of shield index — but it doesn't prove it's the equipped
magic ID (which was the early guess). 200FIGHT's
apply_combat_damage_with_absorb proves it's specifically the shield
type that drives absorption — the icon read happens to use the same
byte because the icon represents the absorbing shield. Always cross-
reference at least one combat or game-loop chunk before adopting a name
sourced from a UI/menu chunk.
Conflicting evidence: damage write beats icon-draw read
Default precedence rule when two chunks imply different semantics for the same byte:
damage / arithmetic / FSM-write > set/test/clear pair > display read
The chunk that modifies the byte based on its value is the one whose naming convention wins. The chunk that just displays the byte adopts the canonical name even if its narrow read suggests something simpler.
Stale .lst files after a failed build
TASM leaves a .lst from the last successful compile. If a build
fails midway, the .lst you read may be from before the change.
Delete .lst (and .bin) for the changed file before re-running
verify1.py.
EQU in the wrong common include
A canonical EQU added to stdply.inc is invisible to 200FIGHT.asm,
which includes zr2com.inc. Always add the EQU to every common
include whose chunks reference the symbol — TASM accepts duplicates
with matching values. See Step 5c for the include map.
Auto-generated label names hiding renames
game_func_7 may have been renamed to apply_combat_damage_with_absorb,
but a stale loc_700 reference in another chunk wasn't updated. After
each batch, grep for the full set of pre-rename names — not just the
ones you remember touching.
Don't promote a name from one chunk's use without verifying
the address shows the same access pattern in other chunks
A name like trade_marker_flag may have made sense in 212ARMRP because
that's where the byte is set. But if 200FIGHT also reads the byte and
its access pattern there is unrelated to trading, the name is wrong.
Trace at least two distinct call sites before adopting a name —
ideally one write and one read in different chunks.
Tooling reference
Same scripts as /label-audit, plus:
| Script | Purpose |
|---|---|
analyze_stat_layout.py --range LO HI --segment SEG |
Per-address access-pattern classifier; resolves EQU symbols |
find_missing_equs.py |
Lists raw-hex operands without an EQU (MISSING) and EQUs that aren't being used at call sites (UNUSED). Output: working/MISSING_EQUS.md |
survey_full_address_space.py |
Bucketed wide-net survey across all addresses; useful for prioritising a sweep |
evidence_check.py |
Phase 1/2/3 verifier for a proposed rename batch |
compare_ida_names.py |
Diffs cleaned-source names against IDA's; useful for finding drift between the two |
verify1.py <subpath/file.asm> |
Per-edit fast verify; <2s per file |
build_all.py --verify |
End-of-batch full SAR rebuild + bit-perfect comparison |
Common include files for cross-include propagation:
| Include | Scope |
|---|---|
drivers/stdply.inc |
Player record (0x80..0xE8) — canonical home |
core/zeliard.inc |
gvar_* (FF00..FFFF) — canonical home |
zelres1/code/zr1com.inc |
zelres1-chunk duplicates of stdply.inc + zeliard.inc |
zelres2/code/zr2com.inc |
zelres2-chunk duplicates of stdply.inc + zeliard.inc |
zelres3/code/zr3com.inc |
zelres3-chunk duplicates of stdply.inc + zeliard.inc |
Worked examples (from this codebase)
-
shield_type / shield_HP / shield_max_HP at 0x93/0x94/0x95: early speculative names
equipped_magic,player_exp,player_exp_cappromoted from a 201SELCT-only read. Reverted afterapply_combat_damage_with_absorbin 200FIGHT was traced — that proc reads[93h]to pick an absorb table entry, then conditionally decrements[94h](shield HP) by damage taken, capped at[95h](shield max HP). Decisive evidence rule: damage-write beats icon-draw read. -
stick.bin INT 60h dispatch slots
cs:[10Ch..120h]: each slot is a near-pointer to a stick.asm proc. Mapped by reading thedriver_init_datablock in stick.asm and matching each slot index to its target proc. Renamedcs:[10Ch]→sar_loader_fn,cs:[110h]..cs:[120h]→stick_*_handlerseries with descriptive names per their actual role. Cross-include propagation: added the fullstick_*_handlerset tozr1com.incANDzr2com.incbecause both archive groups dispatch through stick. -
graphics-driver dispatch slots
cs:[2000h..2044h]: same pattern as the stick dispatch. Mapped from thegm*.asmdriver dispatch table. Renameddrv_fn_2/3/5/6/7/10/13/14/21/22anddrv2_fn_2/15/20. Multi-purpose:[78h]isdrv_color_lutin the graphics path andweap_dur_curin the combat path — kept both EQUs with explanatory comments. -
player_ability_1/2/3 at 0x9B/0x9C/0x9D: previously named
trade_marker_flag(0x9B, from 212ARMRP) andstat_X9C/stat_X9Dplaceholders. After functest probing showed all three are read together by 201SELCT'sdraw_abilitiesproc vialodsbfrom a 3-byte stride, renamed toplayer_ability_1/2/3with an AUDIT_TODO entry — specific abilities still TBD pending more evidence. Indirect-access lesson: the static analyzer reported[9Ch]and[9Dh]as having zero readers because the read goes through SI after amov si, OFFSET 9Bhin a different proc. -
200FIGHT 51-proc rename: largest single-chunk sweep. Inventory: 51 procs named
loc_NN,proc_NN,game_func_NN. Rename order chosen carefully (reverse-numeric for digit-overlapping families) to avoid prefix collisions. Outcome: every proc named after its semantic role with one-line evidence comments at the proc header. Bit-perfect verified after each batch of ~8 procs.
Don'ts
- Don't rename in forward numeric order if the family has
digit-overlapping names (
loc_3andloc_30..39). Reverse order. - Don't drop brackets when rewriting raw-hex operands to symbolic.
ds:[X]andds:Xcan emit different bytes. - Don't trust "no readers" claims from static probes — check for
lodsb/mov [si+N]indirect access first. - Don't promote a name from one chunk's narrow use — cross- reference at least one other chunk's access pattern before adopting.
- Don't write the rationale comment before the rename — the
replace_allwill clobber the old name in your own comment. - Don't add the canonical EQU to only one chunk's local source —
it has to live in the shared
.incfiles used by every consuming chunk, even if that means duplicating the EQU acrosszr1com.inc,zr2com.inc,zr3com.inc. - Don't skip verify1.py between batches — the longer the gap between a build break and noticing it, the harder the bisect.
- Don't accept a global rename for a multi-purpose byte — site-by- site decisions are required when the byte's role differs by call context.
- Don't trust user testimony alone as evidence — every promotion still needs an asm trace. User testimony is hypothesis-quality; the bytes are ground truth.