@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 +1 -1
- package/package.json +1 -1
- package/skills/architecture/EVALS.md +1 -1
- package/skills/architecture/SKILL.md +6 -8
- package/skills/design/EVALS.md +2 -2
- package/skills/design/SKILL.md +5 -9
- package/skills/engineering/SKILL.md +125 -541
- package/skills/review/SKILL.md +3 -3
- package/skills/testing/SKILL.md +2 -2
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.
|
|
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
|
@@ -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;
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
package/skills/design/EVALS.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
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
|
|
package/skills/design/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
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.**
|
|
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
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
13
|
-
|
|
14
|
-
**
|
|
15
|
-
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
**
|
|
19
|
-
|
|
20
|
-
**
|
|
21
|
-
|
|
22
|
-
**
|
|
23
|
-
|
|
24
|
-
**
|
|
25
|
-
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
**
|
|
29
|
-
|
|
30
|
-
**
|
|
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
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
**
|
|
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('
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
**
|
|
114
|
+
**Results.** Sort with `Order`, and return a fallible pure value as a `Result`.
|
|
423
115
|
|
|
424
|
-
|
|
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
|
|
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
|
-
|
|
518
|
-
|
|
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
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
```
|
package/skills/review/SKILL.md
CHANGED
|
@@ -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
|
|
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.**
|
|
19
|
-
- **Probes.** Settle a suspicion with
|
|
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.
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -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.
|