Automated summary
Audits a NES DPCM/drum subsystem: how General MIDI percussion notes map to DPCM samples, how .wav files are converted into 1-bit DMC delta data, and how the DMC channel is driven, reporting severity-graded findings.
Strengths
- Splits the audit into numbered dimensions that can be run selectively with --focus (e.g. --focus 1,4)
- Requires every hardware claim to cite docs/APU_DMC_REFERENCE.md or docs/NES_DMA_REFERENCE.md (register map, $C000 + A*64 address formula, (L*16)+1 length formula, 64-byte alignment)
- States an explicit severity rule: a dropped or out-of-range drum sample is at least MEDIUM, and HIGH when it silently strips an intended hit
- Tracks fix history (issue numbers, commits) and asks the auditor to confirm fixes hold on edge cases instead of re-reporting the original bugs
Limitations
- Highly repo-specific: hard-coded paths, document names, issue numbers and commit ids make it unusable outside this project
- Relies on shared files (_audit-common.md, _audit-severity.md) whose contents are not included
- The supplied text stops at Dimension 1, so coverage of the later dimensions cannot be assessed
Use it when reviewing changes to dpcm_sampler/, the drum-note mapping, or DMC output ahead of a release.
Avoid it for non-NES audio stacks or for a general code review unrelated to the DPCM subsystem.
Security analysis
SafeThis is a static code-audit checklist. It contains no executable steps, no network calls, no destructive commands, and no attempts to exfiltrate data or alter the agent's behavior; the only shell snippet mentioned is a read-only `python -c` inspection of a local JSON file. It explicitly instructs the auditor to treat hardware claims skeptically and cite local docs, which is benign. No instruction-injection attempt was detected in the content.
- •No executable or destructive actions are prescribed; the only embedded command is a read-only `python -c` that prints the keys of a local JSON index file.
- •Contains authority-style framing ("Fixed — verify", "Sprint note") that pre-states conclusions and could steer an auditor toward confirming fixes rather than independently discovering defects; this is a bias/manipulation concern for the audit's objectivity, not an execution risk.
Examples
/dpcm-audit/dpcm-audit --focus 1,4 — confirm that a mid-range GM note such as 47 resolves to a real sample name and that the ceiled length_reg fix (#295) still holds.Audit the DMC address and length computation in dpcm_sampler/ and cite the exact sections of docs/APU_DMC_REFERENCE.md and docs/NES_DMA_REFERENCE.md behind each hardware claim.description: "Audit DPCM/drum sampling — drum mapping, sample conversion, DMC constraints" argument-hint: "[--focus <dims>]"
DPCM / Drum-Sampling Audit
Audit the DPCM/drum subsystem — how MIDI General-MIDI percussion notes become DPCM
samples, how .wav is converted/packed into NES 1-bit delta data, and how the DMC
channel is driven. This subsystem owns everything under dpcm_sampler/ plus the
DMC-facing edges of the channel pipeline and the CA65 exporter.
Shared protocol (layout, dedup, finding format): .claude/commands/_audit-common.md.
Severity rubric: .claude/commands/_audit-severity.md. Do not restate them.
Hardware claims must cite docs/APU_DMC_REFERENCE.md (register map, the
$C000 + A*64 address formula, the (L*16)+1 length formula, 64-byte address
alignment, the 0–127 output-level clamp, the $FFFF→$8000 address-wrap quirk) and
docs/NES_DMA_REFERENCE.md (DMC DMA steals 3–4 CPU cycles per byte and is the source
of the controller/$2007/$4015 extra-read glitch). Prefer those docs over
re-deriving APU behavior from source.
A dropped or out-of-range drum sample removes audible content, so per
_audit-severity.md it is at least MEDIUM, and HIGH when it silently strips a hit
that the MIDI clearly intended.
Sprint note: a large bug-fixing pass (commits
be4d2bd…8225696) closed most of the issues this audit used to lead with (#64–#74, #140). The dimensions below describe the current (fixed) behavior and ask you to verify the fix holds up under edge cases, rather than re-discovering the original bugs. #76 remains genuinely open (Dimension 6). #75 (thelength_regrounding) was closed on GitHub, then regressed, and was re-fixed on this tree as #295 (commitd392ef6) —_place_samplenow ceils, so Dimension 4's item is fixed (#295); verify it holds.
Parameters (from $ARGUMENTS)
--focus <dims>— comma-separated dimension numbers (e.g.--focus 1,4). Default: all.
Extra Per-Finding Field
- Hardware ref: the
docs/APU_DMC_REFERENCE.md/docs/NES_DMA_REFERENCE.mdsection backing any hardware claim (omit for pure-software findings).
Dimensions
Dimension 1: Drum-note → sample mapping & coverage
The GM-note maps live in dpcm_sampler/drum_engine.py
(DEFAULT_MIDI_DRUM_MAPPING lines 8-56, ADVANCED_MIDI_DRUM_MAPPING lines 58-78).
Mapping resolution is in EnhancedDrumMapper._resolve_dpcm_sample_name and
map_drums (dpcm_sampler/enhanced_drum_mapper.py):
- Fixed (#73/D-10, verify):
DEFAULT_MIDI_DRUM_MAPPINGnow covers the full GM percussion range 35–81 with generic role names (kick/snare/tom/cymbal/etc, not just 7 notes).ADVANCED_MIDI_DRUM_MAPPINGstill only hand-tunes velocity splits for notes 36 and 38; every other note relies on theDEFAULT_MIDI_DRUM_MAPPINGfallback by design (see the trailing comment atdpcm_sampler/drum_engine.py:75-77). Confirm a mid-range note (e.g. 47, mid tom) still resolves to a real sample name and notNone. - Fixed (verify):
_resolve_dpcm_sample_name(dpcm_sampler/enhanced_drum_mapper.py:390-418) no longer stops at the first candidate — it tries the velocity-split name, then the advanced"primary"name, then theDEFAULT_MIDI_DRUM_MAPPINGrole name, in that order, and only returnsNone(→ noise fallback) if none of the three exist inself.sample_index. Verify this cascade actually reaches the index for a name likekick_softthat legitimately isn't present (falls through to"kick"then to the default role name) rather than silently dropping. - Velocity-0 note-offs are correctly skipped
(
if e.get('velocity', 0) == 0: continue,enhanced_drum_mapper.py:254-255). _get_advanced_sample(enhanced_drum_mapper.py:374-388) selects byvelocity_ranges; its result is now just one candidate in the fallback chain above rather than a hard commit — confirm a nonexistent velocity-split name no longer kills the whole event, only that one candidate.
Dimension 2: dpcm_index.json schema integrity
The index is loaded in the same three places, still against two different shapes:
EnhancedDrumMapper._load_sample_index(dpcm_sampler/enhanced_drum_mapper.py:211-224) passes each entry assample_datatoDPCMSampleManager.allocate_sample(dpcm_sampler/dpcm_sample_manager.py:15-65), which readssample_data.get('length', 1024)(line 34),sample_data.get('data', [])(line 55), andsample_data.get('frequency', 33144)(line 58).- Still true (this is not a bug that was "fixed", just the current shape of the
data): the real
dpcm_index.jsonentries only containidandfilename(verify:python -c "import json; print(list(json.load(open('dpcm_index.json')).values())[0])"). Solength,data, andfrequencystill always fall back to defaults on real input. What did change (#70/#71, see Dimension 7) is that the sample manager now uses one consistent accounting formula for those defaults instead of two divergent ones, and the now-dead similarity/dedup code that also depended ondatawas removed outright rather than left silently inert. Assess whether relying on constant placeholder size/frequency for every sample is still an acceptable simplification or worth back-filling from the real.dmcfiles at index-generation time. - The packer path moved:
dpcm_sampler/generate_dpcm_index.py:load_dpcm_index_into_packer(lines 38-79) is now the single shared call site (used from bothmain.py:run_export~lines 317-345 andmain.py:run_full_pipeline~lines 625-670) and readssample.get('pitch', 15)(line 75) andsample['filename'](line 66) —pitchis still absent from the shipped index. Confirmid/filenameremain the only keys any consumer can rely on, and thatgenerate_dpcm_index(dpcm_sampler/generate_dpcm_index.py:82-102) is still the sole writer (it emits exactlyid+filename).
Dimension 3: DPCM conversion correctness (1-bit delta)
dpcm_sampler/dpcm_converter.py does WAV→PCM→delta→packed-bits. This file has not
changed in the recent fix sprint — the following are still open, unverified claims:
delta_encode(lines 34-42) walks a ±1 step counter, butdpcm_compress(lines 45-66) then re-derives the bit purely fromencoded[i] > encoded[i-1]. Check that the two stages agree and that the bit polarity matches hardware: perdocs/APU_DMC_REFERENCE.md, a1bit adds 2 to the output level and0subtracts 2 (the engine never sets a level, only nudges ±2). A constant-input run encodes all-zero bits ⇒ the level ramps down on playback — verify the start-level assumption (prev = 0x40, line 36).- Bit packing order (line 63):
byte |= (bits[i+j] << j)packs LSB-first. Confirm against the DMC shifter order indocs/APU_DMC_REFERENCE.md("Reader → Buffer → Shifter") — wrong bit order plays the sample bit-reversed (audible garbage). - Resampling:
convert_wav_to_unsigned_pcm(line 7) usesnp.interplinear resampling to a fixedsample_rate=8000, independent of the DMC rate index written elsewhere (pitch/$4010). Flag any mismatch between the conversion rate and the playback rate index that would pitch-shift samples.
Dimension 4: Sample size / address / DMC range constraints
dpcm_sampler/dpcm_packer.py computes the $4012/$4013 register values:
- Fixed (#75 → regressed → #295/DP-01) — verify it holds:
_place_sample(lines 77-98) computesdpcm_address_val = (start_address - 0xC000) // 64(line 78) anddpcm_length_val = max(0, (sample['size'] + 14) // 16)(line 88). Verify againstdocs/APU_DMC_REFERENCE.md: address =$C000 + A*64, length =(L*16)+1bytes.length_regnow uses ceiling division ((size + 14) // 16= ceil((size-1)/16)) so asizenot of the form16k+1still gets its full tail read: the engine reads(length_reg*16)+1bytes and the.align 64gap after each sample makes the few extra bytes safe zero-pad, not neighbouring data. This is the flooring bug originally closed as #75 that regressed and was re-fixed here as #295 (commitd392ef6); confirm the ceiling still holds and thatsizestays bounded to 4081 so the 8-bit register can't overflow. - Fixed (verify): the 4081-byte oversized-sample path no longer aborts the pack.
DpcmPacker.add_sample(lines 13-47) now truncates to 4081 bytes whentruncate=True(lines 31-36) instead of always raisingValueError— and the shared call siteload_dpcm_index_into_packer(dpcm_sampler/generate_dpcm_index.py:72-77) always passestruncate=True, so in practice a too-long sample is clamped, not fatal (#68).add_samplestill raises if a caller explicitly passestruncate=False; confirm no current call site does that.dpcm_converter.dpcm_compress(line 66) independently truncates withdmc_bytes[:4081]at conversion time — the two truncation points are consistent (both clamp to 4081), not contradictory. START_ADDR = 0xC000,BANK_SIZE = 8192(lines 5-6), and the 60-bank ceiling (OverflowErrorat line 70). Check.align 64ingenerate_assembly(line 100) keeps every sample 64-byte aligned, and whether anything guards the$FFFF→$8000address-wrap quirk documented indocs/APU_DMC_REFERENCE.md(a sample bleeding past$FFFFplays garbage from$8000).- Fixed, verify no regression (#140): the packer used to receive the entire
1923-sample catalog regardless of what a song used, overflowing the 60-bank
budget and silencing percussion on every drummed song. It's now filtered via
get_dpcm_sample_ids_from_frames(dpcm_sampler/generate_dpcm_index.py:105-117, reads framenote = sample_id + 1) passed assample_ids=intoload_dpcm_index_into_packer, so only samples the exported song actually references get packed.generate_assembly(lines 91-147) now emits sparse lookup tables sized tomax_id + 1with$00placeholders for unpacked ids (lines 123-146) — verify those placeholder slots are provably unreachable (no frame indexes an id that wasn't packed) rather than merely "usually" unreachable.
Dimension 5: DMC level handling & DMA-timing implications
- Fixed, verify no regression (#72/D-09): the DMC output level (
$4011)CMD_DMC_LEVEL($87) emitter path was removed entirely (commit5c032d2) — no stage ever produceddmc_level, so the branch was dead. Confirm it hasn't been re-added; if it is re-added for the$4011non-linear-mixer trick (docs/APU_DMC_REFERENCE.md§6), re-check the level is clamped to the 7-bit 0–127 range$4011accepts before emission. - Silence init:
docs/APU_DMC_REFERENCE.mdsays init should write$00to$4011so the DMC counter doesn't muffle Triangle/Noise via the non-linear mixer. Live on the bytecode path:nes/audio_engine.asm:128writesLDA #$00/STA $4011ataudio_init. (The direct-exportinit_music/resetomits it — open gap #348/NH-HW-1; nes/mmc3_init.asm, which also had this write, was deleted as dead code, #203.) The live DPCM trigger is@write_dpcm(nes/audio_engine.asm, ~line 512), which writes$4010→$4012→$4013then toggles$4015(disable-then-enable-with-DMC), matchingdocs/APU_DMC_REFERENCE.md; re-verify this order is still correct if the trigger routine changes. (The oldseq_cmd_dpcm_play/seq_cmd_instrumentcopies innes/project_builder.pywere removed as dead code — #314/EXP-12; only the livefetch_sequence_byteremains.) - DMA cost:
docs/NES_DMA_REFERENCE.mdnotes each DMC DMA steals 3–4 CPU cycles and a heavy drum catalog fires constantly, delaying OAM DMA and corrupting side-effect reads ($4016/$2007/$4015). This subsystem can't avoid the cost, but flag if the generated engine/docs omit the mandatory DPCM-safe controller-read warning, or if rapid back-to-back triggers are emitted with no awareness of the cycle budget.
Dimension 6: Config robustness (DrumMapperConfig)
dpcm_sampler/enhanced_drum_mapper.py defines DrumPatternConfig (line 12),
SampleManagerConfig (line 53), DrumMapperConfig (line 95) with validate() /
to_file / from_file (line 163):
- Still open (#76/D-13):
from_file(lines 163-191) doesDrumPatternConfig(**config_data.get('pattern_detection', {}))(line 170) and the equivalent forSampleManagerConfig(line 172) — an unexpected/renamed key in the JSON raisesTypeError(not caught; onlyFileNotFoundErrorandjson.JSONDecodeErrorare handled, lines 188-191). No commit in the recent sprint touched this. Note the current blast radius: the CLI--configflag that used to feedmain.py:load_config(main.py:462-466) into this path was intentionally removed (main.py:772-774, #13) because nothing wired it toassign_tracks_to_nes_channels— so todayfrom_fileis reachable only via direct API use (and is exercised bytests/test_drum_mapper_config.py), not the CLI. Still worth fixing/hardening since it's public API surface. validate()(DrumMapperConfig.validate, line 110) enforces weight sums ≈ 1 and ranges.EnhancedDrumMapper.__init__(line 196) callsself.config.validate()unconditionally on whatever config object it holds, so a config passed in afterDrumMapperConfig.from_file(...)is validated before use, so long as it's routed throughEnhancedDrumMapper.__init__— confirm there's no path that uses afrom_file-loaded config directly without going through that constructor.SampleManagerConfig.memory_limitis bounded 1KB–16KB andmax_samples1–64 (lines 77-80), butDPCMSampleManager.__init__defaults (max_samples=16, memory_limit=4096) are declared independently (dpcm_sampler/dpcm_sample_manager.py:5). In the current codebase the only production instantiation site isEnhancedDrumMapper.__init__(enhanced_drum_mapper.py:203-206), which always passes the validated config values through — direct unvalidated construction only happens intests/test_dpcm_sample_manager.py. Low residual risk; flag only if a new production call site constructsDPCMSampleManagerdirectly.
Dimension 7: Sample-manager dedup & lifecycle
dpcm_sampler/dpcm_sample_manager.py (130 lines):
- Fixed, verify (#69/D-06):
allocate_sample(lines 15-65) now assigns ids from a monotonicself._next_idcounter (line 13, incremented at line 51) instead oflen(self.active_samples), so an evicted id is never handed out again to a later allocation. Confirmdpcm_eventsemitted before an eviction still resolve to the sample that was live when they were created (i.e. nothing re-keys already-emitted events). - Fixed, verify (#70/D-07): memory accounting is now unified —
allocate_samplechecksself._get_total_memory() + sample_size > self.memory_limitup front (line 42, accounting for the pending sample before it's inserted) and_get_total_memory(lines 120-130) sumss['metadata']['size'], the same fieldallocate_samplepopulates (line 57) — previously these used two different formulas (one of which,len(data)//8, was always 0 for real index data)._optimize_sample_bank(lines 67-111) now triggers on memory pressure alone, not just the sample-count limit (condition at lines 79-81). Verify a few-but-large-samples scenario (smallmax_samples, largemetadata.size) still evicts correctly. - Removed as dead code, not "fixed to use real data" (#71/D-08):
_find_similar_sample/_calculate_sample_similarityno longer exist in this file — they were deleted (commit5c032d2) rather than repaired, since the underlyingdataarrays are always empty for real index entries (Dimension 2) and the comparison was permanently inert. If similarity-based dedup is reintroduced, it needs real waveform data from the index to do anything useful.
Dimension 8: Channel-pipeline integration (noise vs DMC)
tracker/track_mapper.py:assign_tracks_to_nes_channels(midi_events, dpcm_index_path)
(line 179; called from main.py:run_map and the full pipeline):
- It calls
map_drums_to_dpcm(line 244) and routes results:nes_tracks['dpcm'] = dpcm_events(line 246-247, still overwrites any prior dpcm assignment) andnoise_eventsonly land onnoiseif it's still empty (lines 249-251). Fixed, but only partially (#74/D-11): whennoiseis already occupied, the drum noise-fallback events are still discarded (this is physically unavoidable — NES has one Noise channel, perdocs/APU_NOISE_REFERENCE.md) — but this is no longer silent: aprint(...)warning now reports how many events were dropped and why (tracker/track_mapper.py:253-259). Verify the count in the warning matcheslen(noise_events)exactly and that this is the only discard path for these events. - The hardcoded default
'dpcm_index.json'path means a missing file still raises inside_load_sample_index(enhanced_drum_mapper.py:216-219,FileNotFoundError). This is not caught in the standalonemapsubcommand (main.py:run_map, lines 104-112; the mapper call is line 110) — that path still crashes with a raw traceback (#256), unlike every other step-by-step guard inmain.py(load_json_stage, #120).run_full_pipelinefares better only because its whole body is wrapped in one outertry/except Exception(main.py:~992-997) that turns any such crash into a clean[ERROR] Pipeline failed: ...message — but the pipeline still aborts entirely rather than degrading to a drumless build, in contrast to the DPCM packer path (main.py:run_export/run_full_pipeline), which explicitly handles a missing index file gracefully ("No dpcm_index.json found, skipping"). Assess whetherrun_mapshould get the same guard as the packer path. - Fixed, verify (#9, #66, #67, #200/D-14, #254):
dpcm_eventscarry{frame, sample_id, velocity}; the frame-generation stage (nes/emulator_core.py:188-235) remaps the raw catalogsample_ids a song actually references to a dense, song-local0..N-1range (dense_id_of, line 218) and encodesnote = min(255, dense_id + 1)(line 227; 0 stays the rest sentinel), emitting adpcm_sample_map(dense_id → catalog_id) side table (lines 232-235) so the pack stage can resolve the real sample files. This replaced both the old 0-95 tone-note clamp (#67) and the rawmin(255, sample_id + 1)that aliased every catalog id ≥ 255 onto note 255 (#200/D-14); theMAX_SAFE_SAMPLE_ID = 254guard that used to pre-empt the remap and route all shipped-catalog drums to noise is also gone (#254). The exporter (exporter/exporter_ca65.py) applies the same byte-ceiling for the'dpcm'channel and the generated trigger routine (nes/project_builder.py) recovers the sample vianote - 1. Confirm the round-trip holds when a song references near 254 distinct drums (dense_id + 1must stay ≤ 255), not merely when a raw catalog id is near 254.
Skeptical checklist
- [ ] Does an unmapped/rare GM drum note (e.g. 47, mid tom) still resolve through
DEFAULT_MIDI_DRUM_MAPPING, or does it now silently fail some other way? - [ ] Does the velocity → primary → default fallback chain in
_resolve_dpcm_sample_nameever return a name that isn't actually inself.sample_index(a logic gap in the finalfor name in candidatesloop)? - [ ] Do
length/data/frequencyever come from the real index, or always defaults — and does that still matter now that the dead similarity/dedup code that depended ondatahas been removed? - [ ] Does
length_reg = max(0, (size+14)//16)(ceiling) read the full sample tail for a sample not sized16k+1(#75 regressed, re-fixed as #295 — verify)? - [ ] Does
DrumMapperConfig.from_filestill raise an uncaughtTypeErroron a stray config key (#76, unfixed)? - [ ] Can an evicted sample id still be reused now that
_next_idis monotonic — or is there an edge case (overflow, reset) that reintroduces reuse? - [ ] Is the memory limit actually enforced end-to-end now that both call sites use
metadata['size'], including the up-frontpending_sizecheck? - [ ] When a real noise track exists, are drum noise-fallback hits still discarded — and is the new warning message accurate?
- [ ] Does the
dpcmchannel's dense-remapped id survive correctly intomusic.asmwhen a song references near 254 distinct drums (dense_id + 1must stay ≤ 255)? - [ ] Does packing only referenced samples (#140) ever leave a frame pointing at an
id that wasn't packed (a
$00placeholder misread as a real sample)?
For every hardware claim, re-open the cited docs/APU_DMC_REFERENCE.md /
docs/NES_DMA_REFERENCE.md line and confirm before reporting. Attempt to disprove
each finding (per _audit-common.md) before including it.
Output
Write the report to: docs/audits/AUDIT_DPCM_<TODAY>.md (YYYY-MM-DD).
Structure:
- Summary — finding counts by severity, the highest-risk drop/round-trip issues.
- Findings — base format from
_audit-common.mdplus theHardware reffield.
Then suggest:
/audit-publish docs/audits/AUDIT_DPCM_<TODAY>.md
</content>From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/dpcm-drum-sampling-audit-msh3uu41/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/dpcm-drum-sampling-audit-msh3uu41' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/dpcm-drum-sampling-audit-msh3uu41/download' --output '.skill-downloads/dpcm-drum-sampling-audit-msh3uu41/SKILL.md.part' && mv '.skill-downloads/dpcm-drum-sampling-audit-msh3uu41/SKILL.md.part' '.skill-downloads/dpcm-drum-sampling-audit-msh3uu41/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.
Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/dpcm-drum-sampling-audit-msh3uu41/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.