# Okstra Coding Preflight

These rules apply to every worker writing or editing project code (executor and verifiers alike). Enforcement is self-check: the agent runs each rule's check immediately before reporting "done"; skipping a check is itself a contract violation.

## Core principles

1. **DRY — single reference point** — One implementation per capability. A second caller signals "extract shared logic", not "duplicate path".
2. **KISS — simplest sufficient design** — Add abstraction layers (helper modules, strategy/factory, configuration flags, indirection) only when an existing concrete call site requires them. *Self-check: name the second caller now; if you cannot, inline.* *Example violation: extracting `formatUserName()` helper used by exactly one call site, "in case we need it elsewhere".*
3. **YAGNI — build only for current requirements** — No speculative parameters, optional configs, "future-proof" hooks, or pre-1.0 backwards-compat shims. *Self-check: every newly introduced identifier has ≥1 current internal caller, or was explicitly user-requested.* *Example violation: adding `options?: { retries?, timeout? }` parameter when the current call passes nothing.*
4. **Clean Code — names carry WHAT, comments explain WHY** — Identifiers must make intent obvious; if a comment would describe WHAT the code does, rename instead. Reserve comments for non-obvious WHY (hidden constraint, workaround, surprising invariant). Delete dead/commented-out code immediately — git history is the archive. *Example — WHAT (rename instead): `// increment counter` above `i++`. WHY (keep): `// retry up to 3x: upstream returns 502 during deploys`.*
5. **Function length cap — 50 lines** — A single function/method body must stay within 50 lines, counting only effective code (exclude blank lines, comments, and pure data declarations such as large enums, lookup tables, or constant maps). Crossing the cap is an extraction signal, not a style nit. *Self-check: for any function newly added or substantially edited, count effective body lines; if over 50, split before declaring complete, or surface the violation and confirm with the user.*
6. **Simplest conforming approach** — When choosing how to implement or fix something, pick the simplest, most direct solution path that still conforms to the project's architecture and established conventions. This is broader than KISS (#2): KISS bounds abstraction; this bounds the whole approach. Simplicity never licenses bending the structure or a convention — rule out non-conforming paths first, then take the plainest of what remains. Self-check: name the architecture/convention boundary the chosen approach respects; if a simpler path respects it too, take that one.
7. **Fix at the cause, inside the change set** — An error surfacing during your work is fixed in the code that produced it — the files you are changing and the modules they directly call. A build/deploy/test failure is NOT a license to edit configuration or manifest files (`package.json`, lockfiles, `tsconfig`, CI config, build scripts) that were already passing before this change; a regression in something previously green almost always lives in the new code, not in long-stable config. If a config/manifest edit looks unavoidable, stop and confirm with the user first — state which file, why the current code can't carry the fix, and what was passing before. *Self-check: every file edited to resolve the error is in the current change set or a module it directly calls; no previously-passing config/manifest was touched to silence the failure without the user's explicit sign-off.* *Example violation: tests fail to import a new module, so you loosen `package.json`/`tsconfig` paths instead of fixing the import in the code you just wrote.*

## Routed resource selection

Read `clean-code.md` for the language-agnostic principles, then select language, framework, and architecture resources in three ordered stages. Each stage is a list of rules; a rule has one or more conditions; if ANY condition matches, include that rule's resource. Iterate EVERY rule in a stage — do not stop at the first match, because one change set can touch multiple languages, frameworks, or architectures. De-duplicate the final set, then state in one sentence which resources you applied (e.g., *"Applying TS + Node server + hexagonal; domain at src/domain/."*).

### Stage 1 — Language (iterate all rules)

| Resource | Include when any condition matches |
|---|---|
| [languages/javascript-typescript.md](languages/javascript-typescript.md) | a touched file is `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`; or a JS package manifest (`package.json`) implies JS/TS work |
| [languages/python.md](languages/python.md) | a touched file is `.py`; or manifests include `pyproject.toml`, `requirements.txt`, `setup.py`, `setup.cfg` |
| [languages/rust.md](languages/rust.md) | a touched file is `.rs`; or `Cargo.toml` is in scope |
| [languages/java.md](languages/java.md) | a touched file is `.java`; or manifests include `pom.xml` / `build.gradle` |
| [languages/kotlin.md](languages/kotlin.md) | a touched file is `.kt` / `.kts`; or manifests include `build.gradle.kts` |
| [languages/sql.md](languages/sql.md) | a touched file is `.sql`; migration directories are touched; `prisma/schema.prisma` is touched; or embedded query strings / ORM query builders change |

