@typeonce/oxlint-plugin-effect-machine 0.26.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +151 -9
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/ambient.d.ts +6 -0
  7. package/dist/internal/ambient.d.ts.map +1 -0
  8. package/dist/internal/ambient.js +118 -0
  9. package/dist/internal/ambient.js.map +1 -0
  10. package/dist/internal/ast.d.ts +7 -0
  11. package/dist/internal/ast.d.ts.map +1 -0
  12. package/dist/internal/ast.js +62 -0
  13. package/dist/internal/ast.js.map +1 -0
  14. package/dist/internal/imports.d.ts +1 -0
  15. package/dist/internal/imports.d.ts.map +1 -1
  16. package/dist/internal/imports.js +19 -9
  17. package/dist/internal/imports.js.map +1 -1
  18. package/dist/internal/planning.d.ts +3 -0
  19. package/dist/internal/planning.d.ts.map +1 -1
  20. package/dist/internal/planning.js +54 -7
  21. package/dist/internal/planning.js.map +1 -1
  22. package/dist/internal/rules/noAsyncPlanningCallback.d.ts.map +1 -1
  23. package/dist/internal/rules/noAsyncPlanningCallback.js +18 -3
  24. package/dist/internal/rules/noAsyncPlanningCallback.js.map +1 -1
  25. package/dist/internal/rules/noBrowserApiInPlanning.d.ts +3 -0
  26. package/dist/internal/rules/noBrowserApiInPlanning.d.ts.map +1 -0
  27. package/dist/internal/rules/noBrowserApiInPlanning.js +54 -0
  28. package/dist/internal/rules/noBrowserApiInPlanning.js.map +1 -0
  29. package/dist/internal/rules/noConflictingInvocationIdentity.d.ts +3 -0
  30. package/dist/internal/rules/noConflictingInvocationIdentity.d.ts.map +1 -0
  31. package/dist/internal/rules/noConflictingInvocationIdentity.js +289 -0
  32. package/dist/internal/rules/noConflictingInvocationIdentity.js.map +1 -0
  33. package/dist/internal/rules/noNondeterministicPlanning.d.ts +3 -0
  34. package/dist/internal/rules/noNondeterministicPlanning.d.ts.map +1 -0
  35. package/dist/internal/rules/noNondeterministicPlanning.js +33 -0
  36. package/dist/internal/rules/noNondeterministicPlanning.js.map +1 -0
  37. package/dist/internal/rules/noRedundantResolve.d.ts.map +1 -1
  38. package/dist/internal/rules/noRedundantResolve.js +41 -15
  39. package/dist/internal/rules/noRedundantResolve.js.map +1 -1
  40. package/dist/recommended.d.ts +3 -0
  41. package/dist/recommended.d.ts.map +1 -1
  42. package/dist/recommended.js +3 -0
  43. package/dist/recommended.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +6 -0
  46. package/src/internal/ambient.ts +134 -0
  47. package/src/internal/ast.ts +83 -0
  48. package/src/internal/imports.ts +27 -8
  49. package/src/internal/planning.ts +69 -7
  50. package/src/internal/rules/noAsyncPlanningCallback.ts +21 -4
  51. package/src/internal/rules/noBrowserApiInPlanning.ts +57 -0
  52. package/src/internal/rules/noConflictingInvocationIdentity.ts +343 -0
  53. package/src/internal/rules/noNondeterministicPlanning.ts +39 -0
  54. package/src/internal/rules/noRedundantResolve.ts +48 -16
  55. package/src/recommended.ts +3 -0
