@starci/eslint-canon-be 1.2.1 → 1.2.2

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/exceptions.mjs CHANGED
@@ -1,265 +1,262 @@
1
- /**
2
- * The rules that hold `exceptions.md`.
3
- *
4
- * FOUR RULES, AND THE PAIRING IS THE POINT. Two watch the throw site and two watch the declaration,
5
- * because either half alone has a hole the other closes: a class that extends a framework base is
6
- * thrown by its own house-shaped name, so a throw-site rule passes it, and one such class stayed
7
- * live and thrown from four call sites while the gate stayed green.
8
- *
9
- * `throw-abstract-exception` is a heuristic and says so: a rule reading one file at a time cannot
10
- * verify that a thrown class extends anything, so it matches `Error` and a named list of framework
11
- * exceptions. `exception-extends-abstract` is what makes that heuristic sound - it guarantees the
12
- * only `*Exception` classes in the tree are house ones.
13
- */
14
-
15
- /** Forward-slash form of a filename, so Windows paths compare like every other path. */
16
- const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
17
-
18
- /** The house base every exception extends. */
19
- const EXCEPTION_BASE = "AbstractException"
20
-
21
- /** Anything named like an exception. */
22
- const EXCEPTION_NAME = /Exception$/
23
-
24
- /** Framework exceptions that carry an HTTP status and no identity. */
25
- const FRAMEWORK_EXCEPTIONS = new Set([
26
- "BadRequestException",
27
- "NotFoundException",
28
- "UnauthorizedException",
29
- "ForbiddenException",
30
- "InternalServerErrorException",
31
- "HttpException",
32
- "ConflictException",
33
- "NotAcceptableException",
34
- "RequestTimeoutException",
35
- "GoneException",
36
- "PayloadTooLargeException",
37
- "UnsupportedMediaTypeException",
38
- "UnprocessableEntityException",
39
- "NotImplementedException",
40
- "BadGatewayException",
41
- "ServiceUnavailableException",
42
- "GatewayTimeoutException",
43
- ])
44
-
45
- /** Where every exception is declared. */
46
- const ERRORS_FOLDER = /\/exceptions\/errors\//
47
-
48
- /**
49
- * True when a file sits in an exceptions folder - ANY of them.
50
- *
51
- * THE PATH USED TO BE `/platform/exceptions/errors/`, WHICH WAS ONE REPOSITORY'S LAYOUT WRITTEN
52
- * INTO A LAW. EXCEPTION-4 asks for one place per APPLICATION to look, not for one literal path: a
53
- * repository with several apps has `apps/<name>/src/exceptions/errors/` per app and still satisfies
54
- * it, because "what can this application throw" still has a single answer.
55
- *
56
- * Measured before changing: the narrow path reported 83 findings in a second back end, and the top
57
- * offenders were files already sitting in an `exceptions/errors/` folder - just not that one. A rule
58
- * that fires on correct code is worse than no rule, because the next author learns to scroll past it.
59
- */
60
- const isInErrorsFolder = (filename) => ERRORS_FOLDER.test(normalizePath(filename))
61
-
62
- /** The base class's own file, which is the one class allowed to extend something else. */
63
- const isExceptionBaseFile = (filename) => /exceptions\/errors\/abstract\.ts$/.test(normalizePath(filename))
64
-
65
- /**
66
- * The test lanes, where `throw new Error` is a test-runner assertion rather than a domain failure.
67
- *
68
- * THE CARVE-OUT WAS ALREADY GRANTED IN PROSE AND MISSING FROM THE RULE, which is the worst of the
69
- * two places for them to disagree: canon said the lanes may throw, the artifact said they may not,
70
- * and a repository adopting the rule inherited 69 findings its own canon had already excused.
71
- *
72
- * A flow that could not fail its own setup would have to invent a domain exception for "the fixture
73
- * is missing", which is a failure of the test rather than of the business - and naming it as a
74
- * domain failure would put it in the same vocabulary the product uses to describe real ones.
75
- */
76
- const isTestLane = (filename) => {
77
- const path = normalizePath(filename)
78
- return /\.spec\.ts$/.test(path) || /-spec\.ts$/.test(path) || path.includes("/src/tests/")
79
- }
80
-
81
- /**
82
- * A liveness or readiness probe, where the HTTP STATUS is the whole contract.
83
- *
84
- * THE RULE'S OWN REASON IS WHAT GRANTS THIS. A framework exception is refused because it "carries an
85
- * HTTP status and no identity" - the status is not enough to tell two failures apart. A probe is the
86
- * one endpoint where that sentence inverts: kubernetes reads the status code and NOTHING else, never
87
- * the body, so the status is the entire identity and a domain exception carrying a rich code would
88
- * arrive as a 500 and be read as "down" for the wrong reason.
89
- *
90
- * Narrow on purpose. Only a health controller qualifies, so this cannot become the place a service
91
- * goes to avoid naming its failures.
92
- */
93
- const isHealthProbe = (filename) => /\/health(?:z)?\.controller\.ts$|\/health\//.test(normalizePath(filename))
94
-
95
- // -- EXCEPTION-1 -----------------------------------------------------------------------------------
96
-
97
- /** Throw a house exception, never `Error` and never a framework built-in. */
98
- export const throwAbstractException = {
99
- meta: {
100
- type: "problem",
101
- docs: { description: "Throw an `AbstractException` subclass, not `Error` or a framework exception." },
102
- schema: [],
103
- messages: {
104
- bareError:
105
- "`throw new Error(...)` carries a sentence and no code, so nothing downstream can group, match or retry on it without parsing English. Throw the exception class that NAMES this failure.",
106
- framework:
107
- "`throw new {{name}}(...)` carries an HTTP status and no identity - two unrelated failures arrive at a client indistinguishable, and the only thing telling them apart is the message, which is the part that gets reworded. Throw an `AbstractException` subclass.",
108
- },
109
- },
110
- create(context) {
111
- const filename = context.filename || context.getFilename()
112
- // the lanes canon already excuses - see `isTestLane`
113
- if (isTestLane(filename)) return {}
114
- const probe = isHealthProbe(filename)
115
- return {
116
- ThrowStatement(node) {
117
- const thrown = node.argument
118
- if (!thrown || thrown.type !== "NewExpression" || thrown.callee.type !== "Identifier") return
119
- const name = thrown.callee.name
120
- if (name === "Error") {
121
- context.report({ node: thrown, messageId: "bareError" })
122
- return
123
- }
124
- // a probe answers with a status code and nothing else - see `isHealthProbe`. `Error` above
125
- // is still refused here: a probe's status is its contract, an unnamed crash is not.
126
- if (probe) return
127
- if (FRAMEWORK_EXCEPTIONS.has(name)) {
128
- context.report({ node: thrown, messageId: "framework", data: { name } })
129
- }
130
- },
131
- }
132
- },
133
- }
134
-
135
- // -- EXCEPTION-2 -----------------------------------------------------------------------------------
136
-
137
- /** The constructor takes one metadata object, `{}` when there is nothing to say. */
138
- export const requireExceptionObjectArg = {
139
- meta: {
140
- type: "problem",
141
- docs: { description: "`new XException(...)` passes exactly one object-literal argument." },
142
- schema: [],
143
- messages: {
144
- zero:
145
- "`new {{name}}()` - pass the metadata object even when it is empty: `new {{name}}({})`. One spelling for every throw means a reader never has to check whether THIS exception happens to take arguments.",
146
- notObject:
147
- "`new {{name}}(...)` takes a metadata OBJECT, not a positional value. A positional shape cannot grow: the day this failure needs a second field, every throw site is edited and the ones edited wrong still compile.",
148
- extra:
149
- "`new {{name}}(...)` takes exactly ONE metadata object, not several arguments.",
150
- },
151
- },
152
- create(context) {
153
- return {
154
- ThrowStatement(node) {
155
- const thrown = node.argument
156
- if (!thrown || thrown.type !== "NewExpression" || thrown.callee.type !== "Identifier") return
157
- const name = thrown.callee.name
158
- if (name === EXCEPTION_BASE || !EXCEPTION_NAME.test(name)) return
159
- /*
160
- * A FRAMEWORK EXCEPTION IS NOT A HOUSE ONE, AND ITS SHAPE IS NOT OURS TO DICTATE.
161
- *
162
- * This rule says a house exception takes exactly one metadata object. `EXCEPTION_NAME`
163
- * matches anything ending in `Exception`, so it was also demanding that shape from Nest's
164
- * own classes - `new ServiceUnavailableException(body)` is the constructor Nest published,
165
- * and rewriting it to satisfy a house convention would change what the framework sends.
166
- *
167
- * Whether a framework exception may be thrown at all is EXCEPTION-1's question, and it
168
- * answers it. This rule only governs the ones we declare.
169
- */
170
- if (FRAMEWORK_EXCEPTIONS.has(name)) return
171
-
172
- if (thrown.arguments.length === 0) {
173
- context.report({ node: thrown, messageId: "zero", data: { name } })
174
- return
175
- }
176
- if (thrown.arguments.length > 1) {
177
- context.report({ node: thrown, messageId: "extra", data: { name } })
178
- }
179
- if (thrown.arguments[0].type !== "ObjectExpression") {
180
- context.report({ node: thrown.arguments[0], messageId: "notObject", data: { name } })
181
- }
182
- },
183
- }
184
- },
185
- }
186
-
187
- // -- EXCEPTION-3 -----------------------------------------------------------------------------------
188
-
189
- /** The class extends the house base, so the throw site tells the truth about what is thrown. */
190
- export const exceptionExtendsAbstract = {
191
- meta: {
192
- type: "problem",
193
- docs: { description: "An `*Exception` class extends `AbstractException`." },
194
- schema: [],
195
- messages: {
196
- base:
197
- "`{{name}}` extends `{{parent}}`. Every exception extends `{{expected}}` - a framework base slips past the throw-site rule, because the throw then READS as a house exception and nothing looks wrong at any call site.",
198
- },
199
- },
200
- create(context) {
201
- if (isExceptionBaseFile(context.filename || context.getFilename())) return {}
202
- return {
203
- ClassDeclaration(node) {
204
- if (!node.id || !EXCEPTION_NAME.test(node.id.name)) return
205
- const parent = node.superClass
206
- if (!parent || parent.type !== "Identifier" || parent.name === EXCEPTION_BASE) return
207
- context.report({
208
- node: parent,
209
- messageId: "base",
210
- data: { name: node.id.name, parent: parent.name, expected: EXCEPTION_BASE },
211
- })
212
- },
213
- }
214
- },
215
- }
216
-
217
- // -- EXCEPTION-4 -----------------------------------------------------------------------------------
218
-
219
- /** Every exception is declared in one folder, so the set of failures is readable in one place. */
220
- export const exceptionInErrorsFolder = {
221
- meta: {
222
- type: "problem",
223
- docs: { description: "An `*Exception` class is declared under the exceptions folder." },
224
- schema: [],
225
- messages: {
226
- place:
227
- "`{{name}}` is declared outside the exceptions folder. They all live in one place so \"what can this application throw?\" has one answer, and so a reviewer sees a new failure mode ARRIVE in a diff rather than discovering it in production.",
228
- },
229
- },
230
- create(context) {
231
- if (isInErrorsFolder(context.filename || context.getFilename())) return {}
232
- return {
233
- ClassDeclaration(node) {
234
- if (!node.id || !EXCEPTION_NAME.test(node.id.name)) return
235
- // an undecorated class with no base is a shape, not an exception - the declaration this
236
- // rule is after always extends something
237
- if (!node.superClass) return
238
- context.report({ node: node.id, messageId: "place", data: { name: node.id.name } })
239
- },
240
- }
241
- },
242
- }
243
-
244
- /** The rules this law contributes to the plugin. */
245
- export const rules = {
246
- "throw-abstract-exception": throwAbstractException,
247
- "require-exception-object-arg": requireExceptionObjectArg,
248
- "exception-extends-abstract": exceptionExtendsAbstract,
249
- "exception-in-errors-folder": exceptionInErrorsFolder,
250
- }
251
-
252
- /**
253
- * The level this law asks for, as the plugin's own opinion.
254
- *
255
- * All four measured at zero debt in the reference repository. The sanctioned exit is stated where it
256
- * applies rather than carved into the rule: the spec family and the test tree may `throw new Error`,
257
- * because there it means "the runner cannot continue" rather than naming a failure the product can
258
- * produce. A consuming repository turns `throw-abstract-exception` off for those globs.
259
- */
260
- export const recommended = {
261
- "starci-be/throw-abstract-exception": "error",
262
- "starci-be/require-exception-object-arg": "error",
263
- "starci-be/exception-extends-abstract": "error",
264
- "starci-be/exception-in-errors-folder": "error",
265
- }
1
+ /**
2
+ * The rules that hold `exceptions.md`.
3
+ *
4
+ * FOUR RULES, AND THE PAIRING IS THE POINT. Two watch the throw site and two watch the declaration,
5
+ * because either half alone has a hole the other closes: a class that extends a framework base is
6
+ * thrown by its own house-shaped name, so a throw-site rule passes it, and one such class stayed
7
+ * live and thrown from four call sites while the gate stayed green.
8
+ *
9
+ * `throw-abstract-exception` is a heuristic and says so: a rule reading one file at a time cannot
10
+ * verify that a thrown class extends anything, so it matches `Error` and a named list of framework
11
+ * exceptions. `exception-extends-abstract` is what makes that heuristic sound - it guarantees the
12
+ * only `*Exception` classes in the tree are house ones.
13
+ */
14
+
15
+ import { normalizePath } from "./lib/path.mjs"
16
+
17
+ /** The house base every exception extends. */
18
+ const EXCEPTION_BASE = "AbstractException"
19
+
20
+ /** Anything named like an exception. */
21
+ const EXCEPTION_NAME = /Exception$/
22
+
23
+ /** Framework exceptions that carry an HTTP status and no identity. */
24
+ const FRAMEWORK_EXCEPTIONS = new Set([
25
+ "BadRequestException",
26
+ "NotFoundException",
27
+ "UnauthorizedException",
28
+ "ForbiddenException",
29
+ "InternalServerErrorException",
30
+ "HttpException",
31
+ "ConflictException",
32
+ "NotAcceptableException",
33
+ "RequestTimeoutException",
34
+ "GoneException",
35
+ "PayloadTooLargeException",
36
+ "UnsupportedMediaTypeException",
37
+ "UnprocessableEntityException",
38
+ "NotImplementedException",
39
+ "BadGatewayException",
40
+ "ServiceUnavailableException",
41
+ "GatewayTimeoutException",
42
+ ])
43
+
44
+ /** A feature or module capability owns its own errors, including nested error groups. */
45
+ const ERRORS_FOLDER = /\/src\/(?:modules\/(?:domain|platform|integrations)|features)\/[^/]+\/(?:[^/]+\/)*errors\//
46
+
47
+ /**
48
+ * HFS puts domain failures with their domain capability and provider failures with their integration.
49
+ * A feature can likewise own a transport or orchestration error in its own errors folder. A central
50
+ * platform exceptions capability can still own generic errors.
51
+ */
52
+ const isInErrorsFolder = (filename) => ERRORS_FOLDER.test(normalizePath(filename))
53
+
54
+ /** The base class's own file, which is the one class allowed to extend something else. */
55
+ const isExceptionBaseFile = (filename) =>
56
+ /\/src\/modules\/(?:domain|platform|integrations)\/[^/]+\/(?:errors\/)?abstract\.ts$/.test(normalizePath(filename))
57
+
58
+ /**
59
+ * The test lanes, where `throw new Error` is a test-runner assertion rather than a domain failure.
60
+ *
61
+ * THE CARVE-OUT WAS ALREADY GRANTED IN PROSE AND MISSING FROM THE RULE, which is the worst of the
62
+ * two places for them to disagree: canon said the lanes may throw, the artifact said they may not,
63
+ * and a repository adopting the rule inherited 69 findings its own canon had already excused.
64
+ *
65
+ * A flow that could not fail its own setup would have to invent a domain exception for "the fixture
66
+ * is missing", which is a failure of the test rather than of the business - and naming it as a
67
+ * domain failure would put it in the same vocabulary the product uses to describe real ones.
68
+ */
69
+ const isTestLane = (filename) => {
70
+ const path = normalizePath(filename)
71
+ return /\.spec\.ts$/.test(path) || /-spec\.ts$/.test(path) || path.includes("/src/tests/")
72
+ }
73
+
74
+ /**
75
+ * A liveness or readiness probe, where the HTTP STATUS is the whole contract.
76
+ *
77
+ * THE RULE'S OWN REASON IS WHAT GRANTS THIS. A framework exception is refused because it "carries an
78
+ * HTTP status and no identity" - the status is not enough to tell two failures apart. A probe is the
79
+ * one endpoint where that sentence inverts: kubernetes reads the status code and NOTHING else, never
80
+ * the body, so the status is the entire identity and a domain exception carrying a rich code would
81
+ * arrive as a 500 and be read as "down" for the wrong reason.
82
+ *
83
+ * Narrow on purpose. Only a health controller qualifies, so this cannot become the place a service
84
+ * goes to avoid naming its failures.
85
+ */
86
+ const isHealthProbe = (filename) => /\/health(?:z)?\.controller\.ts$|\/health\//.test(normalizePath(filename))
87
+
88
+ // -- EXCEPTION-1 -----------------------------------------------------------------------------------
89
+
90
+ /** Throw a house exception, never `Error` and never a framework built-in. */
91
+ export const throwAbstractException = {
92
+ meta: {
93
+ type: "problem",
94
+ docs: { description: "Throw an `AbstractException` subclass, not `Error` or a framework exception." },
95
+ schema: [],
96
+ messages: {
97
+ bareError:
98
+ "`throw new Error(...)` carries a sentence and no code, so nothing downstream can group, match or retry on it without parsing English. Throw the exception class that NAMES this failure.",
99
+ framework:
100
+ "`throw new {{name}}(...)` carries an HTTP status and no identity - two unrelated failures arrive at a client indistinguishable, and the only thing telling them apart is the message, which is the part that gets reworded. Throw an `AbstractException` subclass.",
101
+ },
102
+ },
103
+ create(context) {
104
+ const filename = context.filename || context.getFilename()
105
+ // the lanes canon already excuses - see `isTestLane`
106
+ if (isTestLane(filename)) return {}
107
+ const probe = isHealthProbe(filename)
108
+ return {
109
+ ThrowStatement(node) {
110
+ const thrown = node.argument
111
+ if (!thrown || thrown.type !== "NewExpression" || thrown.callee.type !== "Identifier") return
112
+ const name = thrown.callee.name
113
+ if (name === "Error") {
114
+ context.report({ node: thrown, messageId: "bareError" })
115
+ return
116
+ }
117
+ // a probe answers with a status code and nothing else - see `isHealthProbe`. `Error` above
118
+ // is still refused here: a probe's status is its contract, an unnamed crash is not.
119
+ if (probe) return
120
+ if (FRAMEWORK_EXCEPTIONS.has(name)) {
121
+ context.report({ node: thrown, messageId: "framework", data: { name } })
122
+ }
123
+ },
124
+ }
125
+ },
126
+ }
127
+
128
+ // -- EXCEPTION-2 -----------------------------------------------------------------------------------
129
+
130
+ /** The constructor takes one metadata object, `{}` when there is nothing to say. */
131
+ export const requireExceptionObjectArg = {
132
+ meta: {
133
+ type: "problem",
134
+ docs: { description: "`new XException(...)` passes exactly one object-literal argument." },
135
+ schema: [],
136
+ messages: {
137
+ zero:
138
+ "`new {{name}}()` - pass the metadata object even when it is empty: `new {{name}}({})`. One spelling for every throw means a reader never has to check whether THIS exception happens to take arguments.",
139
+ notObject:
140
+ "`new {{name}}(...)` takes a metadata OBJECT, not a positional value. A positional shape cannot grow: the day this failure needs a second field, every throw site is edited and the ones edited wrong still compile.",
141
+ extra:
142
+ "`new {{name}}(...)` takes exactly ONE metadata object, not several arguments.",
143
+ },
144
+ },
145
+ create(context) {
146
+ return {
147
+ ThrowStatement(node) {
148
+ const thrown = node.argument
149
+ if (!thrown || thrown.type !== "NewExpression" || thrown.callee.type !== "Identifier") return
150
+ const name = thrown.callee.name
151
+ if (name === EXCEPTION_BASE || !EXCEPTION_NAME.test(name)) return
152
+ /*
153
+ * A FRAMEWORK EXCEPTION IS NOT A HOUSE ONE, AND ITS SHAPE IS NOT OURS TO DICTATE.
154
+ *
155
+ * This rule says a house exception takes exactly one metadata object. `EXCEPTION_NAME`
156
+ * matches anything ending in `Exception`, so it was also demanding that shape from Nest's
157
+ * own classes - `new ServiceUnavailableException(body)` is the constructor Nest published,
158
+ * and rewriting it to satisfy a house convention would change what the framework sends.
159
+ *
160
+ * Whether a framework exception may be thrown at all is EXCEPTION-1's question, and it
161
+ * answers it. This rule only governs the ones we declare.
162
+ */
163
+ if (FRAMEWORK_EXCEPTIONS.has(name)) return
164
+
165
+ if (thrown.arguments.length === 0) {
166
+ context.report({ node: thrown, messageId: "zero", data: { name } })
167
+ return
168
+ }
169
+ if (thrown.arguments.length > 1) {
170
+ context.report({ node: thrown, messageId: "extra", data: { name } })
171
+ }
172
+ if (thrown.arguments[0].type !== "ObjectExpression") {
173
+ context.report({ node: thrown.arguments[0], messageId: "notObject", data: { name } })
174
+ }
175
+ },
176
+ }
177
+ },
178
+ }
179
+
180
+ // -- EXCEPTION-3 -----------------------------------------------------------------------------------
181
+
182
+ /** The class extends the house base, so the throw site tells the truth about what is thrown. */
183
+ export const exceptionExtendsAbstract = {
184
+ meta: {
185
+ type: "problem",
186
+ docs: { description: "An `*Exception` class extends `AbstractException`." },
187
+ schema: [],
188
+ messages: {
189
+ base:
190
+ "`{{name}}` extends `{{parent}}`. Every exception extends `{{expected}}` - a framework base slips past the throw-site rule, because the throw then READS as a house exception and nothing looks wrong at any call site.",
191
+ },
192
+ },
193
+ create(context) {
194
+ const baseFile = isExceptionBaseFile(context.filename || context.getFilename())
195
+ return {
196
+ ClassDeclaration(node) {
197
+ if (!node.id || !EXCEPTION_NAME.test(node.id.name)) return
198
+ if (baseFile && node.id.name === EXCEPTION_BASE) return
199
+ const parent = node.superClass
200
+ if (!parent || parent.type !== "Identifier" || parent.name === EXCEPTION_BASE) return
201
+ context.report({
202
+ node: parent,
203
+ messageId: "base",
204
+ data: { name: node.id.name, parent: parent.name, expected: EXCEPTION_BASE },
205
+ })
206
+ },
207
+ }
208
+ },
209
+ }
210
+
211
+ // -- EXCEPTION-4 -----------------------------------------------------------------------------------
212
+
213
+ /** Each capability declares its failures in its own errors folder. */
214
+ export const exceptionInErrorsFolder = {
215
+ meta: {
216
+ type: "problem",
217
+ docs: { description: "An `*Exception` class is declared under its owning capability's errors folder." },
218
+ schema: [],
219
+ messages: {
220
+ place:
221
+ "`{{name}}` is declared outside its owning feature or module capability's errors folder. Put the failure under that owner's errors directory.",
222
+ },
223
+ },
224
+ create(context) {
225
+ const filename = context.filename || context.getFilename()
226
+ if (isInErrorsFolder(filename)) return {}
227
+ const baseFile = isExceptionBaseFile(filename)
228
+ return {
229
+ ClassDeclaration(node) {
230
+ if (!node.id || !EXCEPTION_NAME.test(node.id.name)) return
231
+ if (baseFile && node.id.name === EXCEPTION_BASE) return
232
+ // an undecorated class with no base is a shape, not an exception - the declaration this
233
+ // rule is after always extends something
234
+ if (!node.superClass) return
235
+ context.report({ node: node.id, messageId: "place", data: { name: node.id.name } })
236
+ },
237
+ }
238
+ },
239
+ }
240
+
241
+ /** The rules this law contributes to the plugin. */
242
+ export const rules = {
243
+ "throw-abstract-exception": throwAbstractException,
244
+ "require-exception-object-arg": requireExceptionObjectArg,
245
+ "exception-extends-abstract": exceptionExtendsAbstract,
246
+ "exception-in-errors-folder": exceptionInErrorsFolder,
247
+ }
248
+
249
+ /**
250
+ * The level this law asks for, as the plugin's own opinion.
251
+ *
252
+ * All four measured at zero debt in the reference repository. The sanctioned exit is stated where it
253
+ * applies rather than carved into the rule: the spec family and the test tree may `throw new Error`,
254
+ * because there it means "the runner cannot continue" rather than naming a failure the product can
255
+ * produce. A consuming repository turns `throw-abstract-exception` off for those globs.
256
+ */
257
+ export const recommended = {
258
+ "starci-be/throw-abstract-exception": "error",
259
+ "starci-be/require-exception-object-arg": "error",
260
+ "starci-be/exception-extends-abstract": "error",
261
+ "starci-be/exception-in-errors-folder": "error",
262
+ }