okstra 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.kr.md +8 -7
- package/README.md +8 -7
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +23 -24
- package/docs/kr/cli.md +6 -6
- package/docs/project-structure-overview.md +13 -9
- package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +559 -0
- package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +121 -0
- package/docs/task-process/error-analysis.md +1 -1
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +18 -14
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/gemini-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +14 -12
- package/runtime/bin/lib/okstra/usage.sh +6 -0
- package/runtime/bin/okstra-render-report-views.py +1 -1
- package/runtime/bin/okstra-team-reconcile.sh +28 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/launch.template.md +4 -2
- package/runtime/prompts/profiles/_common-contract.md +15 -15
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +3 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +10 -9
- package/runtime/prompts/profiles/implementation.md +1 -1
- package/runtime/prompts/profiles/improvement-discovery.md +5 -5
- package/runtime/prompts/profiles/release-handoff.md +2 -2
- package/runtime/prompts/profiles/requirements-discovery.md +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
- package/runtime/python/okstra_ctl/clarification_items.py +11 -11
- package/runtime/python/okstra_ctl/context_cost.py +308 -0
- package/runtime/python/okstra_ctl/migrate.py +2 -12
- package/runtime/python/okstra_ctl/paths.py +22 -0
- package/runtime/python/okstra_ctl/render.py +285 -126
- package/runtime/python/okstra_ctl/render_final_report.py +32 -1
- package/runtime/python/okstra_ctl/report_views.py +12 -12
- package/runtime/python/okstra_ctl/run.py +510 -248
- package/runtime/python/okstra_ctl/sequence.py +2 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
- package/runtime/python/okstra_ctl/wizard.py +219 -136
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree.py +13 -5
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/skills/okstra-brief/SKILL.md +1 -1
- package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
- package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
- package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
- package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
- package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
- package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
- package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
- package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
- package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
- package/runtime/skills/okstra-convergence/SKILL.md +8 -8
- package/runtime/skills/okstra-inspect/SKILL.md +100 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +27 -23
- package/runtime/skills/okstra-run/SKILL.md +3 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +8 -5
- package/runtime/templates/reports/final-report.template.md +188 -187
- package/runtime/templates/reports/i18n/en.json +4 -4
- package/runtime/templates/reports/i18n/ko.json +4 -4
- package/runtime/templates/reports/implementation-planning-input.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +1 -1
- package/runtime/templates/reports/user-response.template.md +1 -1
- package/runtime/templates/worker-prompt-preamble.md +4 -4
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/validate-implementation-plan-stages.py +9 -9
- package/runtime/validators/validate-report-views.py +10 -10
- package/runtime/validators/validate-run.py +36 -36
- package/runtime/validators/validate_improvement_report.py +8 -8
- package/src/_python-helper.mjs +3 -3
- package/src/context-cost.mjs +27 -0
- package/src/install.mjs +1 -0
- package/src/uninstall.mjs +1 -0
|
@@ -172,7 +172,7 @@ def preview_worktree_decision(
|
|
|
172
172
|
helpers so preview never diverges from the actual provisioning result.
|
|
173
173
|
"""
|
|
174
174
|
project_root = Path(project_root)
|
|
175
|
-
if not
|
|
175
|
+
if not is_git_work_tree(project_root):
|
|
176
176
|
return WorktreeDecision(status="skipped-not-git", path=str(project_root))
|
|
177
177
|
if _is_inside_non_main_worktree(project_root):
|
|
178
178
|
return WorktreeDecision(status="skipped-in-worktree", path=str(project_root))
|
|
@@ -232,8 +232,16 @@ def _is_inside_non_main_worktree(project_root: Path) -> bool:
|
|
|
232
232
|
return common_abs != per_tree_abs
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
def
|
|
236
|
-
|
|
235
|
+
def is_git_work_tree(project_root: Path) -> bool:
|
|
236
|
+
"""project_root 가 git work tree 내부인지 판정하는 public git-introspection seam.
|
|
237
|
+
|
|
238
|
+
git 미설치(FileNotFoundError) 를 포함한 모든 실패는 False — 호출자(migrate
|
|
239
|
+
등)가 non-git 레이아웃으로 안전하게 폴백할 수 있도록 한다. 과거 migrate.py
|
|
240
|
+
가 같은 판정을 자체 구현(`--show-toplevel`)했는데 이 seam 으로 통합한다."""
|
|
241
|
+
try:
|
|
242
|
+
res = _git(project_root, "rev-parse", "--is-inside-work-tree")
|
|
243
|
+
except (OSError, FileNotFoundError):
|
|
244
|
+
return False
|
|
237
245
|
return res.returncode == 0 and res.stdout.strip() == "true"
|
|
238
246
|
|
|
239
247
|
|
|
@@ -260,7 +268,7 @@ def _resolve_commit_sha(cwd: Path, ref: str) -> str:
|
|
|
260
268
|
return res.stdout.strip()
|
|
261
269
|
|
|
262
270
|
|
|
263
|
-
def
|
|
271
|
+
def main_worktree_path(project_root: Path) -> Path:
|
|
264
272
|
"""Locate the repository's MAIN worktree (the original checkout).
|
|
265
273
|
|
|
266
274
|
`git worktree list --porcelain` lists worktrees in a stable order
|
|
@@ -601,7 +609,7 @@ def provision_task_worktree(
|
|
|
601
609
|
"work-category before retrying."
|
|
602
610
|
)
|
|
603
611
|
|
|
604
|
-
main_root =
|
|
612
|
+
main_root = main_worktree_path(project_root)
|
|
605
613
|
requested_base = (base_ref or "").strip()
|
|
606
614
|
if not requested_base and require_base_ref:
|
|
607
615
|
raise RuntimeError(
|
|
@@ -83,6 +83,10 @@
|
|
|
83
83
|
"approved": {
|
|
84
84
|
"type": "boolean",
|
|
85
85
|
"description": "사용자 승인 플래그. report-writer 는 항상 false 로 발행하고, 사용자가 `--approve` 또는 직접 편집으로 true 로 토글합니다. `implementation` task-type 의 prepare 단계가 `--approved-plan` 으로 지정된 보고서의 이 필드를 읽어 진입 여부를 결정합니다. 다른 task-type 에서는 의미 없이 false 로 유지됩니다."
|
|
86
|
+
},
|
|
87
|
+
"implementationOption": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "유저가 implementation-planning final-report 에서 고른 Option Candidate 이름. report-writer 는 항상 빈 문자열로 발행하고, 사용자가 `--implementation-option <name>` 또는 직접 편집으로 채웁니다. `implementation` task-type 이 이 값으로 진행하며, 비어 있으면 plan 의 `Recommended Option` 으로 폴백합니다. 다른 task-type 에서는 의미 없이 빈 값으로 유지됩니다."
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
},
|
|
@@ -760,7 +760,7 @@ If the list is non-empty, run **one** `AskUserQuestion`:
|
|
|
760
760
|
1. `Yes — collect now (Recommended)` — proceed to 6.5c.
|
|
761
761
|
2. `No — leave for the downstream phase` — set
|
|
762
762
|
`reporter-confirmations: skipped`. The phase will promote each
|
|
763
|
-
pending row into its own `##
|
|
763
|
+
pending row into its own `## 1. Clarification Items` as
|
|
764
764
|
`Blocks=next-phase` (`Blocks=approval` only in
|
|
765
765
|
`implementation-planning`); see each phase profile's "Brief
|
|
766
766
|
consumption" addendum.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okstra-coding-preflight
|
|
3
|
+
description: Provides language-specific coding conventions, idioms, and test-writing guidance for Java, Kotlin, JavaScript, TypeScript, Node.js, Python, SQL, and Rust. okstra lead/workers MUST consult this skill before writing, editing, refactoring, or testing code in any of these languages — including any new file, function, or test — even when the task seems simple enough to handle directly. Detect the target language from the file extension, project config (package.json/Cargo.toml/pyproject.toml/pom.xml/build.gradle), or task brief, and apply the matching conventions.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# okstra Code Preflight
|
|
8
|
+
|
|
9
|
+
## These 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.
|
|
10
|
+
|
|
11
|
+
1. **DRY — single reference point** — One implementation per capability. A second caller signals "extract shared logic", not "duplicate path".
|
|
12
|
+
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".*
|
|
13
|
+
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.*
|
|
14
|
+
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`.*
|
|
15
|
+
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.*
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Before writing or editing **any** code:
|
|
21
|
+
|
|
22
|
+
1. Identify the target language (file extension, project config, or explicit user request).
|
|
23
|
+
2. Read the matching language reference from `languages/`.
|
|
24
|
+
3. Read [clean-code.md](clean-code.md) for the language-agnostic principles.
|
|
25
|
+
4. **Check for architecture overlays.** If the project uses ports-and-adapters (signals: `domain/` + `ports/` + `adapters/` folders, `*.port.*` files, NestJS hex split), also read [architecture/hexagonal.md](architecture/hexagonal.md). Record the detected layout in one line so later edits don't re-discover it.
|
|
26
|
+
5. In **one short sentence** to the user, state which conventions you will apply (e.g., *"Applying Kotlin conventions + hexagonal overlay; domain at `src/domain/`."*).
|
|
27
|
+
6. Then write code.
|
|
28
|
+
|
|
29
|
+
If the language is not listed below, stop and ask the user for the canonical style guide they want to follow before writing code.
|
|
30
|
+
|
|
31
|
+
## Language router
|
|
32
|
+
|
|
33
|
+
| Language | Reference | Triggers |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Java | [languages/java.md](languages/java.md) | `.java`, `pom.xml`, `build.gradle` |
|
|
36
|
+
| Kotlin | [languages/kotlin.md](languages/kotlin.md) | `.kt`, `.kts`, `build.gradle.kts` |
|
|
37
|
+
| JavaScript / TypeScript | [languages/javascript-typescript.md](languages/javascript-typescript.md) | `.js`, `.ts`, `.tsx`, `.jsx` |
|
|
38
|
+
| Node.js (server) | [languages/nodejs.md](languages/nodejs.md) | `package.json` with server entry, `express`, `fastify`, `nestjs` |
|
|
39
|
+
| Python | [languages/python.md](languages/python.md) | `.py`, `pyproject.toml`, `requirements.txt`, `setup.py`, `setup.cfg` |
|
|
40
|
+
| SQL | [languages/sql.md](languages/sql.md) | `.sql`, migration directories, `prisma/schema.prisma`, raw queries embedded in code |
|
|
41
|
+
| Rust | [languages/rust.md](languages/rust.md) | `.rs`, `Cargo.toml` |
|
|
42
|
+
|
|
43
|
+
For Node.js work, load **both** `javascript-typescript.md` and `nodejs.md`.
|
|
44
|
+
|
|
45
|
+
## Architecture overlays
|
|
46
|
+
|
|
47
|
+
Loaded **in addition to** the language reference when the project matches. Overlays add architectural rules that cut across languages.
|
|
48
|
+
|
|
49
|
+
| Overlay | Reference | When to load |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Hexagonal (ports & adapters) | [architecture/hexagonal.md](architecture/hexagonal.md) | Project has `domain/` + `ports/` + `adapters/` (or equivalent: `core/`, `infrastructure/`, `application/`), `*.port.*` files, or `abstract class` files at a domain boundary. Confirm once with the user if ambiguous. |
|
|
52
|
+
|
|
53
|
+
If you suspect an overlay applies but the layout is non-standard, ask one question — *"does this project follow ports-and-adapters? where is the domain?"* — and record the answer.
|
|
54
|
+
|
|
55
|
+
## Mandatory pre-write checks (every language)
|
|
56
|
+
|
|
57
|
+
- [ ] Language reference read for this turn.
|
|
58
|
+
- [ ] `clean-code.md` principles applied: DRY, KISS, SOLID, YAGNI, meaningful naming (truthful + standalone), single-purpose functions, plain-English summary test, 50-line cap, no magic numbers, shallow nesting, comments-explain-why.
|
|
59
|
+
- [ ] 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.
|
|
60
|
+
- [ ] **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.
|
|
61
|
+
- [ ] **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/`.
|
|
62
|
+
- [ ] Existing code searched: `grep` for the symbol / file / identifier you are about to add. Do not duplicate.
|
|
63
|
+
- [ ] Project conventions checked: `.editorconfig`, `CONTRIBUTING.md`, formatter config (`.prettierrc`, `rustfmt.toml`, `ktlint`, `google-java-format`, etc.). **Project rules override this skill on conflict.**
|
|
64
|
+
|
|
65
|
+
## Boundaries
|
|
66
|
+
|
|
67
|
+
- This skill does **not** auto-format. Run the project's formatter yourself.
|
|
68
|
+
- This skill does **not** replace repo-local rules. Repo rules win on conflict.
|
|
69
|
+
- This skill does **not** cover every language. If the target language is missing, stop and ask.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Hexagonal Architecture (Ports & Adapters) Overlay
|
|
2
|
+
|
|
3
|
+
Load this **in addition to** the matching language reference when the project follows ports-and-adapters. Detection signals:
|
|
4
|
+
|
|
5
|
+
- A `domain/` (or `domains/`, `core/`) folder holding entities and value objects.
|
|
6
|
+
- A `ports/` folder, files matching `*.port.*`, or `abstract class` / `interface` files declared at a domain boundary.
|
|
7
|
+
- An `adapters/` (or `infrastructure/`) folder holding implementations of those ports.
|
|
8
|
+
- Build config / framework that names the pattern explicitly (e.g., NestJS module split into `domain` / `application` / `infrastructure`).
|
|
9
|
+
|
|
10
|
+
If unsure, ask the user once: *"Does this project use ports-and-adapters? What folder is the domain in?"* — and record the answer in one line so subsequent edits don't re-discover it.
|
|
11
|
+
|
|
12
|
+
These rules are language-agnostic. The examples use TypeScript/Java-ish syntax; translate to Rust traits, Kotlin interfaces, etc., as needed.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Rule H1 — Ports stay thin
|
|
17
|
+
|
|
18
|
+
A port is the interface (or abstract class) at the domain/application boundary. The port declares **shape**, not behavior.
|
|
19
|
+
|
|
20
|
+
A port file's method body — or the interface contract itself — must **not** contain:
|
|
21
|
+
|
|
22
|
+
- `if` statements that branch on domain state.
|
|
23
|
+
- Validation that throws or returns errors based on input shape.
|
|
24
|
+
- Filtering (`.filter(...)`) over domain collections.
|
|
25
|
+
- Business calculations (math on amounts, date arithmetic that expresses a rule, etc.).
|
|
26
|
+
|
|
27
|
+
If the port file's method body does any of the above, the logic belongs in the domain (an invariant on the entity, a domain service) — or in the adapter if it's truly I/O-shaped.
|
|
28
|
+
|
|
29
|
+
Violations:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// bad: validation in a port body
|
|
33
|
+
abstract class UserRepository {
|
|
34
|
+
async save(user: User) {
|
|
35
|
+
if (!user.email) throw new Error('email required'); // rule check in port
|
|
36
|
+
return this.persist(user);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// bad: filtering in a port
|
|
41
|
+
abstract class OrderRepository {
|
|
42
|
+
async findPending(orders: Order[]) {
|
|
43
|
+
return orders.filter(o => o.status === 'pending'); // domain rule in port
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Not a violation:
|
|
49
|
+
|
|
50
|
+
- A pure interface with no method bodies.
|
|
51
|
+
- An abstract method with no implementation.
|
|
52
|
+
- A port file that *imports* domain objects (that's the correct pattern).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Rule H2 — Adapters do I/O, not business logic
|
|
57
|
+
|
|
58
|
+
Same principle as H1, on the adapter side. The adapter's job is to implement **just** the I/O — a SQL query, an HTTP call, a filesystem read. It is **not** the place to filter by domain state, branch on domain values, or compute business answers.
|
|
59
|
+
|
|
60
|
+
Smells in adapter methods:
|
|
61
|
+
|
|
62
|
+
- A `.filter(x => x.status === 'active')` predicate over domain state, *after fetching rows*.
|
|
63
|
+
- An `if` that rejects results based on a domain rule (*"skip if expired"*, *"exclude shared items"*).
|
|
64
|
+
- A method whose name encodes a business rule — `findValid*` / `findActive* `/ `findEligible*`. The adapter should fetch *things*; the caller (or the query itself) should apply the rule.
|
|
65
|
+
- Multiple joins and conditions assembled specifically to satisfy a compound domain rule — the adapter is answering a business question rather than offering a primitive fetch.
|
|
66
|
+
|
|
67
|
+
Judgment: an adapter **can** push filtering into SQL (`WHERE status = 'active'`) — that's still I/O-shaped. The smell is when:
|
|
68
|
+
|
|
69
|
+
- The filtering is *application code after the fetch*, or
|
|
70
|
+
- The method name itself encodes a domain rule that a reader cannot tell from the name alone.
|
|
71
|
+
|
|
72
|
+
A domain-flavored method name (`findValid*` / `findActive*` / `findEligible*`) is not automatically a violation. Accept it when **either** holds:
|
|
73
|
+
|
|
74
|
+
- The query body is still simple — a single `WHERE` clause on an obvious column, no compound predicate.
|
|
75
|
+
- Moving the predicate out would force an N+1 (the caller would have to fetch all rows and round-trip per row to decide which are valid).
|
|
76
|
+
|
|
77
|
+
Reject it when the query is complex enough that a reader can't tell what "valid"/"active"/"eligible" means without reverse-engineering the SQL. At that point the rule really is hiding in infrastructure — move the predicate to the domain and rename the adapter to `findAll` / `findByX`.
|
|
78
|
+
|
|
79
|
+
Not a smell:
|
|
80
|
+
|
|
81
|
+
- Simple `WHERE` clauses for *data-shape* reasons (`WHERE user_id = ?`) — that's scoping the fetch, not a business rule.
|
|
82
|
+
- Pagination, ordering, basic projection.
|
|
83
|
+
- Mapping raw rows to domain objects — that's the adapter's translation job.
|
|
84
|
+
|
|
85
|
+
Why it matters: when adapters hold business rules, the rules become invisible from the domain. A reviewer reading the domain sees a clean method; the rule is hidden two layers down in a repository. Changes to the rule require editing infrastructure code. Tests of the rule are forced through I/O. The whole point of ports-and-adapters collapses.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Rule H3 — Domain objects live in the domain folder
|
|
90
|
+
|
|
91
|
+
Entities, value objects, domain types / enums, and domain errors must be declared under the project's `domain/` folder (or whatever this repo calls it). They must **not** be declared inside a port file or an adapter file.
|
|
92
|
+
|
|
93
|
+
Violations:
|
|
94
|
+
|
|
95
|
+
- `class Order { ... }` declared in `order.port.ts`.
|
|
96
|
+
- `type OrderStatus = 'pending' | 'shipped'` declared inside `order.repository.adapter.ts`.
|
|
97
|
+
- `class OrderNotFoundError extends Error {}` declared in a port file but never defined in `domain/`.
|
|
98
|
+
- An interface representing a domain concept (not a port contract) declared outside `domain/`.
|
|
99
|
+
|
|
100
|
+
Not a violation:
|
|
101
|
+
|
|
102
|
+
- Domain objects *imported* into a port or adapter file — that's the correct pattern.
|
|
103
|
+
- DTOs (data-transfer types used only by an adapter for wire shape) living next to the adapter.
|
|
104
|
+
- Utility types used only by an adapter's implementation.
|
|
105
|
+
|
|
106
|
+
The test: **is this type/class a thing the business talks about, or is it plumbing?** Business things belong in `domain/`. Plumbing can live near the plumbing.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Quick checklist before declaring a port/adapter change complete
|
|
111
|
+
|
|
112
|
+
- [ ] No `if` / `.filter` / validation / business math inside any port body.
|
|
113
|
+
- [ ] Adapter methods are I/O only — any post-fetch JS filtering moved into the query or back to the caller.
|
|
114
|
+
- [ ] Adapter method names are neutral (`findAll`, `findByUserId`) unless the domain-flavored name passes the H2 judgment test.
|
|
115
|
+
- [ ] Every entity, value object, domain enum, and domain error is declared under `domain/`.
|
|
116
|
+
- [ ] Port files only declare shape and `import` domain types — they don't *define* them.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Clean Code Principles (language-agnostic)
|
|
2
|
+
|
|
3
|
+
Apply these on every change. They override personal taste. They do **not** override project-local rules.
|
|
4
|
+
|
|
5
|
+
## DRY — Don't Repeat Yourself
|
|
6
|
+
|
|
7
|
+
A second copy of the same logic is a signal to extract. Two callers of the same algorithm should call the same function.
|
|
8
|
+
|
|
9
|
+
Bad:
|
|
10
|
+
```javascript
|
|
11
|
+
let total = 0;
|
|
12
|
+
for (let i = 0; i < prices.length; i++) total += prices[i];
|
|
13
|
+
// later, elsewhere:
|
|
14
|
+
let t = 0;
|
|
15
|
+
for (let i = 0; i < prices.length; i++) t += prices[i];
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Good:
|
|
19
|
+
```javascript
|
|
20
|
+
const sum = (arr) => arr.reduce((acc, x) => acc + x, 0);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Caveat: do not extract until the duplication is real (rule of three). Premature abstraction is also a code smell.
|
|
24
|
+
|
|
25
|
+
## KISS — Keep It Simple
|
|
26
|
+
|
|
27
|
+
The simplest solution that meets the requirement wins. Cleverness has a maintenance cost.
|
|
28
|
+
|
|
29
|
+
Bad:
|
|
30
|
+
```javascript
|
|
31
|
+
if (data !== null || data !== undefined || data !== '') { ... }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Good:
|
|
35
|
+
```javascript
|
|
36
|
+
if (data) { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## SOLID
|
|
40
|
+
|
|
41
|
+
- **S**ingle Responsibility — one reason to change per class / module.
|
|
42
|
+
- **O**pen/Closed — open for extension, closed for modification. Prefer composition.
|
|
43
|
+
- **L**iskov Substitution — a subtype must honour the parent's contract.
|
|
44
|
+
- **I**nterface Segregation — many small interfaces beat one fat one.
|
|
45
|
+
- **D**ependency Inversion — depend on abstractions, not concrete implementations.
|
|
46
|
+
|
|
47
|
+
Bad (SRP):
|
|
48
|
+
```python
|
|
49
|
+
class User:
|
|
50
|
+
def save(self): ...
|
|
51
|
+
def send_email(self, msg): ...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Good (SRP):
|
|
55
|
+
```python
|
|
56
|
+
class UserRepository:
|
|
57
|
+
def save(self, user): ...
|
|
58
|
+
|
|
59
|
+
class EmailService:
|
|
60
|
+
def send(self, user, msg): ...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## YAGNI — You Aren't Gonna Need It
|
|
64
|
+
|
|
65
|
+
Implement what is required **now**. Do not add knobs, options, hooks, or layers for a hypothetical future caller.
|
|
66
|
+
|
|
67
|
+
Bad:
|
|
68
|
+
```javascript
|
|
69
|
+
function calculator(a, b, op) {
|
|
70
|
+
switch (op) {
|
|
71
|
+
case 'add': return a + b;
|
|
72
|
+
case 'multiply': return a * b; // not asked for
|
|
73
|
+
case 'divide': return a / b; // not asked for
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Good:
|
|
79
|
+
```javascript
|
|
80
|
+
const add = (a, b) => a + b;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Meaningful naming
|
|
84
|
+
|
|
85
|
+
Names reveal intent. `d` and `crtUsrAcct()` are bugs; `elapsedTimeInDays` and `createUserAccount()` are documentation.
|
|
86
|
+
|
|
87
|
+
### Names must be truthful
|
|
88
|
+
|
|
89
|
+
The name must describe what the function actually does. Misleading names break reader expectations and survive renames.
|
|
90
|
+
|
|
91
|
+
Flag and rename:
|
|
92
|
+
|
|
93
|
+
- `get*` / `fetch*` / `find*` that also mutate state, throw, or write logs / audits.
|
|
94
|
+
- `is*` / `has*` / `can*` that throw instead of returning a boolean.
|
|
95
|
+
- `find*` that *creates* the entity when missing → `findOrCreate*` or `ensure*`.
|
|
96
|
+
- Names that describe the implementation, not the intent: `loopOverUsers` → `notifyActiveUsers`, `runQuery` → `listOverdueInvoices`.
|
|
97
|
+
- Names that say the opposite of what they do under some branch: `enableFoo` that disables when a flag is off, with no hint in the name.
|
|
98
|
+
|
|
99
|
+
### Names must stand alone
|
|
100
|
+
|
|
101
|
+
The test: if the name appeared in a stacktrace, an autocomplete list, or a grep result, would the reader know what it does without opening the file? If not, add specificity.
|
|
102
|
+
|
|
103
|
+
- Names missing the noun: `createPending()` → `createPendingInstallation()`. The verb is fine; the object is missing.
|
|
104
|
+
- Generic verbs with no information: `handle`, `process`, `execute`, `doStuff`, `manage`. If the function deletes-then-inserts, name it `replace`, not `update`.
|
|
105
|
+
- Constants that will collide with siblings later: `BUCKET` → `FONTS_BUCKET`, `TIMEOUT` → `HTTP_READ_TIMEOUT_MS`, `URL` → `AUTH_SERVICE_URL`.
|
|
106
|
+
- Repository / port methods named after the rule they enforce, not the data they fetch: `findValid*` / `findActive*` / `findEligible*` — the adjective encodes a business rule the caller can't inspect from the name. Prefer `findDesktopLibrariesForUser(userId)` + a domain predicate applied by the caller.
|
|
107
|
+
|
|
108
|
+
This rule applies most strongly to **names crossing module boundaries**: public methods, exported constants, port methods. Private helpers in a tight local scope get more slack.
|
|
109
|
+
|
|
110
|
+
## Functions do one thing
|
|
111
|
+
|
|
112
|
+
If the name contains "and", or you must scroll to read it, split it.
|
|
113
|
+
|
|
114
|
+
Bad:
|
|
115
|
+
```javascript
|
|
116
|
+
function handleData() {
|
|
117
|
+
fetchData();
|
|
118
|
+
processData();
|
|
119
|
+
displayData();
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Good: each step is its own function with a clear name and signature.
|
|
124
|
+
|
|
125
|
+
### Plain-English summary test
|
|
126
|
+
|
|
127
|
+
For every function you add or meaningfully modify, ask:
|
|
128
|
+
|
|
129
|
+
> Can I summarize what this function does, line by line, as a short English sentence per line — and does the resulting summary read as prose?
|
|
130
|
+
|
|
131
|
+
If the answer is "no, I'd have to group several lines together and invent a name for the group", **that grouping should already exist in the code as a named helper or named intermediate value**. The fact that you had to invent the name is the signal to extract.
|
|
132
|
+
|
|
133
|
+
### Also flag and split
|
|
134
|
+
|
|
135
|
+
- Nested conditionals 3+ levels deep — flatten with early returns or named predicates.
|
|
136
|
+
- Mixed levels of abstraction — a high-level orchestrator suddenly doing string parsing, date arithmetic, or SQL assembly inline.
|
|
137
|
+
- A reader has to track 5+ anonymous temporary values to understand the return.
|
|
138
|
+
- A long `.map().filter().reduce()` chain where each stage does non-obvious work and no stage has a name.
|
|
139
|
+
- Boolean expressions long enough that intent is buried — `if (a && b && !c && (d || e.f > 0) && ...)` without a named predicate.
|
|
140
|
+
- A function that orchestrates 4+ distinct logical phases inline (*fetch → resolve → branch → persist → publish*), each more than a line or two. Even without deep nesting, that's enough work to warrant named sub-methods.
|
|
141
|
+
|
|
142
|
+
### 50-line cap
|
|
143
|
+
|
|
144
|
+
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).
|
|
145
|
+
|
|
146
|
+
Crossing the cap is an extraction signal, not a style nit. Before declaring a function complete, count effective body lines; if over 50, split, or surface the violation and confirm with the user before continuing.
|
|
147
|
+
|
|
148
|
+
### What's fine
|
|
149
|
+
|
|
150
|
+
- Functions that read as a straight sequence of clear, already-named steps (each line is an English sentence).
|
|
151
|
+
- Short functions (< ~10 lines) regardless of shape.
|
|
152
|
+
- Idiomatic framework patterns used clearly (a 30-line controller method obviously doing its job).
|
|
153
|
+
- Guard clauses at the top — they *help* readability, they don't count against you.
|
|
154
|
+
|
|
155
|
+
## Testing discipline
|
|
156
|
+
|
|
157
|
+
These principles apply to every test file regardless of language or framework. The mechanical detection patterns (which mock library, which spy API) are in each `languages/*.md` Tests section.
|
|
158
|
+
|
|
159
|
+
### No self-mocking
|
|
160
|
+
|
|
161
|
+
A unit test for class `Foo` must **not** stub, spy on, or replace methods on the instance of `Foo` being tested. Mocking injected collaborators (other services, repositories, adapters, ports, the clock) is fine and expected.
|
|
162
|
+
|
|
163
|
+
Why it matters: when you mock a method on the subject under test (SUT), the test no longer verifies that method's behavior — it verifies that *some* version of the class (the one you wired up) calls the mocked method. If someone deletes the real implementation, the test still passes. The test has become a proof of its own shape.
|
|
164
|
+
|
|
165
|
+
Bad (pseudo, language-agnostic):
|
|
166
|
+
```
|
|
167
|
+
sut = new Foo(deps)
|
|
168
|
+
mock(sut.calculateTotal).returns(100) // mocking the SUT's own method
|
|
169
|
+
result = sut.checkout()
|
|
170
|
+
assert result.total == 100 // proves wiring, not logic
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Good:
|
|
174
|
+
```
|
|
175
|
+
sut = new Foo(deps) // real Foo, real calculateTotal
|
|
176
|
+
mock(deps.payment.charge).returns(ok) // mock the collaborator at the boundary
|
|
177
|
+
result = sut.checkout()
|
|
178
|
+
assert result.total == expectedTotal // proves the logic
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Behavioral, not implementation tests
|
|
182
|
+
|
|
183
|
+
Assertions should be about **outcomes** — return values, thrown errors, persisted state, published events, calls made to genuine external boundaries. They should **not** be about *how* the work was done internally — which private helper was called, in what order.
|
|
184
|
+
|
|
185
|
+
Smells:
|
|
186
|
+
|
|
187
|
+
- `expect(spy).toHaveBeenCalledWith(...)` as the *only* or *primary* assertion, when the spy is on an internal helper or a pure collaborator with no side effects.
|
|
188
|
+
- Tests that would break if you renamed a private method without changing behavior.
|
|
189
|
+
- Assertions on private methods reached via reflection, cast-to-any, or bracket access — the issue is *reaching into privates*, not the language escape hatch itself.
|
|
190
|
+
- A test file where most assertions are on spies rather than on results.
|
|
191
|
+
|
|
192
|
+
Legitimately behavioral (these are fine):
|
|
193
|
+
|
|
194
|
+
- `expect(mailerSpy).toHaveBeenCalledWith(...)` — sending mail *is* the behavior.
|
|
195
|
+
- `expect(paymentGateway.charge).toHaveBeenCalledWith(...)` — charging a card *is* the behavior.
|
|
196
|
+
- `expect(orderRepo.save).toHaveBeenCalledWith(...)` in an application-service test — producing the right save call *is* the point of the service.
|
|
197
|
+
|
|
198
|
+
The judgment call: is the mocked thing an **outcome boundary** (port, external service, side-effecting adapter) or an **internal helper**? Asserting on boundaries is behavioral; asserting on helpers is implementation-coupled.
|
|
199
|
+
|
|
200
|
+
## No magic numbers
|
|
201
|
+
|
|
202
|
+
Replace hardcoded values with named constants.
|
|
203
|
+
|
|
204
|
+
Bad: `if (age > 18)`
|
|
205
|
+
Good: `const LEGAL_AGE = 18; if (age > LEGAL_AGE)`
|
|
206
|
+
|
|
207
|
+
## Limit nesting depth
|
|
208
|
+
|
|
209
|
+
Flatten with early returns / guard clauses. If you reach four nested blocks, refactor.
|
|
210
|
+
|
|
211
|
+
Bad:
|
|
212
|
+
```javascript
|
|
213
|
+
function process(user) {
|
|
214
|
+
if (user) {
|
|
215
|
+
if (user.isActive) {
|
|
216
|
+
if (user.hasPermission) {
|
|
217
|
+
// ...
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Good:
|
|
225
|
+
```javascript
|
|
226
|
+
function process(user) {
|
|
227
|
+
if (!user) return;
|
|
228
|
+
if (!user.isActive) return;
|
|
229
|
+
if (!user.hasPermission) return;
|
|
230
|
+
// ...
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Comments explain *why*, not *what*
|
|
235
|
+
|
|
236
|
+
The code already says **what**. Comments record the hidden constraint, the bug being worked around, the surprising tradeoff. Never narrate the next line.
|
|
237
|
+
|
|
238
|
+
Bad:
|
|
239
|
+
```javascript
|
|
240
|
+
// increment i
|
|
241
|
+
i++;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Good:
|
|
245
|
+
```javascript
|
|
246
|
+
// User may be soft-deleted; invalidate cache so stale reads vanish.
|
|
247
|
+
cache.invalidate(user.id);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Default to writing **no comment**. Only add one when removing it would confuse a future reader.
|
|
251
|
+
|
|
252
|
+
## Boy Scout Rule
|
|
253
|
+
|
|
254
|
+
Leave the file cleaner than you found it. Fix one small inconsistency on the way through, do not embark on a separate refactor.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Java Conventions
|
|
2
|
+
|
|
3
|
+
## Style guide
|
|
4
|
+
|
|
5
|
+
Google Java Style Guide is the default. If the project ships its own (`CONTRIBUTING.md`, `.editorconfig`, `checkstyle.xml`, `google-java-format` config), follow that instead.
|
|
6
|
+
|
|
7
|
+
## Naming
|
|
8
|
+
|
|
9
|
+
| Element | Convention | Example |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Class / interface / enum | UpperCamelCase | `ImmutableList`, `HashIntegrationTest` |
|
|
12
|
+
| Method / variable / parameter | lowerCamelCase | `sendMessage`, `computedValues` |
|
|
13
|
+
| Constant (`static final` + immutable value) | UPPER_SNAKE_CASE | `MAX_COUNT`, `DEFAULT_TIMEOUT` |
|
|
14
|
+
| Package | lowercase, no underscores | `com.example.deepspace` |
|
|
15
|
+
| Type variable | single letter or short PascalCase | `T`, `E`, `Result` |
|
|
16
|
+
| Test class | `<ClassUnderTest>Test` / `<ClassUnderTest>IT` | `UserServiceTest`, `OrderRepositoryIT` |
|
|
17
|
+
|
|
18
|
+
## Formatting
|
|
19
|
+
|
|
20
|
+
- Indent: **2 spaces**, never tabs.
|
|
21
|
+
- Column limit: **100** characters.
|
|
22
|
+
- Brace style: K&R — opening brace on the same line.
|
|
23
|
+
- One statement per line.
|
|
24
|
+
- Always brace `if`, `else`, `for`, `while`, `do`, even for single-statement bodies.
|
|
25
|
+
- `switch` statements must be exhaustive — supply a `default` case, or rely on enum/sealed-type coverage.
|
|
26
|
+
|
|
27
|
+
## Required practices
|
|
28
|
+
|
|
29
|
+
- `@Override` on every override (including interface methods).
|
|
30
|
+
- Never silently swallow exceptions. If you intentionally ignore one, leave a one-line comment stating why.
|
|
31
|
+
- Access static members via the class name (`Foo.bar()`, not `instance.bar()`).
|
|
32
|
+
- Do not use finalizers. Use `try-with-resources` for `AutoCloseable` and `java.lang.ref.Cleaner` for native handles.
|
|
33
|
+
- Declare local variables near first use, with the narrowest possible scope.
|
|
34
|
+
- Prefer `List.of(...)` / `Map.of(...)` / `Set.of(...)` for small immutable collections.
|
|
35
|
+
- Prefer `Optional<T>` as a return type for "might be missing"; never use `Optional` for fields, parameters, or collection elements.
|
|
36
|
+
- Use `record` for plain data carriers (Java 16+).
|
|
37
|
+
- Use `var` only when the right-hand side makes the type obvious to a reader.
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
- Framework: **JUnit 5** unless the project uses TestNG or JUnit 4.
|
|
42
|
+
- File suffix: `*Test.java` for unit, `*IT.java` for integration.
|
|
43
|
+
- Method naming: `methodUnderTest_condition_expectedResult` (e.g. `parse_emptyInput_throws`).
|
|
44
|
+
- Use **AssertJ** or JUnit's `assertThat` for fluent assertions; avoid bare `assertTrue(a == b)`.
|
|
45
|
+
- One logical assertion per test. Multiple `assertThat` lines on the *same state* are fine.
|
|
46
|
+
- No `Thread.sleep` in tests — use **Awaitility** for async.
|
|
47
|
+
- Mock at process boundaries only (HTTP, DB, time, randomness). Do not mock value objects or DTOs.
|
|
48
|
+
- Use `@Nested` to group related test cases.
|
|
49
|
+
- Use parameterized tests (`@ParameterizedTest` + `@CsvSource` / `@MethodSource`) for table-driven coverage.
|
|
50
|
+
|
|
51
|
+
### Self-mock signals to refuse (rule from `clean-code.md` → Testing discipline)
|
|
52
|
+
|
|
53
|
+
- A field annotated with **both** `@InjectMocks` *and* `@Spy` — the SUT is being wrapped as a spy so its own methods can be stubbed. Drop `@Spy`; stub only the `@Mock` collaborators on the same class.
|
|
54
|
+
- `Mockito.spy(realSut)` followed by `doReturn(...).when(spy).someMethod(...)` — same anti-pattern, manually constructed.
|
|
55
|
+
- `MockedStatic<SomeUtil>` when `SomeUtil` is the unit under test rather than a dependency.
|
|
56
|
+
- `ReflectionTestUtils.setField(sut, "privateState", ...)` to drive a branch, or `ReflectionTestUtils.invokeMethod(sut, "privateHelper")` to assert on a private helper — that's reaching into privates and breaks behavioral-testing discipline.
|
|
57
|
+
|
|
58
|
+
What's fine: `@Mock` on collaborators injected into `@InjectMocks`, `verify(collaborator).methodCall(...)` on outcome boundaries (repository save, mailer send, gateway charge).
|
|
59
|
+
|
|
60
|
+
## Formatter / linter to run
|
|
61
|
+
|
|
62
|
+
- `google-java-format` (preferred) or the `spotless` Gradle/Maven plugin.
|
|
63
|
+
- `checkstyle`, `spotbugs`, `error-prone` if configured by the project.
|
|
64
|
+
- Run before commit: `./gradlew spotlessApply check` (or the project's equivalent).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# JavaScript / TypeScript Conventions
|
|
2
|
+
|
|
3
|
+
## Style guide
|
|
4
|
+
|
|
5
|
+
Airbnb JavaScript Style Guide is the common baseline. The project's `.eslintrc(.json|.cjs|.mjs)`, `.prettierrc`, `tsconfig.json`, and `package.json` `engines` override these defaults — read them **first**.
|
|
6
|
+
|
|
7
|
+
## Naming
|
|
8
|
+
|
|
9
|
+
| Element | Convention | Example |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Variable / function | camelCase | `userCount`, `fetchProfile` |
|
|
12
|
+
| Class / type / interface / enum | PascalCase | `UserProfile`, `HttpClient`, `Status` |
|
|
13
|
+
| Constant (true immutable, module-level) | UPPER_SNAKE_CASE | `MAX_RETRIES` |
|
|
14
|
+
| Boolean variable | `is` / `has` / `should` / `can` prefix | `isLoading`, `hasPermission`, `shouldRetry` |
|
|
15
|
+
| React component | PascalCase | `UserCard` |
|
|
16
|
+
| Hook | `use` prefix | `useAuth` |
|
|
17
|
+
| File: React component | matches component name | `UserCard.tsx` |
|
|
18
|
+
| File: everything else | kebab-case | `format-date.ts` |
|
|
19
|
+
|
|
20
|
+
## Formatting
|
|
21
|
+
|
|
22
|
+
- Indent: **2 spaces**.
|
|
23
|
+
- Semicolons: pick one project-wide and never mix.
|
|
24
|
+
- Single quotes for strings, backticks for templates and any string needing interpolation.
|
|
25
|
+
- Trailing commas in multi-line literals / params.
|
|
26
|
+
- Prefer arrow functions for callbacks and small utilities; reserve `function` for top-level named declarations and class methods.
|
|
27
|
+
- Destructure when accessing 2 or more properties: `const { id, name } = user`.
|
|
28
|
+
|
|
29
|
+
## TypeScript-specific (mandatory)
|
|
30
|
+
|
|
31
|
+
- `tsconfig.json`: `"strict": true`. No exceptions. `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` are recommended.
|
|
32
|
+
- **Never** use `any`. If a type is truly unknown, use `unknown` and narrow.
|
|
33
|
+
- Let inference do its job for locals. Declare explicit types at:
|
|
34
|
+
- Exported function signatures (parameters **and** return type).
|
|
35
|
+
- Public class members.
|
|
36
|
+
- Empty literals where inference would default to `never` or `{}` — `const items: User[] = []`.
|
|
37
|
+
- Use `readonly` for arrays, properties, and parameters that must not mutate.
|
|
38
|
+
- Discriminated unions over loose objects with optional flags. Use a literal `kind` / `type` field.
|
|
39
|
+
- Type guards (`function isX(v): v is X`) over casts (`as X`). Casts are a last resort.
|
|
40
|
+
- `interface` for object shapes that may be extended; `type` for unions / mapped / conditional types.
|
|
41
|
+
- Generics: meaningful names when single letters are unclear (`TUser`, `TResult`).
|
|
42
|
+
- Never disable rules inline (`// eslint-disable`, `// @ts-ignore`, `// @ts-expect-error`) without a one-line comment explaining why.
|
|
43
|
+
|
|
44
|
+
## Required practices
|
|
45
|
+
|
|
46
|
+
- `const` by default, `let` only when reassigned. `var` is **forbidden**.
|
|
47
|
+
- Use `===` / `!==`, never `==` / `!=`.
|
|
48
|
+
- Use optional chaining `?.` and nullish coalescing `??` instead of long `&&` / `||` ladders.
|
|
49
|
+
- `for...of` over indexed `for` loops on iterables. `map` / `filter` / `reduce` over manual loops when transforming.
|
|
50
|
+
- `async` / `await` over raw `.then` chains. Mixing is forbidden in one function.
|
|
51
|
+
- Throw `Error` instances, never strings. Subclass `Error` for typed domain errors.
|
|
52
|
+
- Avoid `export default` unless the file has exactly one obvious export — named exports are easier to refactor and rename.
|
|
53
|
+
- Do not mutate function parameters or imported modules.
|
|
54
|
+
|
|
55
|
+
## Tests
|
|
56
|
+
|
|
57
|
+
- Framework: **Vitest**, **Jest**, or Node's built-in `node:test`. Match what the project uses.
|
|
58
|
+
- File suffix: `*.test.ts` or `*.spec.ts`, colocated with source (`src/foo/bar.ts` → `src/foo/bar.test.ts`).
|
|
59
|
+
- Structure: `describe(unit, () => { it('does X when Y', () => { ... }) })`.
|
|
60
|
+
- **One behaviour per `it` block.**
|
|
61
|
+
- Async tests: `await` every promise. Returning a promise without `await` hides assertion failures.
|
|
62
|
+
- Mocking: prefer dependency injection over module mocks. When you must mock, use `vi.mock` / `jest.mock` and reset between tests (`beforeEach(() => vi.clearAllMocks())`).
|
|
63
|
+
- React: **React Testing Library**, not Enzyme. Query by role / label / text, not by class or test-id, unless nothing else works.
|
|
64
|
+
- Snapshot tests only for stable, intentional output. Snapshots that "just changed" are noise.
|
|
65
|
+
|
|
66
|
+
### Self-mock signals to refuse (rule from `clean-code.md` → Testing discipline)
|
|
67
|
+
|
|
68
|
+
- `jest.spyOn(sut, 'someMethod').mockReturnValue(...)` / `.mockResolvedValue(...)` / `.mockImplementation(...)` where `sut` is the subject under test.
|
|
69
|
+
- `jest.spyOn(FooService.prototype, 'someMethod').mockReturnValue(...)` in `foo.service.spec.ts`.
|
|
70
|
+
- `sut.calculateTotal = jest.fn(...)` — manually overwriting a method on the SUT instance.
|
|
71
|
+
- `vi.mocked(sut)` followed by stubbing the SUT's own methods.
|
|
72
|
+
- Reaching into privates via `(sut as any).privateMethod()` or `sut['privateMethod']()` — the issue is the *private access*, not the cast itself.
|
|
73
|
+
|
|
74
|
+
`any` / `as any` in `*.spec.ts` / `*.test.ts` is otherwise fine — test setup justifies looser typing. Only flag it when it's the vehicle for one of the violations above.
|
|
75
|
+
|
|
76
|
+
What's fine: `jest.fn()` for collaborator stubs passed via constructor DI, `vi.mock('../mailer')` to fake a side-effecting module at the boundary, `expect(mailer.send).toHaveBeenCalledWith(...)` on an outcome boundary.
|
|
77
|
+
|
|
78
|
+
## Formatter / linter to run
|
|
79
|
+
|
|
80
|
+
- `prettier --write .` (formatting).
|
|
81
|
+
- `eslint --fix .` (style + correctness).
|
|
82
|
+
- `tsc --noEmit` (type check).
|
|
83
|
+
- All three should pass before commit. CI should fail on warnings, not just errors.
|
|
84
|
+
|
|
85
|
+
## Forbidden and Prohibited
|
|
86
|
+
|
|
87
|
+
- never use any type assertions (`as` or ``)
|