* Always read the full README.md before doing anything * Build commands: * `cmake --build ./build_release` * `cmake --build ./build_debug` * Never use `ninja` directly: it bypasses cmake's configuration and invalidates the build cache * Always try the release build first before building with the debug version * Use the debug build only when it is useful to obtain a clear stack trace with symbols, inspect names, place breakpoints, or test a small case interactively * The debug build is very slow, so use it only on small fast tests such as operation validations, not on network validations # Core engineering philosophy * Clean architecture matters as much as making the immediate test pass * Prefer fixes that preserve clear ownership boundaries, explicit invariants, and simple dataflow * Do not stack compensating fixes on top of earlier mistakes. If the current approach is becoming messy, stop and explain why * A correct fix should usually make the responsible producer, resolver, verifier, or lowering own the behavior directly * Avoid late repair passes, defensive cleanup, or broad rewrites when a cleaner owner-side fix is possible * Do not hide an upstream modeling bug by normalizing it later in the pipeline. Fix the producer when the producer owns the invariant * Prefer patterns/rewrites for local IR canonicalization. Use module walks only when pass-level structural analysis genuinely requires them * Prefer compact, structured designs over long case-by-case implementations # Think before coding * State assumptions explicitly before implementing when they affect the design * If multiple interpretations exist, present them instead of silently choosing one * If a simpler approach exists, say so and prefer it unless there is a clear reason not to * If something is unclear, stop, name what is confusing, and ask * If the requested or obvious approach would make the architecture worse, push back and propose a cleaner alternative # Code changes * Keep changes minimal and localized to the relevant parts of the code * Preserve the existing naming conventions and coding style used in the surrounding code * Keep code easy to read, well organized, and suitable for future extensibility * A function must not exceed roughly 200/250 lines. If a change pushes a function beyond that, extract focused helpers * Prefer clear naming and structure over comments. Add comments only when they materially improve clarity * Do not rename symbols, move files, or restructure modules unless that is necessary for the requested change * Avoid duplicate ad-hoc logic. If the same concept appears in multiple places, consider whether it deserves a shared helper/API * When adding a helper or API, ask: * Could this be useful to another component now * Is another component already implementing the same idea differently * Is this likely to be needed by a future adjacent component * What is the narrowest useful abstraction * What is the correct ownership level for this API * If a shared API is justified, place it at the lowest clean layer that can be used by all relevant consumers without creating dependency cycles or leaking policy across layers * If an existing component should use a newly introduced shared API, refactor that component in the same patch when doing so is directly related and reduces duplication * Do not create broad frameworks just because a helper might someday be useful. Shared APIs should encode a real reusable concept, not speculative generality * If the reusable abstraction is plausible but not clearly needed yet, keep the code local and mention the possible future extraction separately # Avoid case-listing designs * Avoid solving problems with large chains of `if`/`else`, switches, or repeated special cases that enumerate every possible situation * Long case listings tend to overfit the current tests, grow the codebase, and hide the underlying abstraction * When you see a growing list of special cases, stop and look for the shared concept, data model, interface, or normalization step that would make the cases collapse * Prefer table-driven logic, traits/interfaces, small reusable predicates, structured dispatch, or producer-side normalization when they express the invariant more directly * A few explicit cases are fine when the domain is genuinely small and closed * If the list is likely to grow, refactor toward a cleaner and more compact design instead of adding another branch * When keeping a case list is the pragmatic choice, explain why the domain is closed or why a broader abstraction would be premature # Ownership and invariants Before implementing, identify the owner of the behavior: * A producer should emit IR/data that satisfies the contract of the next stage * A lowering should make representation changes explicit and semantically correct * A resolver should resolve existing structure without silently changing semantics * A verifier should reject invalid states with bounded, actionable diagnostics * Codegen should assume verified invariants and fail clearly if they are violated When fixing a bug: * State the invariant that was violated * State which component should own that invariant * Fix that component directly * Avoid fixes that merely mask the violation later in the pipeline * Add or preserve verification if the invariant is important enough to regress # Refactor and API policy You may propose or implement a refactor when: * the local fix would duplicate logic * the local fix would violate a layer boundary * the bug exists because responsibility is assigned to the wrong component * multiple components already implement ad-hoc variants of the same concept * a shared helper/API would make the code smaller, clearer, and easier to maintain * existing callers can be migrated cleanly without broad churn * the current implementation is turning into a long list of special cases instead of a structured solution When proposing or implementing a refactor: * Explain what responsibility is being moved or shared * Justify why the new location is the right ownership level * Keep the API narrow and named after the concept or invariant it represents * Migrate directly related existing users when that improves compactness and consistency * Separate changes required for correctness from optional cleanup * Avoid unrelated renames, formatting changes, or module moves * Do not expand a justified refactor beyond directly related callers Do not refactor when: * the issue is truly local and a local fix is clearer * the abstraction would have only one user and no clear adjacent use * the abstraction would mix policies from different layers * the refactor would affect unrelated behavior * the refactor is mainly aesthetic # Working style * Infer style and conventions from the existing code before introducing new patterns * When several implementation options are possible, prefer the simplest one that fits the current architecture and minimizes churn * Push back when the requested or obvious fix would make the architecture worse * If a cleaner fix requires a small refactor or shared helper/API, propose it explicitly and justify it * Avoid broad refactors unless explicitly requested or clearly necessary for correctness and maintainability * When tests fail, bucket failures by likely root cause and separate patch-related failures from pre-existing or out-of-scope failures # Simplicity first * Minimum code that solves the problem cleanly. Nothing speculative * No features beyond what was asked * No error handling for impossible scenarios * If you write 200 lines and it could be 50, rewrite it * Ask: “Would a senior engineer say this is overcomplicated?” If yes, simplify * Prefer direct, explicit code over generic machinery unless the generic machinery clearly reduces duplication and preserves boundaries # Fallbacks and defaults * Avoid silent fallback behavior when the semantic category is unknown * Do not treat “unknown” as “safe” unless the codebase already defines that convention * If a value cannot be classified, either preserve the existing behavior deliberately or fail with a clear diagnostic * When adding a fallback, state why it is semantically valid and what invariant makes it safe # Surgical changes * Touch only what you must * Clean up only the mess introduced by your own change * Do not “improve” adjacent code, comments, or formatting * Match existing style, even if you would personally do it differently * If you notice unrelated dead code, bad abstractions, or fragile design, mention it separately. Do not delete or rewrite it unless asked * When your changes create orphans, remove imports, variables, functions, or files made unused by your change * Every changed line should trace directly to the requested fix, a required cleanup, or a justified reuse/refactor decision # Diagnostics and verification * Use existing bounded diagnostic mechanisms for pass-level verification or codegen failures * Do not emit unbounded repeated diagnostics from loops or parallel workers * Diagnostics should identify the violated invariant and the relevant value/op when useful * Verifiers should reject invalid states, not repair them * Codegen should not compensate for invalid IR/data unless codegen is the owner of that invariant * Do not make failing tests pass by weakening verifiers, assertions, or diagnostics unless the check itself is proven wrong * If a check is too strict, explain the valid case it rejects and update the invariant accordingly * Prefer fixing invalid IR/data producers over relaxing consumers * If adding diagnostics only for debugging, remove them or cap them before finalizing # Temporary debugging code * Temporary diagnostics, dumps, assertions, and debug-only helpers must be removed or intentionally converted into bounded permanent diagnostics before finalizing * If debug instrumentation remains, explain why it is useful as permanent infrastructure * Do not leave noisy validation output behind # Performance awareness * Avoid algorithmic regressions in compiler passes, especially repeated full-module walks, repeated expensive analyses, or per-op recomputation inside nested loops * If a change adds a walk, cache, analysis, or structural traversal, justify why it is needed * For hot paths, prefer preserving existing asymptotic behavior unless a better structure is part of the requested change * If performance may change, mention the expected impact and suggest a targeted timing check # Goal-driven execution For multi-step tasks, state a brief plan: 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] Define success criteria before implementing: * For bug fixes, success means reproducing or identifying the failure, fixing the responsible owner, and verifying the targeted case * For refactors, success means preserving behavior while making ownership, reuse, or structure cleaner * For validation changes, success means checking both valid and invalid cases when applicable Transform tasks into verifiable goals: * “Fix the bug” → identify the invariant, reproduce the failure, fix the owner, verify the targeted case * “Add validation” → write or identify tests for invalid inputs, then make them pass/fail as expected * “Refactor X” → preserve behavior before and after, then run relevant tests # Final self-review Before reporting completion, check: * Did I fix the owner of the invariant rather than masking the issue downstream * Did I avoid broad case lists and ad-hoc special handling * Did I introduce a helper/API only at the right ownership level * Did I migrate directly related duplicate logic when doing so improves compactness * Did I avoid weakening verifiers or assertions unnecessarily * Did I remove temporary debugging code or make it bounded and intentional * Did I avoid unrelated formatting, renames, or cleanup * Did I consider performance impact for added walks, analyses, caches, or repeated computations * Did I run the required build/test commands * Did I clearly report remaining failures or risks When reporting back: * Say what changed * Say what was verified * Say what remains * When showing code in chat, make it easy to copy-paste into the codebase * Keep outputs focused on the changed parts * List bad practices, fragile assumptions, or cleaner alternatives separately * If a change is intentionally pragmatic rather than architecturally ideal, say so and explain the tradeoff