### Stage 2 — Framework / runtime (iterate all rules)

| Resource | Include when any condition matches |
|---|---|
| [frameworks/node-server.md](frameworks/node-server.md) | `package.json` or its dependencies/scripts show server-side Node work (`express`, `fastify`, `nestjs`, server entrypoints, API routes, CLI services); or a touched file is a Node runtime module |

Node.js server work also matches Stage 1's JavaScript/TypeScript rule — load both `languages/javascript-typescript.md` and `frameworks/node-server.md`.

### Stage 3 — Architecture (iterate all rules)

| Resource | Include when any condition matches |
|---|---|
| [architectures/hexagonal.md](architectures/hexagonal.md) | ports-and-adapters / hexagonal signals: `domain/` + `ports/` + `adapters/` (or `core/` + `infrastructure/` + `application/`), `*.port.*` files, NestJS hex split, or `abstract class` files at a domain boundary |

If a layout looks hexagonal but is non-standard, ask one question — *"does this project follow ports-and-adapters? where is the domain?"* — and record the answer. Architecture overlays default to none when no rule matches.

If no Stage 1 language rule matches (an unlisted language), stop and ask the user for the canonical style guide before writing code — do not invent a default.

## Mandatory pre-write checks (every language)

- [ ] Language reference read for this turn.
- [ ] `clean-code.md` principles applied: DRY, KISS, SOLID, YAGNI, meaningful naming (truthful + standalone + one identifier one meaning per file), single-purpose functions, plain-English summary test, 50-line cap, no magic numbers, shallow nesting, comments-explain-why.
- [ ] **Mutation and state boundaries** (`clean-code.md`): decide on the direct identifier, not a status/flag proxy; capture before-state in one snapshot ahead of the mutating boundary; update only this work's owned fields on an existing row; re-read state before calling a zero-affected-rows write success or failure; put priority-between-inputs in a named domain function; keep error messages to what was observed.
- [ ] Tests planned: which test(s) cover this change. New behaviour without a test is **incomplete** unless the user has explicitly opted out for this change.
- [ ] **Existing tests surveyed (run this before planning any new test):** for every surface this change touches, `grep -rln` the candidate test files, list their test names, then READ the bodies of the ones that could overlap. A list of names does not tell you what a test asserts, and behavioural overlap does not collide on names — treat a clean name search as unfinished, not as a clear result. Record per surface: `extend <path:line>` / `new (nothing covers <behaviour>)` / `retire <path:line> (behaviour moved to <where>)`. A new test or test file with no survey line is unfinished work. The survey may change the design — moving the seam so existing tests keep passing beats adding parallel ones. Carry out a `retire` verdict in the same commit, on behavioural grounds only: the behaviour it asserted is gone, or a new test asserts it more strongly through the same seam. "Looks similar" / "touches the same file" is not grounds; without grounds the test stays, because a silent coverage loss costs more than one extra test.
- [ ] **Testing discipline:** the test does not stub/spy methods on the SUT itself (collaborators are fine), and assertions are on outcomes (return values, state, events, boundary calls) — not on which internal helper was called. Each branch this change adds (`catch`, guard, early return, `else`) has a test that fails when the branch body is deleted; assertions land on the last write to a record, not an intermediate one; each test title names its unit and the single condition it isolates; no effect is claimed under its own mock; shared fixtures keep their ordinary defaults; the scenarios' setup values actually differ; every new test helper/mock is used by a test in this same change; no positional mock-argument access (`rg 'mock\.calls'`).
- [ ] **Third-party wrapper (when this change wraps a library call):** the wrapper adds behaviour the library does not already provide — read the installed library source before keeping a recovery branch — no comment names a condition the call site does not establish, and a rethrow keeps the original error as `cause`.
- [ ] **Hexagonal overlay (if loaded):** no business logic inside any port body, adapter methods are I/O only (no post-fetch JS filtering on domain state, no `findValid*`/`findActive*` adapter names hiding rules), all domain objects declared under `domain/`, no changed domain file importing outward (ORM / framework / adapters / services), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory — record it with the port sketch; blocking when the project declares `architecture.style = hexagonal` in `.okstra/project.json` — fix before write, never record-and-pass).
- [ ] **Existing code searched — by capability, not by name:** `grep` the identifier you are about to add AND the behaviour it performs (call sites, domain literals, error strings, the shape of the data it returns). A capability that already exists under a different name is the duplication this check exists to catch, and an identifier grep alone never finds it — name-level uniqueness is not evidence of absence. Record per new unit: the search terms used, the nearest existing implementation found, and the verdict `extend <path:line>` / `new (nothing covers <behaviour>)`. No survey line, no new unit.
- [ ] Project conventions checked: `.editorconfig`, `CONTRIBUTING.md`, formatter config (`.prettierrc`, `rustfmt.toml`, `ktlint`, `google-java-format`, etc.). **Project rules override this resource pack on conflict.**

