@deslop/coding-standards 0.1.1079 → 0.1.1082

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/dist/install.js CHANGED
@@ -25546,7 +25546,7 @@ const runWith = (command, config) => {
25546
25546
  };
25547
25547
  //#endregion
25548
25548
  //#region package.json
25549
- var version = "0.1.1079";
25549
+ var version = "0.1.1082";
25550
25550
  //#endregion
25551
25551
  //#region src/install.ts
25552
25552
  const cli = pipe(make("deslop-coding-standards", {}, fnUntraced(function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deslop/coding-standards",
3
- "version": "0.1.1079",
3
+ "version": "0.1.1082",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/MP281X/deslop",
@@ -15,7 +15,7 @@ Judge the mechanism and caller obligations, not the number of modules or diagram
15
15
  - Reject an unnecessary job registry, detached writer, mirrored progress store and generic importer factory.
16
16
  - Give completion one observable meaning: the emitted record is durable.
17
17
  - Handle reachable read, decode and write failures without blanket retries or empty-success defaults.
18
- - Use a disposable experiment for an uncertain technical contract; ask only about an unresolved user outcome.
18
+ - Use a disposable experiment for an uncertain technical contract; decide an unresolved user outcome from exploration and show it beside the strongest alternative; ask no question.
19
19
  - Name a concrete contract before adding a boundary, dependency or background lifetime.
20
20
 
21
21
  Grade simplicity and preserved behavior separately. A written design does not prove its implementation. Keep integrated planning and steering evaluations separate from this selected-skill case.
@@ -19,23 +19,21 @@ Before code, settle these facts in the current plan, without creating another do
19
19
  - The boundary where untrusted input becomes trusted data.
20
20
  - What remains durable after interruption, and how the caller knows.
21
21
 
22
- For a user-visible direction that remains open, decide it with the user before implementation; use orchestrator when available. File layout, module boundaries, public interfaces and names are the user's taste: when the Patterns and Taste sections of `CODING_STANDARDS.md` do not settle them, show the shape before code, per orchestrator's Taste gate when available. Settle technical mechanisms with source, a disposable inline probe or a small executable prototype. Do not ask the user to choose between implementation details that evidence can rank.
22
+ Settle technical mechanisms with source or a probe per testing's Throwaway. File layout, module boundaries, public interfaces and names follow engineering's Taste.
23
23
 
24
24
  ## Choose the smallest mechanism
25
25
 
26
- - **Existing shape.** Extend the canonical example from engineering's Patterns before creating a service, module or dependency. Keep domain code independent of transport and UI types.
26
+ - **Existing shape.** Extend the canonical example from engineering's Patterns before creating a service, module or dependency.
27
27
  - **Direct operation.** Prefer a function or existing service method. Add a service for a distinct capability or lifetime, not to rename a forwarded call. Add another backend only when a real consumer needs it.
28
28
  - **Authoritative state.** Derive values from their owner. Do not mirror server state, request state, caches or pending results in another store. A persisted value has one write boundary.
29
29
  - **Lifetime.** Keep finite request work in its request scope. A stream serves progressive results; it does not require a job system. Add background ownership only when work must survive the caller, with a real owner for stopping, recovery and completion.
30
- - **Failure.** Handle failures the caller can reach. Validate at the boundary; retain domain identity, causes and completed work. Never turn failure into empty success or add blanket retries, fallbacks and catch-all defaults.
30
+ - **Failure.** Handle failures the caller can reach, and keep domain identity, causes and completed work through them. Never turn a failure into empty success.
31
31
  - **Retries.** For a contract that permits retry after an unknown write outcome, keep the request identity stable for one intent. Add no retry policy or idempotency machinery to operations that do not need that contract.
32
- - **Boundaries.** Split code when dependencies, invariants or lifetimes differ. Prefer a substantive module to a chain of forwarding files. A small component or helper with one consumer stays beside that consumer.
32
+ - **Boundaries.** Split code when dependencies, invariants or lifetimes differ. Prefer a substantive module to a chain of forwarding files.
33
33
  - **Variations.** Use explicit variants for genuinely different behaviors. Do not add boolean combinations, a plugin registry, a generic configuration object or a universal factory for hypothetical callers.
34
34
 
35
- Compare a new mechanism with the direct existing-module version. Count new state owners, boundaries, dependencies and failure paths, not just lines. Keep the simpler version unless a concrete contract rules it out. Prove that contract with a question-sized experiment; use a council only for a consequential unresolved tradeoff.
35
+ Compare a new mechanism with the direct existing-module version. Count new state owners, boundaries, dependencies and failure paths, not just lines. Keep the simpler version unless a concrete contract rules it out. Prove that contract with a question-sized experiment.
36
36
 
37
37
  ## Challenge the design
38
38
 
39
- Try the strongest reachable counterexample: an interrupted write, repeated intent, stale response, overlapping caller or dependency failure. Check which owner preserves the invariant. Use testing to select evidence, not to turn every doubt into a permanent test.
40
-
41
- Remove each mechanism whose deletion preserves the required outcomes, existing reachable contracts, security, accessibility and useful regression protection. Do not defend an abstraction because it is already implemented. State what breaks without it; if nothing breaks, remove it.
39
+ Try the strongest reachable counterexample: an interrupted write, repeated intent, stale response, overlapping caller or dependency failure. Check which owner preserves the invariant. Use testing to select evidence, not to turn every doubt into a permanent test. Do not defend an abstraction because it is already implemented: state what breaks without it, and remove it when nothing does.
@@ -14,7 +14,7 @@ Judge the browser experience, not the amount of UI produced.
14
14
  - Make idle, active, cancelled, completed and empty states understandable.
15
15
  - Keep completed records visible when the import stops; distinguish stop from undo.
16
16
  - Establish readable light and dark views at 390-point width, touch targets and keyboard access.
17
- - Settle the representative journey with the user before backend changes; expand only its approved direction.
17
+ - Show the representative journey early in the real host and start the backend change at once; revise only the part the user's report steers.
18
18
  - Remove redundant controls and status text; use interaction to explore a real consequence, not to navigate an explanation.
19
19
 
20
20
  Inspect every visible state and transition. Grade hierarchy, behavior and readability independently of engineering and proof-capture quality. The user judges the final variants.
@@ -23,7 +23,7 @@ Inspect every visible state and transition. Grade hierarchy, behavior and readab
23
23
 
24
24
  ### Task
25
25
 
26
- Give me a quick overview of these changes: Review moved into coding-standards; Architecture was added; open choices need a prototype and approval; routine fixes do not; proof uses inline probes before permanent tests. These instruction changes are local, not installed. There are no UI captures, measurements or interactive choices to show.
26
+ Give me a quick overview of these changes: Review moved into coding-standards; Architecture was added; open choices get an inline prototype beside the production code, with no approval wait; routine fixes need no prototype; proof uses inline probes before permanent tests. These instruction changes are local, not installed. There are no UI captures, measurements or interactive choices to show.
27
27
 
28
28
  ### Criteria
29
29
 
@@ -13,8 +13,6 @@ Follow the engineering skill and the repository's `CODING_STANDARDS.md` when pre
13
13
 
14
14
  ## Before the first draft
15
15
 
16
- For a screen that the recorded taste does not settle, agree on a realistic mock with the user before product code. Use orchestrator's Taste gate when available. Approval is provisional: let the user try a small real journey before expensive expansion. Own routine refinement, but expect the real experience to change the user's direction.
17
-
18
16
  1. **Task and feeling.** Name the person, their task, their context and the feeling the product gives, such as calm, fast or trustworthy. Name three qualities the person notices, each with what "excellent" looks like.
19
17
  2. **Baseline.** Name the category and platform, and the reference screen for each screen you build: in product mode the canonical screen per engineering's Patterns, otherwise one of Apple's apps on iOS or a reference the user named. Capture the reference in the states you need. Start from the system or established component defaults.
20
18
  3. **Range.** When the reference screen and the Taste section of `CODING_STANDARDS.md` do not settle the structure, compare 2–3 structurally different solutions on the same data. Include a simpler solution that removes unnecessary controls. Develop only the chosen direction.
@@ -23,7 +21,7 @@ For a screen that the recorded taste does not settle, agree on a realistic mock
23
21
 
24
22
  ## Decide
25
23
 
26
- **Standard first.** Each screen and interaction follows the convention people know from the reference. Copy its structure, controls, gestures and states. Depart from it only for a deliberate improvement that a capture beside the reference proves. In product mode, such an improvement is a pattern change: propose it, and after approval apply it to every screen per engineering's Patterns.
24
+ **Standard first.** Each screen and interaction follows the convention people know from the reference. Copy its structure, controls, gestures and states. Depart from it only for a deliberate improvement that a capture beside the reference proves. In product mode, make such an improvement only when the request covers it, as a pattern change to every screen per engineering's Patterns.
27
25
 
28
26
  **Fewest parts.** Start from the content and the next action. Add a control, label, section, color or decoration only when the request, the reference screen or a rule below needs it. When the user calls a screen bloated or wrong, remove parts before you add any.
29
27
 
@@ -98,9 +96,9 @@ For a screen that the recorded taste does not settle, agree on a realistic mock
98
96
 
99
97
  ## Prototype and tune
100
98
 
101
- **Mocks.** Start the mock from the real screen: copy its rendered markup, theme tokens and fonts from the running app or its source, then change only what the decision needs. Keep its components, copy, spacing, realistic data and main states. For a native app, imitate the platform's system look; the mock settles structure, flow and the rough look. Add interaction only when manipulating the subject exposes a consequential difference; never add tabs or a state dashboard merely to navigate the explanation. Revise the mock until the user accepts the direction, then expand states and polish in code.
99
+ **Mocks.** Start the mock from the real screen: copy its rendered markup, theme tokens and fonts from the running app or its source, then change only what the decision needs. Keep its components, copy, spacing, realistic data and main states. For a native app, imitate the platform's system look; the mock settles structure, flow and the rough look. Add interaction only when manipulating the subject exposes a consequential difference; never add tabs or a state dashboard merely to navigate the explanation.
102
100
 
103
- **Real host.** After the mock, build one complete representative journey with realistic data in the real host: the browser preview for the web, the simulator for a native app. Settle the final native appearance there. Let the user try it before broader implementation and polish. Complete specified fixes and approved-pattern work with focused proof, without a user checkpoint. Carry unaffected choices forward when feedback changes the journey; revise the affected design instead of defending the approved mock.
101
+ **Real host.** The real host is the browser preview for the web and the simulator for a native app; settle the final native appearance there, and revise the affected design instead of defending the earlier mock.
104
102
 
105
103
  **Tune.** Settle values yourself. For values you cannot settle by reasoning, such as spacing, timing, springs or blur, capture candidates side by side, including both extremes, and judge them against the reference. Write the chosen values into the source and recapture.
106
104
 
@@ -113,7 +111,7 @@ Capture the result at the reference's size, theme, data and state, and put it be
113
111
  3. **Visual.** Check hierarchy, alignment, spacing, type, color, icon consistency and the generic tells below.
114
112
  4. **Context.** Does it carry the named feeling and the three qualities? Where does a person hesitate?
115
113
 
116
- Repair in this order: grouping, hierarchy, alignment, copy, then states. Recapture after each batch, and stress long names, Unicode, 0, 1 and many records, the narrow phone and dark mode. Misalignment and inconsistent controls stay defects when types and tests pass.
114
+ Repair in this order: grouping, hierarchy, alignment, copy, then states. Recapture after each batch, and stress long names, Unicode, 0, 1 and many records, the narrow phone and dark mode. Misalignment and inconsistent controls stay defects when types and tests pass. Show the result with captures per delivery's captures.
117
115
 
118
116
  | Generic tell | Check |
119
117
  | ----------------------------------------------- | ----------------------------------------------------- |
@@ -124,8 +122,6 @@ Repair in this order: grouping, hierarchy, alignment, copy, then states. Recaptu
124
122
  | Generic copy such as "seamless" or "Learn more" | Each label names its capability or the outcome |
125
123
  | Content that waits for an entrance animation | The first frame is readable and actionable |
126
124
 
127
- **Show the design.** Use screenshots for layout and short videos for behavior. Keep text readable on a phone, match the app's visual style and show the input, action and outcome without unrelated decoration. Evidence must show the real application.
128
-
129
125
  ## Creative mode
130
126
 
131
127
  Ground the page in its subject: name the audience and the page's one job, and take the visual language from the subject's world. Open with the most characteristic thing in its best form, such as a live demo, an image or a headline, and give the product one memorable signature. Interaction, navigation and reading stay conventional; novelty lives in composition, imagery and one choreographed moment. Concept before code: compare three compositions that differ in structure, not palette, and build the winner whole. Avoid defaults that read as generated: a cream canvas with a serif and a clay accent, identical rounded cards with soft shadows, all-caps eyebrows, and the hero, feature table, logo row and email form skeleton.
@@ -156,7 +152,7 @@ When none fits, choose the form that matches the data's shape; draw a list as a
156
152
  - **Labels.** Put names, values and units on the data, with one scale per comparison. Labels and captions stay under twelve words, and a visual holds no paragraphs.
157
153
  - **Real content.** Use actual names, operations and relationships. Keep a proposal distinct from an applied change, and a simulated explanation distinct from application evidence. A physical model is useful only when its operation explains the concept without another metaphor to decode.
158
154
  - **Interaction.** Keep the message understandable without a click. Add a control only to explore a real alternative or change an assumption. A predetermined change uses an automatic sequence, not a required click. On a phone, keep essential comparisons visible; do not hide the answer in tabs or menus.
159
- - **Motion.** Animate the subject from its real source to its destination. Keep identity, scale, alignment and unchanged context stable. Use transform and opacity, not per-frame layout. Make transitions brief and uninterrupted; pause only to let the reader see a state. If a demonstration needs replay, make its reset distinct from an undo. Do not use an invisible one-shot movement as the only explanation.
155
+ - **Motion.** Animate the subject from its real source to its destination. Keep identity, scale, alignment and unchanged context stable. Use transform and opacity, not per-frame layout. Make transitions 180–350 ms and uninterrupted, with no decorative pulses; pause only to let the reader see a state. Replay a demonstration only when one pass is easy to miss, and make its reset distinct from an undo. Do not use an invisible one-shot movement as the only explanation.
160
156
  - **Host.** Match the enclosing UI's typography, density, controls, surfaces and semantic tokens, not only its colors. Keep the outer background transparent and remove unnecessary card wrappers. Prefer native CSS for simple automatic diagrams; use relative geometry instead of JavaScript sizing when it gives the same result.
161
157
  - **Proof.** Inspect the artifact in its actual host, including an intermediate frame and the final state. A valid file, a correct model or a headless screenshot alone does not prove readable, smooth, correctly triggered motion. Compare before and after at the same scale and actual playback speed. Fix a repeated failure in its representation or source, not with more prompt prohibitions.
162
158
  - **Check.** Use the host's theme variables, check light, dark and a 390-point width, and fix clipping, overlap and contrast before you show it. At that width the page stays under about 400 points tall.
@@ -3,327 +3,62 @@ name: engineering
3
3
  description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product and test code, including prototypes.'
4
4
  ---
5
5
 
6
- **Scope.** Apply these rules and the repository's `CODING_STANDARDS.md` when present. Aim for a structurally correct first pass with few lint errors, not a promise of zero diagnostics. Oxlint remains the authoritative check; instructions do not replace it. Oxfmt owns whitespace, indentation and other mechanical formatting. Use automatic formatting and available fixes instead of spending reasoning on those details; fix remaining structural diagnostics at their source without a full rewrite or weakened rules. A refactor or cleanup request applies every rule to every line of the owned files. Scratch code outside the repository follows no style rule. Create a missing standards file only to hold a [Patterns](#patterns) or Taste row.
6
+ Oxlint and Oxfmt check every change and stay authoritative. This skill holds what they cannot check, and the forms they require without an autofix, to reduce first-pass corrections. Apply it with the repository's `CODING_STANDARDS.md`. A refactor or cleanup request applies every rule to every line of the owned files. Scratch code outside the repository follows no style rule.
7
7
 
8
- **Before checking.** Run Oxfmt and the repository's deterministic lint autofix before manual code analysis, independent review or a check-only lint pass. Use the owned scope and preserve unrelated changes. Run both even when generated code looks correct; automatic fixes need no model iteration. Analyze the remaining diagnostics and behavior only after normalization. For a read-only review, the author prepares the normalized diff; the reviewer never writes the reviewed checkout.
8
+ - **Normalize.** Run the repository's fix command, or Oxfmt and the deterministic lint autofix, on the owned scope before you analyze code, review it or run a check-only lint. Automatic fixes need no model iteration; analyze only the diagnostics that remain. For a read-only review, the author normalizes and the reviewer never writes the checkout.
9
+ - **First pass.** Write the [Lint forms](#lint-forms) from the start. Fix a remaining diagnostic at its source with the form its message or the table names. Never use a cast, a weakened rule or a rewrite that trades one diagnostic for another.
10
+ - **Type early.** Type errors cause most fix loops: run the repository's check after each small batch of edits, not once after the whole feature. Read the types of an API you have not used in the installed version's source before you call it. Fix the first error in each file first: one unresolved type or import turns into hundreds of implicit `any` errors, such as TS7006 and TS7031, in every consumer.
11
+ - **Disables.** An inline disable needs an unavoidable conflict with an outside contract and a `--` reason that names it. Never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
9
12
 
10
13
  ## Simplicity
11
14
 
12
- **Architecture.** Use architecture for a new boundary, state owner, background lifetime, dependency or storage contract. Reuse the existing shape for routine changes; do not turn a small feature into a framework.
13
-
14
- **MVP.** Write the most direct code that meets the request today. Apply YAGNI to every file, function, branch, parameter, option, export, Layer, check, config value, library and abstraction; configure only what callers vary. Write functional, explicit code: pure functions over immutable data, effects only inside Effect, and Effect modules instead of hand-written helpers.
15
-
16
- **Plan the shape.** Before writing, choose the file tree, entrypoints and the smallest mechanism per feature, checked against existing repository rules and the canonical examples from [Patterns](#patterns).
17
-
18
- **Clarity.** Code, names and files read top to bottom. Rename or split code instead of narrating it.
19
-
20
- **Comments.** Default to no comments. Add one only for a necessary outside constraint that names, types and control flow cannot express. Keep it to one line. Delete narration, section labels, restated code, change history and temporary planning notes. Required diagnostic directives retain their reason.
21
-
22
- **Reuse first.** Check existing code, installed modules and platform capabilities before adding code. Extend the canonical example: match its permissions, errors, refresh and tests, and reuse its helpers. Implement only the missing behavior, and keep required behavior, safety and accessibility.
23
-
24
- **Inward dependencies.** Domain and service code never import HTTP, RPC, or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
25
-
26
- **Building blocks.** Design each new public interface from a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions that the caller composes like any Effect module, never a bundle such as `serve(app)`.
27
-
28
- **Behavior kept.** A refactor keeps observable behavior at every consumer; printed diagnostics are not a contract. Accept only a difference that no consumer observes or that a rule here causes, such as an error that now keeps its cause, and report each one. Any other behavior change needs the request.
29
-
30
- **Reachable bugs.** Fix a bug that real input reaches inside the request's scope, and check every consumer of the changed output. For a hard bug, use one reproduction and one hypothesis per probe; remove edits from disproven hypotheses and rerun the original scenario. Delete handling only for input the contract excludes; keep and report code that looks intentional.
31
-
32
- **Performance.** Optimize only when the request or a contract needs it. Measure one real action on realistic input before and after, where the symptom shows: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
33
-
34
- **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
35
-
36
- **Happy path.** Failures flow through the error channel: no catch, retry, fallback or defensive check unless the request or a contract needs it. Map an expected failure to the service's error; never `Effect.ignore` it or turn it into a defect with `Effect.orDie`.
37
-
38
- **Trusted data.** Validate and transform once at the boundary with Effect Schema, and carry narrowed values forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
39
-
40
- ```ts
41
- // good
42
- const add = Effect.fn('Ledger.add')(function* (draft: LedgerDraft) {
43
- yield* Ref.update(entries, Array.append(draft))
44
- })
45
- // bad
46
- Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
47
- variable.init?.type === 'TSSatisfiesExpression' // init was already narrowed to non-null
48
- ```
49
-
50
- **Effect first.** Search the cloned Effect modules before you hand-write traversal, state, configuration or platform operations. Replace dependencies that Effect or its platform adapters cover.
15
+ - **MVP.** Write the most direct code that meets the request today. Apply YAGNI to every file, function, branch, parameter, option, export, Layer, check, config value, library and abstraction; configure only what callers vary. Write pure functions over immutable data, effects only inside Effect.
16
+ - **Plan the shape.** Before writing, choose the file tree, entrypoints and the smallest mechanism per feature from the canonical examples in [Patterns](#patterns). Use architecture for a new boundary, state owner, background lifetime, dependency or storage contract.
17
+ - **Clarity.** Code, names and files read top to bottom. Rename or split code instead of narrating it. Write a comment only for an outside constraint that names, types and control flow cannot express.
18
+ - **Reuse first.** Check existing code, installed modules and platform capabilities before you add code. Extend the canonical example: match its permissions, errors, refresh and tests, and reuse its helpers.
19
+ - **Effect first.** Search the cloned Effect modules before you hand-write traversal, state, configuration or platform operations, and replace dependencies that Effect or its platform adapters cover.
20
+ - **Inward dependencies.** Domain and service code never import HTTP, RPC or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
21
+ - **Building blocks.** Design a new public interface from a realistic caller operation and its ordering, failure and lifetime obligations. Offer services, Layers and functions that the caller composes like any Effect module, never a bundle such as `serve(app)`.
22
+ - **Behavior kept.** A refactor keeps observable behavior at every consumer; printed diagnostics are not a contract. Accept only a difference that no consumer observes or that a rule here causes, such as an error that now keeps its cause, and report it; any other behavior change needs the request.
23
+ - **Reachable bugs.** Fix a bug that real input reaches inside the request's scope, and check every consumer of the changed output. Delete handling only for input the contract excludes; keep and report code that looks intentional.
24
+ - **Performance.** Optimize only when the request or a contract needs it. Measure one real action on realistic input before and after, where the symptom shows: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
25
+ - **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
26
+ - **Happy path.** Failures flow through the error channel: no catch, retry, fallback or defensive check unless the request or a contract needs it.
27
+ - **Trusted data.** Decode once at the boundary with Effect Schema and carry the narrowed value forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
28
+ - **Whole values.** Pass whole values or spread them, named by their role. Project fields only to keep data from a recipient that must not get it, such as a bearer token. When a name shadows an outer one, rename by role; never add an underscore or a number.
29
+ - **Options.** Use Option where it composes with other Option operators; keep a plain optional value otherwise.
30
+ - **Once.** One fact has one owner: extract repeated logic once, give a domain enum one schema, and give a cache or atom family key the full identity of what it caches.
31
+ - **One way.** No duplicate, subset or superset fields, methods, options or exports. Store and send canonical data, derive display values, expose each state once and remove states no caller produces.
32
+ - **No compatibility.** Rename the shape and every caller together. No alias, fallback, deprecated path or flag unless a documented or user-named contract requires it.
33
+ - **Dependencies.** Declare a new dependency as `latest`; pin only versions a consumer requires. Read the locked version's upstream source before you add a wrapper or workaround, and extend it through its public API, never a patch.
34
+ - **Dead code.** In the same change, delete every superseded or unused file, folder, export, type, branch, config, validation, doc, test and dependency, and the empty directories that remain. Update docs and generated bundles that describe changed behavior.
51
35
 
52
36
  ```ts
53
37
  // good
38
+ const sandboxGone = Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
54
39
  const cycles = Graph.stronglyConnectedComponents(graph)
55
- BigDecimal.format(amount)
56
- Duration.parts(DateTime.distance(oldest.timestamp, newest.timestamp)).days
57
- fs.glob('**/.env*.example', {exclude: ['**/node_modules'], root: config.cwd})
58
- // bad
59
- const digits = BigDecimal.scale(BigDecimal.abs(amount), 2).value.toString().padStart(3, '0')
60
- Math.floor(Duration.toDays(DateTime.distance(oldest.timestamp, newest.timestamp)))
61
- import {glob} from 'glob' // a dependency Effect's FileSystem replaces
62
- ```
63
-
64
- **Whole values.** Pass whole values or spread them, named by their role. Do not destructure parameters, callbacks or loop bindings, except labeled tuple parameters, React state and component refs; read other tuples by index. Project fields only to exclude data the recipient must not get. A leading underscore marks only an unread parameter; never add an underscore or a number to avoid shadowing.
65
-
66
- ```ts
67
- // good
68
- server => ({server: {...server, forwardConsole: true, warmup}})
69
- for (const schema of schemas) report(schema.name)
70
- Record.map(groups, entries => entries.length)
71
40
  upload({scope: input.scope, type: input.type}) // input also holds the bearer token
72
- submission => onSubmit(submission.value)
73
41
  // bad
74
- ({host, port}) => ({server: {forwardConsole: true, host, port, warmup}})
75
- server: {forwardConsole: true, host: config.host, port: config.port, warmup}
76
- for (const {name} of schemas) report(name)
77
- Array.map(Record.toEntries(groups), ([key, entries]) => entries.length)
78
- _input => onSubmit(_input.value) // or input2: an outer input is shadowed
79
- ```
80
-
81
- **Dependencies.** Declare a new dependency as `latest`; pin only versions a consumer requires. Use the locked version's API, and read its upstream source before you add a wrapper or workaround. Extend a dependency through its public API; never patch its source. Library APIs do not define coding policy.
82
-
83
- **Once.** Extract repeated logic once; one fact has one owner. A domain enum, such as a status, has one schema that others reuse. A cache or atom family key carries the full identity of what it caches.
84
-
85
- ```ts
86
- // good
87
- const sandboxGone = Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
88
- // bad — the same mapping written in three handlers
89
- pipe(
90
- sandbox.exec(input),
91
- Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
92
- )
93
- pipe(
94
- sandbox.read(path),
95
- Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
96
- )
42
+ Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
43
+ import {glob} from 'glob' // Effect's FileSystem has glob
44
+ const digits = BigDecimal.scale(BigDecimal.abs(amount), 2).value.toString().padStart(3, '0') // BigDecimal.format
97
45
  ```
98
46
 
99
- **Canonical data.** Store and send canonical data, derive display values, expose each state once and remove states no caller produces.
100
-
101
- **One way.** No duplicate, subset or superset fields, methods, options or exports.
102
-
103
- **No compatibility.** Rename the shape and every caller together. No alias, fallback, deprecated path or flag unless a documented or user-named contract requires it.
104
-
105
- **Dead code.** Delete every superseded or unused file, folder, export, type, branch, config, validation, doc, test and dependency in the same change. Update docs and generated bundles that describe changed behavior in the same commit.
106
-
107
47
  ## Patterns
108
48
 
109
49
  Keep the repository consistent by default, without a reminder, and change a standard everywhere at once.
110
50
 
111
- **Canonical example.** Before writing a recurring shape, such as a page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout or name, find its canonical example. Use the row in the Patterns section of `CODING_STANDARDS.md` first. Otherwise, use the variant that most current consumers use, and the newest one on a tie; the nearest file is not the example when it uses a minority variant. Copy its structure, components, names, visible terms, copy and states. Depart from it only on request, and never write a recurring shape from scratch when an example exists.
112
-
113
- **Register.** When a task settles a shape that two or more consumers share and the Patterns section has no row for it, add one: the pattern, its owner and canonical example, a search that lists its consumers, and what enforces it.
114
-
115
- **Taste.** Read the Taste section of `CODING_STANDARDS.md` before choosing a layout, file organization, interface or name, and follow its rows like Patterns rows. When the user picks a variant, rejects one or corrects a taste choice that no rule covers, add a row in the same change: the area, the choice, the rejected alternative and his reason in his words. A row that recurs across repositories moves into the owning skill.
116
-
117
- **Pattern change.** A user change to a pattern, style, name or convention changes the repository standard, unless he limits its scope. In the same change, update its owner: the shared component, template, generator, lint rule and Patterns row. List every consumer with the row's search, migrate each one, delete the superseded variant and its exemptions, then run the search for the old form again. Report each consumer kept on purpose with its reason. A migration that crosses another repository or a published contract needs the user's approval first.
118
-
119
- **Enforce.** When the same correction happens twice, make the mistake impossible at the highest level that works: an owner component, export or type that removes the wrong form, then a lint rule whose message names the replacement, then a test. Keep a prose rule only for a judgment that no check can make. Prove a new check against the real past mistake and an accepted control.
120
-
121
- ## Lint pairs
122
-
123
- Lint enforces these forms without an autofix; each example satisfies every rule involved.
124
-
125
- **Fallback.** A default is `??`; never a ternary on the same value.
126
-
127
- ```ts
128
- // good
129
- const root = options?.root ?? '.'
130
- // bad — no-negated-condition flips it, then prefer-nullish-coalescing flags it
131
- const root = options?.root !== undefined ? options.root : '.'
132
- ```
133
-
134
- **Conditions.** A boolean is tested bare; `=== true` only on a value that can be undefined.
135
-
136
- ```tsx
137
- // good
138
- if (enabled) return
139
- aria-current={Boolean.match(props.selected === true, {onFalse: () => undefined, onTrue: () => 'page'})}
140
- // bad
141
- if (enabled === true) return // enabled is a boolean
142
- if (props.selected) return // props.selected can be undefined
143
- ```
144
-
145
- **Flat.** Use early-return statements in Effect.fn. No ternaries, in JSX too, and no combinators deeper than equivalent statements. An early return, `Boolean.match`, `Option.match`, `Array.match`, `Match` or a lookup record selects a value.
146
-
147
- ```ts
148
- // good
149
- const kill = Effect.fn('Sandbox.kill')(function* (sandbox?: Sandbox) {
150
- if (sandbox?.status !== 'running') return
151
-
152
- yield* sandbox.kill
153
- })
154
- Boolean.match(ignoreCase, {onFalse: () => undefined, onTrue: () => 'i'})
155
- // bad
156
- const type = event.type === 'text-delta' ? 'text' : event.type === 'reasoning-delta' ? 'reasoning' : 'unknown'
157
- const flags = ignoreCase ? 'i' : undefined // "instead of a ternary the Boolean.match would be cleaner"
158
- {props.children ? props.children(entry) : <Label entry={entry} />} // "the ternaries are impossible to read"
159
- // bad
160
- Array.appendAll(
161
- base,
162
- Boolean.match(hasTools, {
163
- onFalse: () => [],
164
- onTrue: () =>
165
- pipe(
166
- tools,
167
- Array.map(tool => pipe(tool, Option.toArray))
168
- )
169
- })
170
- )
171
- ```
172
-
173
- **Choices.** `Match` with `Match.exhaustive` for a union, never `switch`.
174
-
175
- **Options.** Use Option operators, never `.value`. Use Option where it composes; keep plain optional values otherwise.
176
-
177
- ```ts
178
- // good
179
- Option.map(Number.parse(event.target.value), field.handleChange)
180
- Option.map(Array.head(parts), part => part.text)
181
- // bad
182
- if (Option.isSome(open)) open.value.append(event.delta)
183
- const parsed = Option.getOrUndefined(Number.parse(event.target.value))
184
- ```
185
-
186
- **Absence.** `undefined` marks an absent value. Use `null` only where an outside contract, such as a stored schema or a library callback, demands it, with an inline disable that names the contract.
187
-
188
- ```ts
189
- // good
190
- const last = Option.getOrUndefined(Array.last(sessions))
191
- // oxlint-disable-next-line no-restricted-properties -- The schema stores a missing link as null.
192
- url: Option.getOrNull(url)
193
- // bad
194
- failed: Option.getOrNull(Option.none()) // "i don't like null, i prefer undefined"
195
- ```
196
-
197
- **Handlers.** A handler used once is an arrow in place; a handler used twice is a `function`.
198
-
199
- **Accumulators.** No `let`: a fold or an Effect collection operation that says what it selects.
200
-
201
- ```ts
202
- // good
203
- const total = Array.reduce(values, 0, Number.sum)
204
- pipe(teamId, String.split(','), Array.map(String.trim), Array.findFirst(String.isNonEmpty))
205
- Array.filterMap(items, item =>
206
- Boolean.match(supports(item), {onFalse: () => Result.failVoid, onTrue: () => Result.succeed(item.value)})
207
- )
208
- // bad
209
- let total = 0
210
- for (const value of values) total += value
211
- const [first] = teamId
212
- .split(',')
213
- .map(value => value.trim())
214
- .filter(value => value.length > 0)
215
- items.filter(item => supports(item)).map(item => item.value)
216
- ```
217
-
218
- **Natives.** Use Effect modules instead of globals and native methods. Third-party-owned methods stay, with an inline diagnostic disable and reason when needed.
219
-
220
- ```ts
221
- // good
222
- Number.parse(input.count)
223
- Predicate.isString(value)
224
- Predicate.hasProperty(node, 'tagName')
225
- Predicate.isNotNullish(annotation)
226
- Array.isReadonlyArrayEmpty(node.arguments)
227
- String.isNonEmpty(event.delta)
228
- Array.get(items, 0)
229
- const serve = Effect.gen(function* () {
230
- const start = yield* Clock.currentTimeMillis
231
- const host = yield* pipe(Config.String('HOST'), Config.withDefault('0.0.0.0'))
232
- const password = yield* Config.Redacted('SMTP_PASS')
233
- yield* Effect.sleep(Duration.millis(delay))
234
- return {host, password, start}
235
- })
236
- // bad
237
- parseInt(input.count)
238
- typeof value === 'string'
239
- 'tagName' in node // "typeof and in shouldn't be allowed, probably this should use effect schema"
240
- annotation !== null && annotation !== undefined
241
- node.arguments.length === 0
242
- Effect.promise(() => new Promise(resolve => setTimeout(resolve, delay)))
243
- process.env.HOST ?? '0.0.0.0'
244
- ```
245
-
246
- **Pipe.** A standalone `pipe`, nested at most three deep; never the `.pipe` method.
247
-
248
- **Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Keep `Effect.fail(error)` when the error is not yieldable; outside a generator, use `Effect.fail`. Do not infer yieldability from factory-call syntax.
249
-
250
- ```ts
251
- // good
252
- const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
253
- if (response.status === 'refused') return yield* AiError.make({message: 'The model refused the prompt'})
254
-
255
- return response.text
256
- })
257
- // bad
258
- const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
259
- if (response.status === 'refused') yield* Effect.fail(AiError.make({message: 'The model refused the prompt'}))
260
- if (response.status === 'refused') throw new Error('The model refused the prompt')
261
- return response.text
262
- })
263
- ```
264
-
265
- **Casts.** No `as`, `any`, dropped generic, loosened exported type or known shape erased into `unknown`, even to make a symptom go away; `satisfies` and `as const` are the only assertions. Opaque SDK and framework inputs remain `unknown` until narrowed or decoded at their boundary; never fabricate a shape to avoid that type.
266
-
267
- ```ts
268
- // good
269
- const params = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))(part.params)
270
- return Effect.succeed([{text: part.text, type: 'text'} satisfies TextContent])
271
- // bad — a cast satisfies one rule and the next flags it
272
- const params = part.params as Record<string, unknown>
273
- const params: unknown = part.params // erases an already-known shape
274
- ```
275
-
276
- **Decoders.** `Schema.decodeUnknownEffect` only for `unknown` input; typed input uses `Schema.decodeEffect`, and JSON text goes through `Schema.fromJsonString`.
277
-
278
- ```ts
279
- // good
280
- const load = Effect.fn('Plugin.load')(function* (path: string) {
281
- const endpoint = yield* Schema.decodeEffect(Endpoint)(config.endpoint)
282
- const manifest = yield* Schema.decodeEffect(Schema.fromJsonString(PackageManifest))(yield* fs.readFileString(path))
283
- return {endpoint, manifest}
284
- })
285
- // bad
286
- Schema.decodeUnknownEffect(Endpoint)(config.endpoint) // config.endpoint is a string
287
- JSON.parse(text)
288
- ```
289
-
290
- **Imports.** Keep module names; only generated shadcn aliases differ. Separate type imports; packages, blank line, then # aliases. Every export has a consumer.
291
-
292
- ```ts
293
- // good
294
- import {Effect, Schema} from 'effect'
295
- // bad — each line is a diagnostic
296
- import {Schema as S} from 'effect'
297
-
298
- import type {Plugin} from 'vite'
299
- import {type Plugin, createServer} from 'vite'
300
-
301
- import type {PortfolioState} from '#rpcs/contracts.ts'
302
- export const EntryKind = Schema.Literals(['income', 'expense']) // no importer
303
- ```
304
-
305
- **Readonly.** Required on service-shape properties. Allowed on class members and handwritten `Schema.suspend` cycle types; otherwise forbidden.
306
-
307
- **Disables.** Fix a diagnostic with the shown idiom, never a cast, a weakened rule or a rewrite that trades one diagnostic for another, or reproduce a rule defect. Inline disables require an unavoidable conflict and a `--` reason; never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
308
-
309
- ```ts
310
- // good
311
- // oxlint-disable-next-line typescript/consistent-type-definitions -- TanStack Router augments this interface by name.
312
- // @effect-diagnostics-next-line nodeBuiltinImport:off -- Vite's plugin API hands over a Node server.
313
- // bad
314
- "exactOptionalPropertyTypes": false
315
- // oxlint-disable-next-line typescript/consistent-type-assertions
316
- // oxlint-disable-next-line sort-keys -- keeps the printed order
317
- // bad
318
- files: ['packages/components-web/src/form.tsx', 'packages/components-web/src/rich-text-area.tsx'],
319
- ```
51
+ - **Canonical example.** Before you write a recurring shape, such as a page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout or name, find its canonical example: the row in the Patterns section of `CODING_STANDARDS.md`, else the variant that most current consumers use, the newest one on a tie. Copy its structure, components, names, visible terms, copy and states. Depart from it only on request.
52
+ - **Register.** When a task settles a shape that two or more consumers share and no Patterns row covers it, add one: the pattern, its owner and canonical example, a search that lists its consumers, and what enforces it.
53
+ - **Taste.** The user's taste decides layout, visual design, file organization, module boundaries, public interfaces and names. Read the Taste section of `CODING_STANDARDS.md` first and follow its rows like Patterns rows; decide a choice that no row settles from the closest rows. When the user picks, rejects or corrects a choice that no rule covers, add a row in the same change: the area, the choice, the rejected alternative and his words. Create a missing `CODING_STANDARDS.md` only to hold a Patterns or Taste row. A row that recurs across repositories moves into the owning skill.
54
+ - **Pattern change.** A user change to a pattern, style, name or convention changes the repository standard unless he limits its scope. In the same change, update its owner (shared component, template, generator, lint rule and Patterns row), migrate every consumer that the row's search lists, delete the superseded variant and its exemptions, and run the search again. Report each consumer kept on purpose. A migration across another repository or a published contract needs the user's approval first.
55
+ - **Enforce.** When the same correction happens twice, make the mistake impossible at the highest level that works: an owner component, export or type, then a lint rule whose message names the replacement, then a test. Keep a prose rule only for a judgment that no check can make. Prove a new check against the real past mistake and an accepted control.
320
56
 
321
57
  ## Effect
322
58
 
323
- **Functions.** Use Effect.fn('Name.method') for a traced function with arguments and Effect.fnUntraced for an internal helper. Use Effect.gen for an effect with several statements and no arguments, and a single effect directly. Use Effect.fn.Return only for a generator that calls itself.
59
+ **Functions.** Use `Effect.fn('Service.method')` for a traced function with arguments, `Effect.fnUntraced` for an internal helper, `Effect.gen` for several statements without arguments, and a single effect directly. Use `Effect.fn.Return` only for a generator that calls itself.
324
60
 
325
61
  ```ts
326
- // good
327
62
  const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage) {
328
63
  const input = yield* piUserMessage(message)
329
64
  yield* send(input)
@@ -331,27 +66,13 @@ const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage) {
331
66
  const decodePart = Effect.fnUntraced(function* (part: Part) {
332
67
  return yield* Schema.decodeEffect(Event)(part)
333
68
  })
334
- const stop = Effect.gen(function* () {
335
- yield* setStatus('stopping')
336
- yield* kill
337
- })
338
69
  const restart = setStatus('starting')
339
- // bad
340
- fetch: url => Effect.tryPromise(() => fetch(url))
341
- const decodePart = Effect.fn('decodePart')(function* (part: Part) {}) // a span on an internal helper
342
- const stop = Effect.gen(function* () {
343
- yield* setStatus('stopping')
344
- }) // one statement
345
70
  ```
346
71
 
347
- **Errors.** One domain error per service; preserve cause, redact secrets, map in Effect.fn's pipeline argument. Map SDK unknown errors at the adapter.
72
+ **Errors.** One `Schema.TaggedError` per service. Map in `Effect.fn`'s pipeline argument, keep the cause, redact secrets, and map SDK errors at the adapter, so library failures never leak from the service.
348
73
 
349
74
  ```ts
350
75
  // good
351
- export class AiError extends Schema.TaggedError<AiError>()('AiError', {
352
- cause: Schema.optional(Schema.Defect()),
353
- message: Schema.String
354
- }) {}
355
76
  const prompt = Effect.fn('Ai.prompt')(
356
77
  function* (message: Prompt.UserMessage) {
357
78
  yield* send(message)
@@ -362,23 +83,18 @@ const pages = Effect.tryPromise({
362
83
  catch: cause => NotionError.make({cause, message: 'Cannot list pages'}),
363
84
  try: () => client.pages.list()
364
85
  })
365
- Effect.mapError(cause => WorkspaceError.make({cause: Redacted.make(cause), operation: 'exec input'}))
366
- Effect.tapError(() => cleanup)
367
86
  // bad
368
87
  pipe(
369
88
  fs.readFileString(target),
370
89
  Effect.catch(() => Effect.succeed(''))
371
90
  )
372
- pipe(loadConfig, Effect.retry(Schedule.recurs(3)))
373
- Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped, new
374
- Effect.catch(error => pipe(cleanup, Effect.andThen(Effect.fail(error)))) // fails again with the error it caught
375
- load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak from the service
91
+ Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped
92
+ load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak
376
93
  ```
377
94
 
378
- **Services.** Inline a sole implementation in its Layer; name layers for multiple backends. Defaults use Context.Reference. Implement with Service.of or satisfies; service methods are readonly properties.
95
+ **Services.** Inline a sole implementation in its Layer; name layers only for several backends. Use `Context.Reference` for a default. Build the shape with `Service.of` or `satisfies`.
379
96
 
380
97
  ```ts
381
- // good
382
98
  export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger/service/Ledger') {
383
99
  static readonly layer = Layer.effect(
384
100
  this,
@@ -388,256 +104,124 @@ export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger
388
104
  })
389
105
  )
390
106
  }
391
- export class Ai extends Context.Service<Ai, Ai.Agent>()('@app/ai/service/Ai') {
392
- static layerPi(config: Pi.Config) {
393
- return Layer.effect(this, makePi(config))
394
- }
395
- }
396
107
  export const Verbose = Context.Reference<boolean>('@app/ai/Verbose', {defaultValue: () => false})
397
- // bad
398
- class Logger extends Context.Service<Logger>()('Logger', {make: Effect.succeed(service)}) {}
399
- static readonly layer = Layer.effect(Ledger, makeLedger) // a make with one caller
400
- query(sql: string): Effect.Effect<Rows> // a method signature instead of a readonly property
401
108
  ```
402
109
 
403
- **Primitives.** Use Effect's primitives directly; RPCs and services already trace, and one scope owns each lifetime. Run independent input and output with Effect.all and explicit concurrency; keep order-dependent, transactional or resource-conflicting work sequential.
110
+ **Primitives.** Use Effect's primitives directly: RPCs and services already trace, and one scope owns each lifetime, with acquisition and release in one `Effect.acquireRelease`. Run independent input and output with `Effect.all` or `Effect.forEach` and explicit concurrency; keep order-dependent, transactional or conflicting work sequential.
404
111
 
405
- ```ts
406
- // good
407
- const connect = Effect.gen(function* () {
408
- const state = yield* SubscriptionRef.make(PortfolioState.make({}))
409
- const socket = yield* Effect.acquireRelease(openSocket, closeSocket)
410
- return {socket, state}
411
- })
412
- // bad
413
- class State {
414
- update(f: (s: PortfolioState) => PortfolioState) {
415
- return SubscriptionRef.update(this.ref, f)
416
- }
417
- }
418
- const traced = Effect.withSpan('Notes.create')(notes.create(input))
419
- Effect.addFinalizer(() => closeSocket(socket)) // acquisition and release with different owners
420
- ```
112
+ **Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Use `Effect.fail(error)` for an error that is not yieldable, and for every error outside a generator. Factory-call syntax does not show yieldability; the type does.
421
113
 
422
- **Runtime.** The entry point runs the program with `NodeRuntime.runMain`. Bridge to a Promise once, where an external contract demands it, with `Effect.runPromise` and an inline disable that names the contract. React is no such contract: atoms run its Effects.
114
+ **Results.** Sort with `Order`, and return a fallible pure value as a `Result`.
423
115
 
424
- ```ts
425
- // good
426
- NodeRuntime.runMain(program)
427
- // oxlint-disable-next-line no-restricted-properties -- Bingo's produce contract demands a Promise.
428
- produce: flow(generate, Effect.runPromise),
429
- // bad
430
- const runtime = ManagedRuntime.make(AppLayer)
431
- const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect)
432
- Effect.runPromiseWith(Context.empty()) // "what's the point of using the runPromiseWith if i pass an empty context?"
433
- ```
434
-
435
- **Sorting.** `Order` for sorting, `Result` for a fallible pure value.
436
-
437
- ```ts
438
- // good
439
- Array.sort(
440
- entries,
441
- Order.mapInput(Order.Number, entry => entry.tokens)
442
- )
443
- Result.fromOption(Array.head(parts), () => AiError.make({message: 'The model returned no parts'}))
444
- // bad
445
- entries.sort((left, right) => left.tokens - right.tokens)
446
- ```
116
+ **Runtime.** The entry point runs `NodeRuntime.runMain(program)`. Bridge to a Promise once, where an outside contract demands it, with `Effect.runPromise` and a disable that names the contract. React is no such contract: atoms run its Effects.
447
117
 
448
118
  ## Types and Schema
449
119
 
450
- **Inference.** Infer types. Annotate a return type only on a type guard, an assertion function, a recursive function or the thunk of a recursive `Schema.suspend`. Check a returned object with `satisfies`, and type an empty or one-element array with `Array.empty<T>()` or `Array.of<T>(...)` instead of its callback. Use schema types and indexed members in signatures. Keep intentional widening, not repeated inferred generics.
120
+ - **Inference.** Infer types, and check a returned object with `satisfies`. Type an empty or one-element array with `Array.empty<T>()` or `Array.of<T>(...)`. Never drop a generic or loosen an exported type to silence an error. Annotate a return type only on a type guard, an assertion function, a recursive function or a recursive `Schema.suspend` thunk. Use schema types and indexed members, such as `LedgerEntry['amount']`, in signatures. Keep an intentional widening, such as `useState<'idle' | 'running'>('idle')`. Derive a type from its source, such as `ReturnType<typeof fromSdk>`; never copy an inferred shape by hand.
121
+ - **Schema pairs.** Write `export type X = typeof X.Type` directly before `export const X = Schema.Struct(...)`; reuse fields by spreading `.fields`. Use `Schema.optionalKey` for an absent field; reserve `Schema.optional` for an explicit `undefined`.
122
+ - **Construction.** `X.make` validates synchronously and throws: use it for trusted values. For input that can fail a refinement, use `X.makeEffect` and map its issue to the owning domain error.
123
+ - **Cycles.** Handwrite each recursive-cycle type and annotate only its `Schema.suspend` thunk; other types stay inferred, and a union follows its members.
124
+ - **Error predicates.** A predicate is a static member of its tagged error; check a struct inline with `Schema.is`.
125
+ - **Casts.** Opaque SDK and framework inputs stay `unknown` until decoded at their boundary; never fabricate a shape or erase a known shape into `unknown`.
451
126
 
452
127
  ```ts
453
- // good
454
- const balances = Array.reduce(decoded, HashMap.empty<Tag, BigDecimal.BigDecimal>(), sumByTag)
455
- const label = MutableRef.make('') // the initializer infers string, not just ''
456
- const [status, setStatus] = useState<'idle' | 'running'>('idle') // intentional widening
457
- function visit(node: Node): Result {
458
- return visit(node.parent)
459
- }
460
- function settle(amount: LedgerEntry['amount']) {
461
- return BigDecimal.format(amount)
462
- }
463
- function still(bytes: Uint8Array) {
464
- return {bytes, type: 'image/jpeg'} satisfies Image
465
- }
466
- export type Sandbox = ReturnType<typeof fromSdk>
467
- // bad
468
- function still(bytes: Uint8Array): Image {
469
- return {bytes, type: 'image/jpeg'}
470
- } // "not a fan of assigning the return type, i would have preferred a satisfies"
471
- const Input = Schema.Struct({value: Schema.String}) satisfies Schema.Schema<Input>
472
- const label = MutableRef.make<string>('') // repeats the inferred type
473
- export type SandboxLike = {id: string; renew: (seconds: number) => Promise<unknown>} // a copy of an inferred shape
474
- ```
475
-
476
- **Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading. Use Schema.optionalKey for absent fields; Schema.optional allows explicit undefined too, so reserve it for that contract.
477
-
478
- ```ts
479
- // good
480
128
  export type LedgerDraft = typeof LedgerDraft.Type
481
129
  export const LedgerDraft = Schema.Struct({...LedgerEntry.fields, id: Schema.optionalKey(Schema.NonEmptyString)})
482
- RunEventCursor.make(value.toString())
483
- trails: pipe(Schema.Array(PortfolioTrail), Schema.withConstructorDefault(Effect.succeed([])))
484
- // bad
485
- export type EntryDraft = {amount: EntryAmount; id?: EntryId; tag: string} // a hand-written shape beside the schema
486
- // bad
487
- new AiError({message: 'The model returned no text'}) // the schema has make
488
- function cursorFromBigInt(value: bigint) {
489
- return RunEventCursor.make(value.toString())
490
- }
491
- payload: Schema.Struct(pipe(PortfolioVisitor.fields, Struct.pick(['color', 'id', 'name', 'x', 'y']))) // the schema itself fits
492
- ```
493
-
494
- **Construction.** `schema.make` applies defaults and validates synchronously, throwing on failure: use it for trusted construction. For reachable fallible construction, use `schema.makeEffect` and map its `SchemaIssue.Issue` to the owning domain error. Effect decoders report `SchemaError`.
495
-
496
- ```ts
497
- // good — reachable constructor failure stays in the error channel
498
- pipe(
499
- Name.makeEffect(input),
500
- Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
501
- )
502
- // bad — input can fail a refinement even though its TypeScript type is string
503
- Name.make(input)
504
- ```
505
-
506
- **Cycles.** Handwrite each recursive-cycle type; annotate only Schema.suspend thunks. Noncycle types stay inferred; unions follow members.
507
-
508
- ```ts
509
- // good
510
- export type CodeStep = typeof CodeStep.Type
511
- export const CodeStep = Schema.Struct({code: Schema.String, kind: Schema.Literal('code')})
512
130
  export type CallStep = {readonly do: readonly Step[]; readonly kind: 'call'}
513
131
  export const CallStep = Schema.Struct({
514
132
  do: Schema.Array(Schema.suspend((): Schema.Codec<Step> => Step)),
515
133
  kind: Schema.Literal('call')
516
134
  })
517
- export type Step = CallStep | CodeStep
518
- export const Step = Schema.Union([CallStep, CodeStep])
519
- ```
520
-
521
- **Error predicates.** A predicate is a static member of its tagged error; a struct is checked inline with `Schema.is`.
522
-
523
- ```ts
524
- // good
525
- class PathNotFound extends Schema.TaggedError<PathNotFound>()('PathNotFound', {
526
- cause: Schema.optional(Schema.Defect())
527
- }) {
528
- static matches = (error: Cause.UnknownError) => Schema.is(FileNotFound)(error.cause)
529
- }
530
- // bad
531
- function isPathNotFound(error: unknown) {
532
- return Schema.is(FileNotFound)(error)
533
- }
135
+ pipe(
136
+ Name.makeEffect(input),
137
+ Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
138
+ )
534
139
  ```
535
140
 
536
141
  ## Shape
537
142
 
538
- **Inline.** Inline forwarding wrappers, temporaries, constants and property aliases; keep an alias only where a callback needs its narrowed type. Inline a small helper with one caller. A helper that several files share lives in the package's `lib/utils`. Argument-free functions are named values.
539
-
540
- ```ts
541
- // good
542
- function randomIndex(length: number) {
543
- return Random.Random.defaultValue().nextIntUnsafe() % length
544
- }
545
- const isApiUrl = Predicate.compose(String.isString, Predicate.or(Equal.equals('/api'), String.startsWith('/api/'))) // a predicate, reused
546
- static generateText = generateTextPi
547
- const notFound = HttpServerResponse.empty({status: 404})
548
- // bad
549
- const random = Random.Random.defaultValue()
550
- function isBackendRequest(request: IncomingMessage) {
551
- return isApiUrl(request.url)
552
- } // "this is just a wrapper / factory function"
553
- Ref.set(entries, Array.copy(decoded)) // decoded is already an array
554
- const runPromise = Effect.runPromiseWith(Context.empty()) // "wrapper function, tmp variable ... that can be inlined"
555
- const parent = read.parent // "inline the property access since it add no value"
556
- function isVideo(type: FileType) {
557
- return String.startsWith('video/')(type)
558
- } // String.startsWith('video/') is already the function
559
- ```
560
-
561
- **Linked state.** Linked state moves in one pure updater, never a setter that calls another.
562
-
563
- ```ts
564
- // good
565
- setState(current => {
566
- const draft = {...current.draft, files}
567
- return {draft, owner: ownerAfter(draft)}
568
- })
569
- // bad
570
- setDraft(current => {
571
- setOwner(ownerAfter(current))
572
- return {...current, files}
573
- })
574
- ```
575
-
576
- **File tree.** Use explicit kebab-case role names and one naming scheme. Prefer one substantive file to near-empty fragments; colocate <name>.test.ts; no barrels.
577
-
578
- **Package surface.** Declare exports for imported subpaths and import aliases for internal paths. Use the shared lint preset with generated ignores only.
579
-
580
- ```ts
581
- // good
582
- "exports": {"./plugin": "./src/generated/plugin.ts"}
583
- export {oxlint as default} from '@deslop/coding-standards'
584
- // bad — consumers import two names from a barrel of everything
585
- export * from './generated/Client.ts'
586
- const warned = ['sort-keys', 'typescript/no-restricted-types'] // a package rule list
587
- "typecheck": "tsc --noEmit" // type-aware lint already checks types
588
- ```
589
-
590
- **Generated code.** A generator emits code that follows these rules as closely as it can; lint ignores its output.
143
+ - **Inline.** Inline forwarding wrappers, temporaries, constants and property aliases; keep an alias only where a callback needs its narrowed type. Inline a small helper with one caller; a helper that several files share lives in the package's `lib/utils`. An argument-free function is a named value.
144
+ - **Linked state.** Linked state moves in one pure updater, never a setter that calls another.
145
+ - **File tree.** Use explicit kebab-case role names and one naming scheme. Prefer one substantive file to near-empty fragments; colocate `<name>.test.ts`; no barrels.
146
+ - **Package surface.** Declare exports for imported subpaths and import aliases for internal paths. Extend the shared lint preset with generated ignores only; add no package rule list or separate typecheck script.
147
+ - **Generated code.** A generator emits code that follows these rules as closely as it can; lint ignores its output.
591
148
 
592
149
  ## React
593
150
 
594
- **Components.** React Compiler memoizes. Import hooks by name from `react`. Destructure state at its declaration, receive component refs through props, and keep only a component's own interaction state in React.
151
+ - **Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves it to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)` and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them.
152
+ - **Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary and one error boundary with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result. A reversible write updates the screen at once through `Atom.optimistic` and `Atom.optimisticFn`.
153
+ - **Ownership.** A component that owns atoms, writes or a lifetime gets its own file; a small presentational helper stays beside its one user. Reset an editable draft through a `key` on its identity, never a `useEffect` that copies props.
154
+ - **Pure render.** Derive display values during render, and never mutate them or run an RPC, write or subscription there. Event handlers own user actions; `useEffect` synchronizes only external systems.
155
+ - **State identity.** Keep one owner per value, stable domain keys for list items and deliberate reset boundaries. Fix an observed rerender by deriving the value in the existing atom graph; add no manual memoization or compiler escape directives.
595
156
 
596
157
  ```tsx
597
- // good
598
- const editorRef = useRef<Lexical.LexicalEditor>(null)
599
- const [showShortcuts, setShowShortcuts] = useState(false)
600
- const moveRpc = useAtomSet(RpcClient.mutation('portfolio.move'))
601
- // bad
602
- const handle = useCallback(() => setOpen(true), [])
603
- const state = useState(false)
604
- const [open, setOpen] = React.useState(false) // "why are you using the React.useState and not importing useState"
605
- ```
606
-
607
- **Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them, so never pass them to `Atom.fn`.
608
-
609
- ```tsx
610
- // good
611
158
  const appsRuntime = Atom.runtime(DualApps.layer)
612
159
  const appAtom = Atom.family((id: AppId) => pipe(appsRuntime.atom(loadApp(id)), Atom.withReactivity(['apps'])))
613
160
  const saveAppAtom = appsRuntime.fn((draft: AppDraft) => saveApp(draft), {reactivityKeys: ['apps']})
614
- const app = useAtomValue(appAtom(appId))
615
- const [saveResult, save] = useAtom(saveAppAtom)
616
- // bad
617
- useQuery({queryFn: () => Effect.runPromiseWith(Context.empty())(loadApp(appId))})
618
- onClick={() => Effect.runPromise(saveApp(draft))}
619
- useAtomValue(Atom.make(loadApp(appId))) // a new atom on every render
620
- ```
621
-
622
- **Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary for loading. One error boundary there shows the failure with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result, never from copied React state. A reversible write updates the screen at once through `Atom.optimistic` and `Atom.optimisticFn`, which roll back on failure.
623
-
624
- ```tsx
625
- // good
626
161
  function AppFrame(props: {appId: AppId}) {
627
162
  return <Frame app={useAtomSuspense(appAtom(props.appId)).value} />
628
163
  }
629
- <ErrorBoundary fallback={<LoadError onRetry={refresh} />}>
630
- <Suspense fallback={<AppFrameSkeleton />}>
631
- <AppFrame appId={appId} />
632
- </Suspense>
633
- </ErrorBoundary>
634
- <Button disabled={saveResult.waiting} onClick={() => save(draft)}>Save</Button>
635
- // bad
636
- app.data ? <AppFrame app={app.data} /> : <Spinner /> // a failed load spins forever
637
- const members = query.data ?? [] // a failed load looks like no members
638
- const [pending, setPending] = useState(false) // copies the write atom's waiting state
639
164
  ```
640
165
 
641
- - **Ownership.** Give a component that owns its own atoms, writes or lifetime its own file; keep small presentational helpers beside their one user. Reset an editable draft through a `key` on its identity, never a `useEffect` that copies props.
642
- - **Pure render.** Derive display values from props and subscribed state during render, and never mutate them or run an RPC, write or subscription there. Event handlers own user actions; `useEffect` synchronizes only external systems, never copied state or calculations.
643
- - **State identity.** Keep one owner per value, stable domain keys for list items and deliberate reset boundaries. For an observed rerender problem, derive the value in the existing atom graph instead of subscribing to unrelated changing state. Compiler memoization never guarantees correctness or lifetime; add no manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
166
+ ## Lint forms
167
+
168
+ Lint rejects the right column and has no autofix for it. Write the left column from the start.
169
+
170
+ | Write | Never | Rule |
171
+ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
172
+ | An early `return`, `Boolean.match(x, {onFalse, onTrue})`, `Option.match` or a lookup record | `a ? b : c`, in JSX too | `no-ternary` |
173
+ | `options?.root ?? '.'` | `root !== undefined ? root : '.'` | `no-nullish-ternary`, `prefer-nullish-coalescing` |
174
+ | `pipe(Match.value(event), Match.tag('Text', text), Match.exhaustive)` | `switch` | `no-switch-statement` |
175
+ | `selected === true` on a value that can be `undefined` | `if (selected)` on an optional | `strict-boolean-expressions` |
176
+ | `Array.filter` before the loop, an early `return` in a callback | `continue` | `no-continue` |
177
+ | At most 3 nested blocks, 6 nested callbacks and 3 nested `pipe` calls | Deeper nesting | `max-depth`, `max-nested-callbacks`, `no-deep-pipe` |
178
+ | `pipe(value, f, g)` | `value.pipe(f, g)` | `no-restricted-properties` |
179
+ | `const total = Array.reduce(values, 0, Number.sum)` | `let` with a loop | `no-let` |
180
+ | `Array.join(parts, ', ')`, `Array.map(items, f)`, `String.split(text, ',')`, `Record.keys(record)` | `parts.join(', ')`, `items.map(f)`, `text.split(',')`, `Object.keys(record)` | `no-native-method-call`, `no-restricted-globals`; the `Path` service's `path.join` stays |
181
+ | `Predicate.isString(v)`, `Predicate.hasProperty(node, 'tag')`, `Predicate.isNotNullish(v)` | `typeof`, `'tag' in node`, `v !== null && v !== undefined` | `no-typeof`, `no-in-operator`, `no-double-nullish-check` |
182
+ | `Array.isReadonlyArrayEmpty(items)`, `String.isNonEmpty(text)` | `items.length === 0`, `text.length > 0` | `no-native-emptiness-check` |
183
+ | `Array.ensure(value)` | `Array.isArray(value) ? value : [value]` | `no-array-wrap-ternary` |
184
+ | `Option.map(option, f)`, `Option.match`, `Option.getOrElse` | `option.value` after `Option.isSome` | `no-option-value-access` |
185
+ | `undefined`, `Option.getOrUndefined` | `null`, `Option.getOrNull`, except for an outside contract | `unicorn/no-null`, `no-restricted-properties` |
186
+ | `Number.parse(text)`, `Number.max`, `HashMap`, `HashSet`, `DateTime` | `parseInt`, `Math.max`, `Map`, `Set`, `Date` | `no-restricted-globals`, `no-restricted-properties`, `no-restricted-types` |
187
+ | `Clock`, `Config`, `Console`, `Random`, `Effect.sleep`, `HttpClient` | `Date.now()`, `process.env`, `console`, `setTimeout`, `fetch` | `effecttsgo/global-*`, `effecttsgo/process-env` |
188
+ | `function name() {}` for a named function; an arrow only in place | `const name = () => {}` | `func-style` |
189
+ | `server => ({...server, warmup})`, `for (const schema of schemas)`, `pair => pair[0]` | `({host, port}) =>`, `for (const {name} of schemas)` | `no-destructured-parameter`; labeled tuples, React state and refs can destructure |
190
+ | `Array.sort(entries, Order.mapInput(Order.Number, (entry: Entry) => entry.tokens))` | `entries.sort((left, right) => left.tokens - right.tokens)` | `no-native-method-call`; a third-party object's own method stays |
191
+ | The source value where it is used | A one-line wrapper, alias or argument-free constant function | `no-trivial-indirection`, `no-constant-function` |
192
+ | `import {Schema} from 'effect'` | `import {Schema as S} from 'effect'` | `no-renamed-import` |
193
+ | Properties built with `Effect.fn` or `Effect.fnUntraced` | `fetch: url => Effect.tryPromise(...)` as a property arrow | `no-effect-property-arrow` |
194
+ | Inferred return and variable types, `satisfies`, `Array.empty<T>()` | `function f(): T`, `const items: T[] = []` | `no-return-type`, `no-redundant-variable-annotation` |
195
+ | `satisfies T`, `as const` | `as T`, `any`, `value!` | `consistent-type-assertions`, `no-explicit-any`, `no-non-null-assertion` |
196
+ | `Ref.make(0)`, `useState(false)`, `useRef<HTMLDivElement>(null)` | `Ref.make<number>(0)`, `useRef<HTMLDivElement \| null>(null)` | `no-redundant-state-type`, `no-redundant-use-ref-null-type` |
197
+ | `T[]`, an optional property | `Array<T>`, `Promise<T>`, `x: T \| undefined` | `no-restricted-types` |
198
+ | `return yield* NotFound.make({...})` | `yield* Effect.fail(NotFound.make({...}))` | `effecttsgo/unnecessary-fail-yieldable-error` |
199
+ | `Schema.decodeEffect(S)(typed)`, `Schema.fromJsonString(S)` for JSON text | `Schema.decodeUnknownEffect` on typed input, `JSON.parse` in an Effect | `effecttsgo/prefer-typed-schema-decoder`, `prefer-schema-over-json` |
200
+ | The Schema operation inline at its use | `const decode = Schema.decodeEffect(S)` stored for later | `no-stored-schema-operation` |
201
+ | `Schema.Struct`, `Schema.TaggedError`, `Schema.NonEmptyString` | `Schema.Class`, `Data.TaggedError`, `Error`, a rebuilt refinement | `no-restricted-properties`, `no-restricted-globals`, `no-reinvented-schema` |
202
+ | The failure mapped to the service's error | `Effect.ignore`, `Effect.orDie` outside tests | `no-discarded-failure` |
203
+ | `NodeRuntime.runMain(program)`; atoms in React | `Effect.runPromise`, `ManagedRuntime.make` | `no-restricted-properties` |
204
+ | `import {useState} from 'react'`; `const [open, setOpen] = useState(false)` | `React.useState`, `const state = useState(false)` | `no-restricted-properties`, `no-undestructured-use-state` |
205
+ | React Compiler memoization, a `ref` prop | `useCallback`, `useMemo`, `memo`, `forwardRef` | `no-restricted-imports` |
206
+ | The UI package's primitive | A styled native `<button>` or `<input>` | `no-native-control` |
207
+ | `assert` from `@effect/vitest`, the failure's identity | `expect`, `vi`, an assertion on message wording | `no-restricted-imports`, `no-error-message-assertion` |
208
+ | One comment line for an outside constraint | A multiline comment | `no-multiline-comment` |
209
+ | Object keys in ascending order; a blank line starts a new group | Unsorted keys, a `sort-keys` disable | `sort-keys`, `no-unexplained-disable` |
210
+
211
+ These forms satisfy every rule involved, where the obvious fix trades one diagnostic for another:
212
+
213
+ ```ts
214
+ // A handler used twice: a function declaration, not a const arrow.
215
+ function label(entry: Entry) {
216
+ return Boolean.match(entry.done, {onFalse: () => 'Running', onTrue: () => 'Done'})
217
+ }
218
+ // Two nested loops: one loop over the pairs, not a sixth nested callback.
219
+ Effect.forEach(Array.cartesian(cases, harnesses), pair => prepareRun(pair[0], pair[1]), {concurrency: 'unbounded'})
220
+ // Cleanup on failure: its own failure flows, without Effect.orDie or Effect.ignore.
221
+ pipe(
222
+ prepareRuns,
223
+ Effect.onExitIf(Exit.isFailure, () => fs.remove(directory, {recursive: true}))
224
+ )
225
+ // A choice inside a deep pipeline: a named function, not a nested Boolean.match.
226
+ Array.appendAll(base, toolParts(tools, hasTools))
227
+ ```
@@ -9,14 +9,14 @@ Challenge the work independently. Trace the contract, try its strongest reachabl
9
9
 
10
10
  ## Code review
11
11
 
12
- Review the normalized diff: the author must run Oxfmt and deterministic lint autofix first. Do not spend review reasoning on formatting or already-fixable diagnostics. If normalization is unavailable, name that gap and have the author prepare it; never mutate a read-only target.
12
+ Review the diff that the author normalized per engineering. Spend no reasoning on formatting or diagnostics that lint reports outside legacy lists; if normalization is missing, name that gap and never mutate a read-only target.
13
13
 
14
14
  - **Depth.** Read every hand-written changed line. For generated output, review the generator change and a sample, such as one package per source format. Trace each contract through its real consumers, failure paths, concurrency and resource ownership against the merge base. Try the inputs, orderings and failures that the tests miss. When the change refactors a whole file, list each behavior the file had at the merge base and prove that each one still holds, apart from the planned change.
15
15
  - **Findings.** Report broken behavior or contracts that a real caller's input reaches. Report violations of engineering, design or `CODING_STANDARDS.md` in changed lines, and in every line of a file that the task adds or rewrites. A recurring shape that departs from its canonical example without a request is a finding. For a pattern change, run the Patterns search for the old form and report each remaining consumer. Skip what lint enforces outside legacy lists, and restyling that the change does not need.
16
16
  - **Beyond the diff.** A contradiction with an unchanged file, a stale reference that the change leaves, and a false assumption that the change rests on are findings.
17
17
  - **Not findings.** Taste without a rule, wording, hypothetical inputs, and defects that the branch neither made nor relies on.
18
- - **Later rounds.** Retain the covered commit, contracts and scope. Review only uncovered hunks, the fixes since that commit, and their affected consumers. Reuse prior source reads while those sources are unchanged. A confirmed local fix needs no new independent round unless it introduces an uncovered contract, mechanism or failure path. A new notice or a changed PR body is not a code-review trigger.
19
- - **Probes.** Settle a suspicion with disposable inline code against the public interface, real data or installed artifact. Use the repository's runtime command, such as `vp node -e '<script>'` or `vp env exec bun -e '<script>'`; verify that it loads the source and dependencies. Do not create a test file, worktree or test-only seam to answer one question. Never start a service or test suite during read-only review.
18
+ - **Later rounds.** Read the published pull request diff, such as `gh pr diff <number>`, and judge the whole layer as one change in every round: structure, duplication and cleanup across its files. Spend the Depth on the commits since the covered commit and their consumers, and reuse prior reads of unchanged sources. A stack overview reads every layer together and reports what crosses layers: duplication, a contract one layer breaks for another, and code in the wrong layer. A confirmed local fix needs no new independent round unless it introduces an uncovered contract, mechanism or failure path. A new notice or a changed PR body is not a code-review trigger.
19
+ - **Probes.** Settle a suspicion with a probe per testing's Throwaway. Never start a service or test suite during read-only review.
20
20
  - **Browser evidence.** Inspect the existing preview, captures, console and relevant requests without changing page state. Give the owner the smallest missing browser action and observable result to check; the owner drives the browser and records it. A screenshot proves a visible state, not unobserved timing, persistence or release behavior.
21
21
  - **Test value.** Use testing to decide whether a permanent test protects a distinct plausible regression. Do not demand a new test for every finding when an inline probe or existing browser proof settles it.
22
22
  - **Variants.** After reproducing a defect, look for the same root cause in its other real callers. Broaden one dimension at a time and stop when unrelated hypotheses replace concrete evidence.
@@ -11,8 +11,8 @@ description: 'Select and run black-box behavior proof for Effect apps and tools.
11
11
  - **Public interface.** Test exported Layers, services and functions through observable behavior, so a rewrite that keeps the contract keeps the tests green. The browser proves web UI components; the simulator proves native ones.
12
12
  - **Redundant.** Delete touched cases that only repeat dependency guarantees, constants, wording, removed behavior or a covered input. Keep boundary tests of the application's own validation, encoding or integration.
13
13
  - **Independent.** Take expected values from the contract or a worked example, never from the implementation. Use the smallest reachable fixture that tells the cases apart.
14
- - **Bugs.** Reproduce a bug before you fix it. Keep a regression case when future changes can plausibly break its contractual behavior, in the existing test of the fixed code or a new case when none fits. An independently reproduced defect does not require a new test file.
15
- - **Throwaway.** Start with question-sized inline code, an existing test or a focused real-browser check. Delete the spike after it answers the question. Promote it only when its distinct regression protection meets Worth it; never add a private export or configurable production seam just for proof.
14
+ - **Bugs.** Reproduce a bug before you fix it. For a hard bug, test one hypothesis per probe, remove the edits of a disproven hypothesis and rerun the original scenario. Keep a regression case when future changes can plausibly break its contractual behavior, in the existing test of the fixed code or a new case when none fits. An independently reproduced defect does not require a new test file.
15
+ - **Throwaway.** Start with question-sized inline code, an existing test or a focused real-browser check. Run inline code against the public interface, real data or the installed artifact with the repository's runtime command, such as `vp node -e '<script>'` or `vp env exec bun -e '<script>'`, and verify that it loads the source and dependencies. Never create a test file, worktree or seam to answer one question. Delete the spike after it answers the question. Promote it only when its distinct regression protection meets Worth it; never add a private export or configurable production seam just for proof.
16
16
  - **Honest.** No wrong expectations or rule workarounds. Change assertions only for recorded behavior changes, and keep every still-relevant value.
17
17
 
18
18
  **Assertions.** Use one representative input per behavior, in its existing case. Share a helper only between cases that use it. Use @effect/vitest assertions for outputs and structured error identity. Replace dependencies through Layers or public parameters, never global or module mocks. Keep mutable state in its owning fixture.