@@ -0,0 +1,343 @@
1
+ import type { Context, ESTree, Rule, Variable } from "@oxlint/plugins"
2
+ import { resolvedVariable, unwrapExpression } from "../ast.js"
3
+ import {
4
+ hasMachineImport,
5
+ isMachineMemberCall,
6
+ type MachineBindings,
7
+ makeMachineBindings,
8
+ recordMachineDefinition,
9
+ recordMachineImport,
10
+ staticMemberName
11
+ } from "../imports.js"
12
+ import { isInvokePlanningCallback, type PlanningFunction } from "../planning.js"
13
+
14
+ interface Identity {
15
+ readonly key: string
16
+ readonly label: string
17
+ readonly node: ESTree.Expression
18
+ }
19
+
20
+ interface InvocationIdentity {
21
+ readonly address?: Identity
22
+ readonly lifecycle: Identity
23
+ }
24
+
25
+ const completionMethods = new Set(["onDone", "onElement", "onFailure", "onSnapshot"])
26
+ const sourceMethods = new Set(["effect", "logic", "stream", "timer"])
27
+
28
+ const variableKey = (variable: Variable): string | undefined => {
29
+ const identifier = variable.identifiers[0]
30
+ return identifier === undefined ? undefined : `binding:${identifier.range[0]}:${identifier.range[1]}`
31
+ }
32
+
33
+ const isStableVariable = (variable: Variable): boolean =>
34
+ variable.defs.some((definition) =>
35
+ definition.type === "ImportBinding" ||
36
+ (definition.type === "Variable" &&
37
+ definition.node.type === "VariableDeclarator" &&
38
+ definition.node.parent.type === "VariableDeclaration" &&
39
+ definition.node.parent.kind === "const")
40
+ )
41
+
42
+ const constInitializer = (
43
+ variable: Variable
44
+ ): ESTree.Expression | undefined => {
45
+ const definition = variable.defs.find((candidate) => candidate.type === "Variable")
46
+ const declaration = definition?.node
47
+ if (
48
+ declaration?.type !== "VariableDeclarator" ||
49
+ declaration.parent.type !== "VariableDeclaration" ||
50
+ declaration.parent.kind !== "const" ||
51
+ declaration.init === null
52
+ ) return undefined
53
+ return declaration.init
54
+ }
55
+
56
+ const staticIdentity = (
57
+ context: Context,
58
+ node: ESTree.Expression,
59
+ bindings: MachineBindings,
60
+ seen: Set<string> = new Set()
61
+ ): Identity | undefined => {
62
+ const expression = unwrapExpression(node)
63
+ if (
64
+ expression.type === "Literal" &&
65
+ (typeof expression.value === "string" || typeof expression.value === "number")
66
+ ) {
67
+ return {
68
+ key: `value:${String(expression.value)}`,
69
+ label: JSON.stringify(expression.value),
70
+ node: expression
71
+ }
72
+ }
73
+ if (expression.type === "TemplateLiteral" && expression.expressions.length === 0) {
74
+ const value = expression.quasis[0]?.value.cooked
75
+ return value === null || value === undefined
76
+ ? undefined
77
+ : { key: `value:${value}`, label: JSON.stringify(value), node: expression }
78
+ }
79
+ if (expression.type === "Identifier") {
80
+ const variable = resolvedVariable(context, expression)
81
+ if (variable === undefined || !isStableVariable(variable)) return undefined
82
+ const key = variableKey(variable)
83
+ if (key === undefined || seen.has(key)) return undefined
84
+ const initializer = constInitializer(variable)
85
+ if (initializer !== undefined) {
86
+ const nextSeen = new Set(seen)
87
+ nextSeen.add(key)
88
+ const initialized = staticIdentity(context, initializer, bindings, nextSeen)
89
+ if (initialized !== undefined) return { ...initialized, node: expression }
90
+ }
91
+ return { key, label: expression.name, node: expression }
92
+ }
93
+ if (
94
+ isMachineMemberCall(expression, "childAddress", bindings)
95
+ ) {
96
+ const argument = expression.arguments[0]
97
+ if (expression.arguments.length === 1 && argument !== undefined && argument.type !== "SpreadElement") {
98
+ return staticIdentity(context, argument, bindings, seen)
99
+ }
100
+ }
101
+ return undefined
102
+ }
103
+
104
+ const descriptorIdentity = (
105
+ context: Context,
106
+ node: ESTree.Expression,
107
+ bindings: MachineBindings,
108
+ seen: Set<string> = new Set()
109
+ ): Identity | undefined => {
110
+ const expression = unwrapExpression(node)
111
+ if (expression.type === "Identifier") {
112
+ const variable = resolvedVariable(context, expression)
113
+ if (variable === undefined || !isStableVariable(variable)) return undefined
114
+ const key = variableKey(variable)
115
+ if (key === undefined || seen.has(key)) return undefined
116
+ const initializer = constInitializer(variable)
117
+ if (initializer !== undefined) {
118
+ const nextSeen = new Set(seen)
119
+ nextSeen.add(key)
120
+ const initialized = descriptorIdentity(context, initializer, bindings, nextSeen)
121
+ if (initialized !== undefined) return { ...initialized, node: expression }
122
+ }
123
+ return { key: `descriptor:${key}`, label: expression.name, node: expression }
124
+ }
125
+ if (
126
+ isMachineMemberCall(expression, "child", bindings)
127
+ ) {
128
+ const argument = expression.arguments[0]
129
+ if (argument !== undefined && argument.type !== "SpreadElement") {
130
+ return staticIdentity(context, argument, bindings)
131
+ }
132
+ }
133
+ return undefined
134
+ }
135
+
136
+ const objectProperty = (
137
+ object: ESTree.ObjectExpression,
138
+ name: string
139
+ ): ESTree.Expression | undefined => {
140
+ for (const property of object.properties) {
141
+ if (property.type !== "Property" || property.kind !== "init") continue
142
+ const propertyName = property.computed
143
+ ? property.key.type === "Literal" && typeof property.key.value === "string"
144
+ ? property.key.value
145
+ : undefined
146
+ : property.key.type === "Identifier" || property.key.type === "Literal"
147
+ ? String(property.key.type === "Identifier" ? property.key.name : property.key.value)
148
+ : undefined
149
+ if (propertyName === name) return property.value
150
+ }
151
+ return undefined
152
+ }
153
+
154
+ const resolvedObject = (
155
+ context: Context,
156
+ node: ESTree.Expression,
157
+ seen: Set<string> = new Set()
158
+ ): ESTree.ObjectExpression | undefined => {
159
+ const expression = unwrapExpression(node)
160
+ if (expression.type === "ObjectExpression") return expression
161
+ if (expression.type !== "Identifier") return undefined
162
+ const variable = resolvedVariable(context, expression)
163
+ if (variable === undefined) return undefined
164
+ const key = variableKey(variable)
165
+ if (key === undefined || seen.has(key)) return undefined
166
+ const initializer = constInitializer(variable)
167
+ if (initializer === undefined) return undefined
168
+ const nextSeen = new Set(seen)
169
+ nextSeen.add(key)
170
+ return resolvedObject(context, initializer, nextSeen)
171
+ }
172
+
173
+ const sourceCall = (
174
+ node: ESTree.Expression
175
+ ): ESTree.CallExpression | undefined => {
176
+ let expression = unwrapExpression(node)
177
+ while (
178
+ expression.type === "CallExpression" &&
179
+ expression.callee.type === "MemberExpression" &&
180
+ completionMethods.has(staticMemberName(expression.callee) ?? "")
181
+ ) expression = unwrapExpression(expression.callee.object)
182
+ return expression.type === "CallExpression" ? expression : undefined
183
+ }
184
+
185
+ const invocationIdentity = (
186
+ context: Context,
187
+ node: ESTree.Expression,
188
+ from: string,
189
+ bindings: MachineBindings
190
+ ): InvocationIdentity | undefined => {
191
+ const call = sourceCall(node)
192
+ if (
193
+ call?.callee.type !== "MemberExpression" ||
194
+ call.callee.object.type !== "Identifier" ||
195
+ call.callee.object.name !== from
196
+ ) return undefined
197
+ const method = staticMemberName(call.callee)
198
+ if (method === "child") {
199
+ const child = call.arguments[0]
200
+ if (child === undefined || child.type === "SpreadElement") return undefined
201
+ const identity = descriptorIdentity(context, child, bindings)
202
+ return identity === undefined ? undefined : { lifecycle: identity, address: identity }
203
+ }
204
+ if (method === undefined || !sourceMethods.has(method)) return undefined
205
+ const id = call.arguments[0]
206
+ if (id === undefined || id.type === "SpreadElement") return undefined
207
+ const lifecycle = staticIdentity(context, id, bindings)
208
+ if (lifecycle === undefined) return undefined
209
+ if (method !== "logic") return { lifecycle }
210
+ const options = call.arguments[1]
211
+ if (options === undefined || options.type === "SpreadElement") return { lifecycle }
212
+ const object = resolvedObject(context, options)
213
+ const addressNode = object === undefined ? undefined : objectProperty(object, "address")
214
+ const address = addressNode === undefined
215
+ ? undefined
216
+ : staticIdentity(context, addressNode, bindings)
217
+ return address === undefined ? { lifecycle } : { lifecycle, address }
218
+ }
219
+
220
+ const returnedExpression = (
221
+ node: PlanningFunction
222
+ ): ESTree.Expression | undefined => {
223
+ if (node.body === null) return undefined
224
+ if (node.body.type !== "BlockStatement") return node.body
225
+ const returns = node.body.body.filter((statement) => statement.type === "ReturnStatement")
226
+ if (returns.length !== 1) return undefined
227
+ return returns[0]!.argument ?? undefined
228
+ }
229
+
230
+ const returnedEntries = (
231
+ context: Context,
232
+ node: PlanningFunction
233
+ ): ReadonlyArray<ESTree.Expression> => {
234
+ const returned = returnedExpression(node)
235
+ if (returned === undefined) return []
236
+ const array = resolvedObjectOrArray(context, returned)
237
+ if (array?.type !== "ArrayExpression") return [returned]
238
+ return array.elements.filter((entry): entry is ESTree.Expression => entry !== null && entry.type !== "SpreadElement")
239
+ }
240
+
241
+ const resolvedObjectOrArray = (
242
+ context: Context,
243
+ node: ESTree.Expression,
244
+ seen: Set<string> = new Set()
245
+ ): ESTree.ObjectExpression | ESTree.ArrayExpression | undefined => {
246
+ const expression = unwrapExpression(node)
247
+ if (expression.type === "ObjectExpression" || expression.type === "ArrayExpression") return expression
248
+ if (expression.type !== "Identifier") return undefined
249
+ const variable = resolvedVariable(context, expression)
250
+ if (variable === undefined) return undefined
251
+ const key = variableKey(variable)
252
+ if (key === undefined || seen.has(key)) return undefined
253
+ const initializer = constInitializer(variable)
254
+ if (initializer === undefined) return undefined
255
+ const nextSeen = new Set(seen)
256
+ nextSeen.add(key)
257
+ return resolvedObjectOrArray(context, initializer, nextSeen)
258
+ }
259
+
260
+ const resolvedEntry = (
261
+ context: Context,
262
+ node: ESTree.Expression
263
+ ): ESTree.Expression => {
264
+ const expression = unwrapExpression(node)
265
+ if (expression.type !== "Identifier") return expression
266
+ const variable = resolvedVariable(context, expression)
267
+ const initializer = variable === undefined ? undefined : constInitializer(variable)
268
+ return initializer === undefined ? expression : unwrapExpression(initializer)
269
+ }
270
+
271
+ export const noConflictingInvocationIdentity: Rule = {
272
+ meta: {
273
+ type: "problem",
274
+ docs: {
275
+ description: "Require unique invocation lifecycle IDs and runtime addresses within a state.",
276
+ recommended: true
277
+ },
278
+ schema: [],
279
+ messages: {
280
+ conflictingAddress:
281
+ "Invocation runtime address {{identity}} is reused in this state. Concurrent children cannot own the same address. Give each from.logic(...) invocation a distinct Machine.childAddress(...), or put sequential work in separate states.",
282
+ conflictingBoth:
283
+ "Invocation identity {{identity}} is reused as both lifecycle ID and runtime address in this state. Outcomes become ambiguous and overlapping starts fail. Give each invocation a unique ID/address, or put sequential work in separate states.",
284
+ conflictingLifecycle:
285
+ "Invocation lifecycle ID {{identity}} is reused in this state. Outcomes are routed by state path and ID, so duplicate IDs are ambiguous and overlapping starts fail. Give each invocation a unique ID, or put sequential work in separate states."
286
+ }
287
+ },
288
+ create(context) {
289
+ const bindings = makeMachineBindings()
290
+ const inspect = (node: PlanningFunction): void => {
291
+ if (!hasMachineImport(bindings) || !isInvokePlanningCallback(node, bindings)) return
292
+ const parameter = node.params[0]
293
+ if (parameter?.type !== "Identifier") return
294
+ const lifecycle = new Map<string, number>()
295
+ const addresses = new Map<string, number>()
296
+ const entries = returnedEntries(context, node)
297
+ entries.forEach((entry, index) => {
298
+ const identity = invocationIdentity(context, resolvedEntry(context, entry), parameter.name, bindings)
299
+ if (identity === undefined) return
300
+ const lifecycleConflict = lifecycle.get(identity.lifecycle.key)
301
+ const addressConflict = identity.address === undefined
302
+ ? undefined
303
+ : addresses.get(identity.address.key)
304
+ if (
305
+ lifecycleConflict !== undefined &&
306
+ addressConflict !== undefined &&
307
+ lifecycleConflict === addressConflict
308
+ ) {
309
+ context.report({
310
+ node: identity.lifecycle.node,
311
+ messageId: "conflictingBoth",
312
+ data: { identity: identity.lifecycle.label }
313
+ })
314
+ } else {
315
+ if (lifecycleConflict !== undefined) {
316
+ context.report({
317
+ node: identity.lifecycle.node,
318
+ messageId: "conflictingLifecycle",
319
+ data: { identity: identity.lifecycle.label }
320
+ })
321
+ }
322
+ if (addressConflict !== undefined && identity.address !== undefined) {
323
+ context.report({
324
+ node: identity.address.node,
325
+ messageId: "conflictingAddress",
326
+ data: { identity: identity.address.label }
327
+ })
328
+ }
329
+ }
330
+ if (!lifecycle.has(identity.lifecycle.key)) lifecycle.set(identity.lifecycle.key, index)
331
+ if (identity.address !== undefined && !addresses.has(identity.address.key)) {
332
+ addresses.set(identity.address.key, index)
333
+ }
334
+ })
335
+ }
336
+ return {
337
+ ImportDeclaration: (node) => recordMachineImport(bindings, node),
338
+ VariableDeclarator: (node) => recordMachineDefinition(bindings, node),
339
+ ArrowFunctionExpression: inspect,
340
+ FunctionExpression: inspect
341
+ }
342
+ }
343
+ }
@@ -0,0 +1,39 @@
1
+ import type { ESTree, Rule } from "@oxlint/plugins"
2
+ import { nondeterministicOperation, nondeterministicProperty } from "../ambient.js"
3
+ import { hasMachineImport, makeMachineBindings, recordMachineDefinition, recordMachineImport } from "../imports.js"
4
+ import { enclosingPlanningCallback } from "../planning.js"
5
+
6
+ export const noNondeterministicPlanning: Rule = {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description: "Keep ambient time and randomness out of Effect Machine planning.",
11
+ recommended: true
12
+ },
13
+ schema: [],
14
+ messages: {
15
+ nondeterministic:
16
+ "{{operation}} produces a different result without a machine event or state change. Pass the value through machine input or an event, or produce it in a state-owned invocation and transition from its outcome."
17
+ }
18
+ },
19
+ create(context) {
20
+ const bindings = makeMachineBindings()
21
+ const report = (
22
+ node: ESTree.CallExpression | ESTree.MemberExpression | ESTree.NewExpression,
23
+ operation: string | undefined
24
+ ): void => {
25
+ if (
26
+ operation !== undefined &&
27
+ hasMachineImport(bindings) &&
28
+ enclosingPlanningCallback(node, bindings) !== undefined
29
+ ) context.report({ node, messageId: "nondeterministic", data: { operation } })
30
+ }
31
+ return {
32
+ ImportDeclaration: (node) => recordMachineImport(bindings, node),
33
+ CallExpression: (node) => report(node, nondeterministicOperation(context, node)),
34
+ MemberExpression: (node) => report(node, nondeterministicProperty(context, node)),
35
+ NewExpression: (node) => report(node, nondeterministicOperation(context, node)),
36
+ VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
37
+ }
38
+ }
39
+ }
@@ -49,6 +49,24 @@ const isDefaultTargetConstruction = (
49
49
  node.callee.object.type === "Identifier" &&
50
50
  node.callee.object.name === binding
51
51
 
52
+ const isEmptyResolver = (
53
+ node: ESTree.ArrowFunctionExpression | ESTree.Function
54
+ ): boolean => node.body?.type === "BlockStatement" && node.body.body.length === 0
55
+
56
+ const isTargetlessReceiver = (node: ESTree.Expression): boolean =>
57
+ node.type === "MemberExpression" && staticMemberName(node) === "none"
58
+
59
+ const isReenterOnlyOptions = (node: ESTree.Expression | undefined): boolean => {
60
+ if (node?.type !== "ObjectExpression" || node.properties.length !== 1) return false
61
+ const property = node.properties[0]
62
+ return property?.type === "Property" &&
63
+ !property.computed &&
64
+ property.key.type === "Identifier" &&
65
+ property.key.name === "reenter" &&
66
+ property.value.type === "Literal" &&
67
+ property.value.value === true
68
+ }
69
+
52
70
  export const noRedundantResolve: Rule = {
53
71
  meta: {
54
72
  type: "suggestion",
@@ -59,7 +77,12 @@ export const noRedundantResolve: Rule = {
59
77
  fixable: "code",
60
78
  schema: [],
61
79
  messages: {
62
- redundantResolver: "Remove this resolver. The selected target already applies default construction."
80
+ redundantResolver:
81
+ "Remove this resolver. The selected target already applies default construction, so use the target selector directly.",
82
+ redundantReenterResolver:
83
+ "Replace this resolver with .reenter(). It applies the same default construction while explicitly reentering the selected state.",
84
+ redundantTargetlessResolver:
85
+ "Remove this empty resolver. A targetless transition performs the same work as to.none; use to.none directly."
63
86
  }
64
87
  },
65
88
  create(context) {
@@ -69,7 +92,7 @@ export const noRedundantResolve: Rule = {
69
92
  CallExpression(node) {
70
93
  if (
71
94
  !hasMachineImport(bindings) ||
72
- node.arguments.length !== 1 ||
95
+ (node.arguments.length !== 1 && node.arguments.length !== 2) ||
73
96
  node.callee.type !== "MemberExpression" ||
74
97
  staticMemberName(node.callee) !== "resolve"
75
98
  ) return
@@ -82,22 +105,31 @@ export const noRedundantResolve: Rule = {
82
105
  if (callback.async || callback.generator) return
83
106
  if (!isPlanningCallback(callback, bindings)) return
84
107
 
108
+ const receiver = node.callee.object
85
109
  const binding = targetBinding(callback)
86
- if (
87
- binding === undefined ||
88
- !isDefaultTargetConstruction(returnedExpression(callback), binding)
89
- ) return
110
+ const defaultConstruction = binding !== undefined &&
111
+ isDefaultTargetConstruction(returnedExpression(callback), binding)
112
+ const targetless = isTargetlessReceiver(receiver) && isEmptyResolver(callback)
113
+ if (!defaultConstruction && !targetless) return
90
114
 
91
- const receiver = node.callee.object
92
- if (context.sourceCode.getCommentsInside(node).length === 0) {
93
- context.report({
94
- node,
95
- messageId: "redundantResolver",
96
- fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(receiver))
97
- })
98
- } else {
99
- context.report({ node, messageId: "redundantResolver" })
100
- }
115
+ const options = node.arguments[1]
116
+ if (options?.type === "SpreadElement") return
117
+ const reenter = options === undefined ? false : isReenterOnlyOptions(options)
118
+ if (options !== undefined && !reenter) return
119
+
120
+ const messageId = reenter
121
+ ? "redundantReenterResolver"
122
+ : targetless
123
+ ? "redundantTargetlessResolver"
124
+ : "redundantResolver"
125
+ const replacement = `${context.sourceCode.getText(receiver)}${reenter ? ".reenter()" : ""}`
126
+ context.report({
127
+ node,
128
+ messageId,
129
+ ...(context.sourceCode.getCommentsInside(node).length === 0
130
+ ? { fix: (fixer) => fixer.replaceText(node, replacement) }
131
+ : undefined)
132
+ })
101
133
  },
102
134
  VariableDeclarator: (node) => recordMachineDefinition(bindings, node)
103
135
  }
@@ -1,6 +1,9 @@
1
1
  /** Rules recommended for every Effect Machine model. */
2
2
  export const recommended = {
3
3
  "effect-machine/no-async-planning-callback": "error",
4
+ "effect-machine/no-browser-api-in-planning": "error",
5
+ "effect-machine/no-conflicting-invocation-identity": "error",
6
+ "effect-machine/no-nondeterministic-planning": "error",
4
7
  "effect-machine/no-redundant-resolve": "error",
5
8
  "effect-machine/prefer-inline-handle": "error"
6
9
  } as const