## Completion sweep (before declaring a multi-file change done)

Per-file checks miss cross-cutting issues; each commit can be individually clean while the sum violates DRY. Before saying "done":

- [ ] **Domain-literal sweep:** `grep -rn` every domain enum value / predicate you added or touched in WHERE clauses, filters, or branches. The same literal at 2+ I/O sites is a *candidate* scattered decision — ask: would these sites change together when the business rule changes? Same decision → consolidate into one named constant or query builder in the domain layer and make every site reference it. Different decisions that merely share a value → leave them separate; coupling incidental duplication is worse than the repetition. (The identifier grep above does NOT catch this — sweep *values*, not just names.)
- [ ] **Stand-alone name test for exports:** for each exported identifier, look at its siblings — can a caller pick the right one from the names alone? If a comment must explain which to use, the name fails; encode the distinguishing fact in it (e.g., the input shape: `parseRows` vs `parseRowsFromFlatItems`).
- [ ] **Self-mock sweep:** for every test file you added or edited, `grep` it for the SUT-stubbing patterns of this language (full list in `languages/*.md` → "Self-mock signals to refuse") — e.g. `spyOn(sut`, `sut.<method> = jest.fn`, `spyk(sut`, `Mockito.spy(`, `@Spy` paired with `@InjectMocks`, `patch(`-ing the class under test, `mockall::mock!` of the unit itself, plus private-reach hacks (`(sut as any).`, `ReflectionTestUtils.invokeMethod(sut`). Any hit where the stubbed/replaced target **is the unit under test** — not an injected collaborator — is a refused self-mock: delete the stub and exercise the real method, or the test only proves its own wiring and survives even if the real implementation is deleted. Mocking injected collaborators at the boundary stays fine; this sweep targets only stubs on the SUT itself. If a method on the SUT feels too painful to leave real, that's a design signal (extract it to a collaborator), not a license to stub it. Enforced by `validators/detect_self_mock.py` (static); absent `qa/self-mock-*.json` sidecar BLOCKS at `validate-run.py`.
- [ ] **No documented forks:** two deliberate variants of one capability must not survive as parallel implementations with a comment explaining the delta. Re-read both bodies and check the deltas are genuinely parametric: if they reduce to a few orthogonal options, collapse into one implementation taking explicit option parameters that encode them. If encoding the delta would take more than ~3 options, or add more branching than the duplication it removes, they are two capabilities — keep two implementations with distinct honest names and delete the "variant of" framing. Either way the comment-documented fork dies. "The divergence is documented" stays a refused rationalization, and a two-capabilities verdict must come from reading the bodies, not from reluctance to refactor.

## Boundaries

- This preflight does **not** auto-format. Run the project's formatter yourself.
- This preflight does **not** replace repo-local rules. Repo rules win on conflict.
- This preflight does **not** cover every language. If the target language is missing, stop and ask.
