@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/observability.mjs CHANGED
@@ -1,275 +1,275 @@
1
- /**
2
- * The rules that hold `observability.md`.
3
- *
4
- * Two rules and one config line, covering the three ways a log escapes the pipeline: the framework's
5
- * own logger, `console`, and a name fused with its data. The third is the one worth a rule of its
6
- * own, because it is the only one that still LOOKS like structured logging - the call goes through
7
- * the right service and produces an unqueryable line anyway.
8
- *
9
- * `no-console` is not reimplemented here: the standard rule already does it exactly, and shipping a
10
- * second implementation of a rule everybody has would be a maintenance cost with no gain. It is
11
- * named in `recommended` so a consuming repository switches it on with the others.
12
- *
13
- * OBSERVABILITY-4 and -5 are judgements a rule cannot make. Whether a log records a decision or
14
- * merely an arrival needs to know what the code is FOR, and no parser knows that.
15
- */
16
-
17
- /** Log methods the house service exposes. */
18
- const LOG_METHODS = new Set(["log", "error", "warn", "info", "debug", "verbose"])
19
-
20
- /** The house logging service, by the name it is injected under. */
21
- const LOGGER_RECEIVER = "winstonService"
22
-
23
- /** The framework logger this replaces. */
24
- const FRAMEWORK_LOGGER = "Logger"
25
-
26
- /** The package the framework logger comes from. */
27
- const FRAMEWORK_PACKAGE = "@nestjs/common"
28
-
29
- /** Whether a call's receiver is the house logging service, injected or via `this`. */
30
- const isLoggerReceiver = (node) => {
31
- if (node.type === "Identifier") return node.name === LOGGER_RECEIVER
32
- if (node.type === "MemberExpression" && !node.computed) return node.property.name === LOGGER_RECEIVER
33
- return false
34
- }
35
-
36
- // -- OBSERVABILITY-1 -------------------------------------------------------------------------------
37
-
38
- /** Logs leave through the house service, never the framework's own logger. */
39
- export const noFrameworkLogger = {
40
- meta: {
41
- type: "problem",
42
- docs: { description: "Only the house logging service logs; the framework's `Logger` is refused." },
43
- schema: [],
44
- messages: {
45
- imported:
46
- "`Logger` from `{{pkg}}` bypasses the correlation id and the transport configuration the house service wires up - the line is written in the right SHAPE and still arrives without the context every other line carries, or does not arrive at all. Inject the house logging service.",
47
- constructed:
48
- "`new Logger(...)` - same bypass, reached through a local construction rather than the import. Inject the house logging service.",
49
- },
50
- },
51
- create(context) {
52
- return {
53
- ImportDeclaration(node) {
54
- if (node.source.value !== FRAMEWORK_PACKAGE) return
55
- for (const specifier of node.specifiers || []) {
56
- if (specifier.type !== "ImportSpecifier") continue
57
- if (specifier.imported && specifier.imported.name === FRAMEWORK_LOGGER) {
58
- context.report({ node: specifier, messageId: "imported", data: { pkg: FRAMEWORK_PACKAGE } })
59
- }
60
- }
61
- },
62
- // caught separately so an aliased import cannot walk past the check above
63
- NewExpression(node) {
64
- if (node.callee.type === "Identifier" && node.callee.name === FRAMEWORK_LOGGER) {
65
- context.report({ node, messageId: "constructed" })
66
- }
67
- },
68
- }
69
- },
70
- }
71
-
72
- // -- OBSERVABILITY-2 -------------------------------------------------------------------------------
73
-
74
- /** The event name is a member of a closed set, never a string built at the call site. */
75
- export const noInterpolatedLogMessage = {
76
- meta: {
77
- type: "problem",
78
- docs: { description: "A log call's first argument is an enum member, not a built string." },
79
- schema: [],
80
- messages: {
81
- built:
82
- "The first argument to `{{method}}(...)` names WHAT happened and must come from the log-name enum. A string built here fuses the name with the data, so in one move the name stops being groupable and the data stops being queryable - and the day somebody rewords it, every dashboard built on it goes quiet. Pass the enum member, and put the variable part in the data object beside it.",
83
- },
84
- },
85
- create(context) {
86
- return {
87
- CallExpression(node) {
88
- const callee = node.callee
89
- if (callee.type !== "MemberExpression" || callee.computed) return
90
- const method = callee.property.name
91
- if (!LOG_METHODS.has(method)) return
92
- if (!isLoggerReceiver(callee.object)) return
93
-
94
- const first = node.arguments[0]
95
- if (!first) return
96
- const built = first.type === "TemplateLiteral"
97
- || (first.type === "BinaryExpression" && first.operator === "+")
98
- || (first.type === "Literal" && typeof first.value === "string")
99
- if (built) context.report({ node: first, messageId: "built", data: { method } })
100
- },
101
- }
102
- },
103
- }
104
-
105
- // -- OBSERVABILITY-5 -------------------------------------------------------------------------------
106
-
107
- /** Whether `node` is a bare reference to the caught error, by name. */
108
- const isIdentifierNamed = (node, name) => Boolean(node) && node.type === "Identifier" && node.name === name
109
-
110
- /** How each wording shape reads back to the author, keyed by the `kind` pushed in `scanErrorUsage`. */
111
- const WORDING_SHAPES = {
112
- message: (name) => `\`${name}.message\``,
113
- stringWrap: (name) => `\`String(${name})\``,
114
- template: (name) => `a template literal that interpolates \`${name}\` directly`,
115
- }
116
-
117
- /**
118
- * Walk a log call's data arguments for two things beside the caught error: a reference that carries
119
- * only its rendered WORDING (`.message`, `String(error)`, `${error}` inside a template), and a
120
- * reference that carries its IDENTITY (`.code`). The walk is a shallow recursion over the expression
121
- * shapes a data object is normally built from - object properties, array elements, template
122
- * expressions, ternary branches, boolean joins. It does not need a type checker: these three wording
123
- * shapes and the one identity shape are syntactic, not semantic, which is exactly the boundary
124
- * OBSERVABILITY-5 draws between what a parser can hold and what needs a reader.
125
- */
126
- const scanErrorUsage = (node, errorName, found) => {
127
- if (!node || typeof node !== "object" || typeof node.type !== "string") return
128
- switch (node.type) {
129
- case "MemberExpression":
130
- if (!node.computed && isIdentifierNamed(node.object, errorName) && node.property.type === "Identifier") {
131
- if (node.property.name === "message") found.wording.push({ node, kind: "message" })
132
- if (node.property.name === "code") found.identity.push(node)
133
- }
134
- scanErrorUsage(node.object, errorName, found)
135
- break
136
- case "CallExpression":
137
- if (
138
- node.callee.type === "Identifier" && node.callee.name === "String"
139
- && node.arguments.length === 1 && isIdentifierNamed(node.arguments[0], errorName)
140
- ) {
141
- found.wording.push({ node, kind: "stringWrap" })
142
- }
143
- for (const arg of node.arguments) scanErrorUsage(arg, errorName, found)
144
- break
145
- case "TemplateLiteral":
146
- for (const expr of node.expressions) {
147
- if (isIdentifierNamed(expr, errorName)) found.wording.push({ node: expr, kind: "template" })
148
- else scanErrorUsage(expr, errorName, found)
149
- }
150
- break
151
- case "ObjectExpression":
152
- for (const prop of node.properties) scanErrorUsage(prop, errorName, found)
153
- break
154
- case "Property":
155
- scanErrorUsage(node.value, errorName, found)
156
- break
157
- case "ArrayExpression":
158
- for (const el of node.elements) scanErrorUsage(el, errorName, found)
159
- break
160
- case "ConditionalExpression":
161
- scanErrorUsage(node.consequent, errorName, found)
162
- scanErrorUsage(node.alternate, errorName, found)
163
- break
164
- case "LogicalExpression":
165
- case "BinaryExpression":
166
- scanErrorUsage(node.left, errorName, found)
167
- scanErrorUsage(node.right, errorName, found)
168
- break
169
- default:
170
- break
171
- }
172
- }
173
-
174
- /**
175
- * A failure log's data carries the exception's `code`, not only its rendered message. Scoped to log
176
- * calls sitting inside a `catch`, and to references to that `catch`'s OWN error identifier - a field
177
- * that happens to be named `error` elsewhere is not this rule's business, and neither is a call
178
- * outside a `catch`, since OBSERVABILITY-5 only binds where the line is what an alert groups by.
179
- */
180
- export const noErrorWordingAsLogIdentity = {
181
- meta: {
182
- type: "problem",
183
- docs: {
184
- description:
185
- "OBSERVABILITY-5 (Rule 5): a failure log's data carries the exception's `code`, so improving the wording never splits an alert in two.",
186
- },
187
- schema: [],
188
- messages: {
189
- wordingOnly:
190
- "This failure log carries {{shape}} but never `{{errorName}}.code`. Wording is what a person reads; code is what a dashboard groups by. Fuse the two and the alert lives or dies on how the sentence happens to be phrased today - reword it tomorrow for clarity and the old alert goes silent while a new one opens under the reworded text. Add `{{errorName}}.code` (and any other identity metadata) to the data, and keep the message only as a secondary field beside it.",
191
- },
192
- },
193
- create(context) {
194
- const catchStack = []
195
- return {
196
- CatchClause(node) {
197
- catchStack.push(node.param && node.param.type === "Identifier" ? node.param.name : null)
198
- },
199
- "CatchClause:exit"() {
200
- catchStack.pop()
201
- },
202
- CallExpression(node) {
203
- const errorName = catchStack[catchStack.length - 1]
204
- if (!errorName) return
205
- const callee = node.callee
206
- if (callee.type !== "MemberExpression" || callee.computed) return
207
- const method = callee.property.name
208
- if (!LOG_METHODS.has(method)) return
209
- if (!isLoggerReceiver(callee.object)) return
210
-
211
- // the event name (first argument) is OBSERVABILITY-2's business, not this rule's
212
- const dataArgs = node.arguments.slice(1)
213
- if (dataArgs.length === 0) return
214
-
215
- const found = { wording: [], identity: [] }
216
- for (const arg of dataArgs) scanErrorUsage(arg, errorName, found)
217
- if (found.wording.length > 0 && found.identity.length === 0) {
218
- const first = found.wording[0]
219
- context.report({
220
- node: first.node,
221
- messageId: "wordingOnly",
222
- data: { errorName, shape: WORDING_SHAPES[first.kind](errorName) },
223
- })
224
- }
225
- },
226
- }
227
- },
228
- }
229
-
230
- /** The rules this law contributes to the plugin. */
231
- export const rules = {
232
- "no-framework-logger": noFrameworkLogger,
233
- "no-interpolated-log-message": noInterpolatedLogMessage,
234
- "no-error-wording-as-log-identity": noErrorWordingAsLogIdentity,
235
- }
236
-
237
- /**
238
- * The level this law asks for, as the plugin's own opinion.
239
- *
240
- * `no-framework-logger` and `no-interpolated-log-message` are both measured at zero debt in the
241
- * reference repository ONCE the sanctioned exit is scoped: `no-framework-logger` reports four sites
242
- * under the standalone-agent folder, and those are the exit rather than the debt. An agent running
243
- * outside the request lifecycle has no request to correlate and no transport configured, so the
244
- * house service would give it a dependency and nothing else. Scope that folder off in the consuming
245
- * config -- once, by path -- rather than letting per-line suppressions accumulate until nobody can
246
- * see how wide the exception has grown.
247
- *
248
- * `no-error-wording-as-log-identity` measures at 64 offenders across 39 files in the reference
249
- * repository (`src/**`, probed against source directly since the published package predates this
250
- * rule) -- real debt, not zero, so it ships at `warn` with that count on record rather than `error`.
251
- * Flip to `error` once the count reaches zero; shipping `error` over standing debt is what teaches an
252
- * author to scroll past a rule instead of fixing it.
253
- *
254
- * `no-console` is the standard rule rather than a house one, listed here so a repository switches
255
- * all three on together: it is the third way out of the pipeline, and leaving it off makes the
256
- * other two decorative.
257
- */
258
- export const recommended = {
259
- "starci-be/no-framework-logger": "error",
260
- "starci-be/no-interpolated-log-message": "error",
261
- "starci-be/no-error-wording-as-log-identity": "warn",
262
- "no-console": "error",
263
- }
264
-
265
- /**
266
- * Paths where a plain logger is sanctioned -- programs with no request to correlate.
267
- *
268
- * Exported so a consuming config and a measuring gate use the SAME list. Two copies of an exemption
269
- * is how one of them silently grows.
270
- */
271
- export const standaloneProgramGlobs = [
272
- "src/modules/playground-agent-core/**",
273
- "apps/playground-*-agent/**",
274
- "apps/cli/**",
275
- ]
1
+ /**
2
+ * The rules that hold `observability.md`.
3
+ *
4
+ * Two rules and one config line, covering the three ways a log escapes the pipeline: the framework's
5
+ * own logger, `console`, and a name fused with its data. The third is the one worth a rule of its
6
+ * own, because it is the only one that still LOOKS like structured logging - the call goes through
7
+ * the right service and produces an unqueryable line anyway.
8
+ *
9
+ * `no-console` is not reimplemented here: the standard rule already does it exactly, and shipping a
10
+ * second implementation of a rule everybody has would be a maintenance cost with no gain. It is
11
+ * named in `recommended` so a consuming repository switches it on with the others.
12
+ *
13
+ * OBSERVABILITY-4 and -5 are judgements a rule cannot make. Whether a log records a decision or
14
+ * merely an arrival needs to know what the code is FOR, and no parser knows that.
15
+ */
16
+
17
+ /** Log methods the house service exposes. */
18
+ const LOG_METHODS = new Set(["log", "error", "warn", "info", "debug", "verbose"])
19
+
20
+ /** The house logging service, by the name it is injected under. */
21
+ const LOGGER_RECEIVER = "winstonService"
22
+
23
+ /** The framework logger this replaces. */
24
+ const FRAMEWORK_LOGGER = "Logger"
25
+
26
+ /** The package the framework logger comes from. */
27
+ const FRAMEWORK_PACKAGE = "@nestjs/common"
28
+
29
+ /** Whether a call's receiver is the house logging service, injected or via `this`. */
30
+ const isLoggerReceiver = (node) => {
31
+ if (node.type === "Identifier") return node.name === LOGGER_RECEIVER
32
+ if (node.type === "MemberExpression" && !node.computed) return node.property.name === LOGGER_RECEIVER
33
+ return false
34
+ }
35
+
36
+ // -- OBSERVABILITY-1 -------------------------------------------------------------------------------
37
+
38
+ /** Logs leave through the house service, never the framework's own logger. */
39
+ export const noFrameworkLogger = {
40
+ meta: {
41
+ type: "problem",
42
+ docs: { description: "Only the house logging service logs; the framework's `Logger` is refused." },
43
+ schema: [],
44
+ messages: {
45
+ imported:
46
+ "`Logger` from `{{pkg}}` bypasses the correlation id and the transport configuration the house service wires up - the line is written in the right SHAPE and still arrives without the context every other line carries, or does not arrive at all. Inject the house logging service.",
47
+ constructed:
48
+ "`new Logger(...)` - same bypass, reached through a local construction rather than the import. Inject the house logging service.",
49
+ },
50
+ },
51
+ create(context) {
52
+ return {
53
+ ImportDeclaration(node) {
54
+ if (node.source.value !== FRAMEWORK_PACKAGE) return
55
+ for (const specifier of node.specifiers || []) {
56
+ if (specifier.type !== "ImportSpecifier") continue
57
+ if (specifier.imported && specifier.imported.name === FRAMEWORK_LOGGER) {
58
+ context.report({ node: specifier, messageId: "imported", data: { pkg: FRAMEWORK_PACKAGE } })
59
+ }
60
+ }
61
+ },
62
+ // caught separately so an aliased import cannot walk past the check above
63
+ NewExpression(node) {
64
+ if (node.callee.type === "Identifier" && node.callee.name === FRAMEWORK_LOGGER) {
65
+ context.report({ node, messageId: "constructed" })
66
+ }
67
+ },
68
+ }
69
+ },
70
+ }
71
+
72
+ // -- OBSERVABILITY-2 -------------------------------------------------------------------------------
73
+
74
+ /** The event name is a member of a closed set, never a string built at the call site. */
75
+ export const noInterpolatedLogMessage = {
76
+ meta: {
77
+ type: "problem",
78
+ docs: { description: "A log call's first argument is an enum member, not a built string." },
79
+ schema: [],
80
+ messages: {
81
+ built:
82
+ "The first argument to `{{method}}(...)` names WHAT happened and must come from the log-name enum. A string built here fuses the name with the data, so in one move the name stops being groupable and the data stops being queryable - and the day somebody rewords it, every dashboard built on it goes quiet. Pass the enum member, and put the variable part in the data object beside it.",
83
+ },
84
+ },
85
+ create(context) {
86
+ return {
87
+ CallExpression(node) {
88
+ const callee = node.callee
89
+ if (callee.type !== "MemberExpression" || callee.computed) return
90
+ const method = callee.property.name
91
+ if (!LOG_METHODS.has(method)) return
92
+ if (!isLoggerReceiver(callee.object)) return
93
+
94
+ const first = node.arguments[0]
95
+ if (!first) return
96
+ const built = first.type === "TemplateLiteral"
97
+ || (first.type === "BinaryExpression" && first.operator === "+")
98
+ || (first.type === "Literal" && typeof first.value === "string")
99
+ if (built) context.report({ node: first, messageId: "built", data: { method } })
100
+ },
101
+ }
102
+ },
103
+ }
104
+
105
+ // -- OBSERVABILITY-5 -------------------------------------------------------------------------------
106
+
107
+ /** Whether `node` is a bare reference to the caught error, by name. */
108
+ const isIdentifierNamed = (node, name) => Boolean(node) && node.type === "Identifier" && node.name === name
109
+
110
+ /** How each wording shape reads back to the author, keyed by the `kind` pushed in `scanErrorUsage`. */
111
+ const WORDING_SHAPES = {
112
+ message: (name) => `\`${name}.message\``,
113
+ stringWrap: (name) => `\`String(${name})\``,
114
+ template: (name) => `a template literal that interpolates \`${name}\` directly`,
115
+ }
116
+
117
+ /**
118
+ * Walk a log call's data arguments for two things beside the caught error: a reference that carries
119
+ * only its rendered WORDING (`.message`, `String(error)`, `${error}` inside a template), and a
120
+ * reference that carries its IDENTITY (`.code`). The walk is a shallow recursion over the expression
121
+ * shapes a data object is normally built from - object properties, array elements, template
122
+ * expressions, ternary branches, boolean joins. It does not need a type checker: these three wording
123
+ * shapes and the one identity shape are syntactic, not semantic, which is exactly the boundary
124
+ * OBSERVABILITY-5 draws between what a parser can hold and what needs a reader.
125
+ */
126
+ const scanErrorUsage = (node, errorName, found) => {
127
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return
128
+ switch (node.type) {
129
+ case "MemberExpression":
130
+ if (!node.computed && isIdentifierNamed(node.object, errorName) && node.property.type === "Identifier") {
131
+ if (node.property.name === "message") found.wording.push({ node, kind: "message" })
132
+ if (node.property.name === "code") found.identity.push(node)
133
+ }
134
+ scanErrorUsage(node.object, errorName, found)
135
+ break
136
+ case "CallExpression":
137
+ if (
138
+ node.callee.type === "Identifier" && node.callee.name === "String"
139
+ && node.arguments.length === 1 && isIdentifierNamed(node.arguments[0], errorName)
140
+ ) {
141
+ found.wording.push({ node, kind: "stringWrap" })
142
+ }
143
+ for (const arg of node.arguments) scanErrorUsage(arg, errorName, found)
144
+ break
145
+ case "TemplateLiteral":
146
+ for (const expr of node.expressions) {
147
+ if (isIdentifierNamed(expr, errorName)) found.wording.push({ node: expr, kind: "template" })
148
+ else scanErrorUsage(expr, errorName, found)
149
+ }
150
+ break
151
+ case "ObjectExpression":
152
+ for (const prop of node.properties) scanErrorUsage(prop, errorName, found)
153
+ break
154
+ case "Property":
155
+ scanErrorUsage(node.value, errorName, found)
156
+ break
157
+ case "ArrayExpression":
158
+ for (const el of node.elements) scanErrorUsage(el, errorName, found)
159
+ break
160
+ case "ConditionalExpression":
161
+ scanErrorUsage(node.consequent, errorName, found)
162
+ scanErrorUsage(node.alternate, errorName, found)
163
+ break
164
+ case "LogicalExpression":
165
+ case "BinaryExpression":
166
+ scanErrorUsage(node.left, errorName, found)
167
+ scanErrorUsage(node.right, errorName, found)
168
+ break
169
+ default:
170
+ break
171
+ }
172
+ }
173
+
174
+ /**
175
+ * A failure log's data carries the exception's `code`, not only its rendered message. Scoped to log
176
+ * calls sitting inside a `catch`, and to references to that `catch`'s OWN error identifier - a field
177
+ * that happens to be named `error` elsewhere is not this rule's business, and neither is a call
178
+ * outside a `catch`, since OBSERVABILITY-5 only binds where the line is what an alert groups by.
179
+ */
180
+ export const noErrorWordingAsLogIdentity = {
181
+ meta: {
182
+ type: "problem",
183
+ docs: {
184
+ description:
185
+ "OBSERVABILITY-5 (Rule 5): a failure log's data carries the exception's `code`, so improving the wording never splits an alert in two.",
186
+ },
187
+ schema: [],
188
+ messages: {
189
+ wordingOnly:
190
+ "This failure log carries {{shape}} but never `{{errorName}}.code`. Wording is what a person reads; code is what a dashboard groups by. Fuse the two and the alert lives or dies on how the sentence happens to be phrased today - reword it tomorrow for clarity and the old alert goes silent while a new one opens under the reworded text. Add `{{errorName}}.code` (and any other identity metadata) to the data, and keep the message only as a secondary field beside it.",
191
+ },
192
+ },
193
+ create(context) {
194
+ const catchStack = []
195
+ return {
196
+ CatchClause(node) {
197
+ catchStack.push(node.param && node.param.type === "Identifier" ? node.param.name : null)
198
+ },
199
+ "CatchClause:exit"() {
200
+ catchStack.pop()
201
+ },
202
+ CallExpression(node) {
203
+ const errorName = catchStack[catchStack.length - 1]
204
+ if (!errorName) return
205
+ const callee = node.callee
206
+ if (callee.type !== "MemberExpression" || callee.computed) return
207
+ const method = callee.property.name
208
+ if (!LOG_METHODS.has(method)) return
209
+ if (!isLoggerReceiver(callee.object)) return
210
+
211
+ // the event name (first argument) is OBSERVABILITY-2's business, not this rule's
212
+ const dataArgs = node.arguments.slice(1)
213
+ if (dataArgs.length === 0) return
214
+
215
+ const found = { wording: [], identity: [] }
216
+ for (const arg of dataArgs) scanErrorUsage(arg, errorName, found)
217
+ if (found.wording.length > 0 && found.identity.length === 0) {
218
+ const first = found.wording[0]
219
+ context.report({
220
+ node: first.node,
221
+ messageId: "wordingOnly",
222
+ data: { errorName, shape: WORDING_SHAPES[first.kind](errorName) },
223
+ })
224
+ }
225
+ },
226
+ }
227
+ },
228
+ }
229
+
230
+ /** The rules this law contributes to the plugin. */
231
+ export const rules = {
232
+ "no-framework-logger": noFrameworkLogger,
233
+ "no-interpolated-log-message": noInterpolatedLogMessage,
234
+ "no-error-wording-as-log-identity": noErrorWordingAsLogIdentity,
235
+ }
236
+
237
+ /**
238
+ * The level this law asks for, as the plugin's own opinion.
239
+ *
240
+ * `no-framework-logger` and `no-interpolated-log-message` are both measured at zero debt in the
241
+ * reference repository ONCE the sanctioned exit is scoped: `no-framework-logger` reports four sites
242
+ * under the standalone-agent folder, and those are the exit rather than the debt. An agent running
243
+ * outside the request lifecycle has no request to correlate and no transport configured, so the
244
+ * house service would give it a dependency and nothing else. Scope that folder off in the consuming
245
+ * config -- once, by path -- rather than letting per-line suppressions accumulate until nobody can
246
+ * see how wide the exception has grown.
247
+ *
248
+ * `no-error-wording-as-log-identity` measures at 64 offenders across 39 files in the reference
249
+ * repository (`src/**`, probed against source directly since the published package predates this
250
+ * rule) -- real debt, not zero, so it ships at `warn` with that count on record rather than `error`.
251
+ * Flip to `error` once the count reaches zero; shipping `error` over standing debt is what teaches an
252
+ * author to scroll past a rule instead of fixing it.
253
+ *
254
+ * `no-console` is the standard rule rather than a house one, listed here so a repository switches
255
+ * all three on together: it is the third way out of the pipeline, and leaving it off makes the
256
+ * other two decorative.
257
+ */
258
+ export const recommended = {
259
+ "starci-be/no-framework-logger": "error",
260
+ "starci-be/no-interpolated-log-message": "error",
261
+ "starci-be/no-error-wording-as-log-identity": "warn",
262
+ "no-console": "error",
263
+ }
264
+
265
+ /**
266
+ * Paths where a plain logger is sanctioned -- programs with no request to correlate.
267
+ *
268
+ * Exported so a consuming config and a measuring gate use the SAME list. Two copies of an exemption
269
+ * is how one of them silently grows.
270
+ */
271
+ export const standaloneProgramGlobs = [
272
+ "src/modules/playground-agent-core/**",
273
+ "apps/playground-*-agent/**",
274
+ "apps/cli/**",
275
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-be",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "The StarCi back-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -11,8 +11,10 @@
11
11
  "main": "./index.mjs",
12
12
  "files": [
13
13
  "*.mjs",
14
+ "lib/*.mjs",
14
15
  "!*.test.mjs",
15
- "README.md"
16
+ "README.md",
17
+ "CHANGELOG.md"
16
18
  ],
17
19
  "engines": {
18
20
  "node": ">=20.9.0"