@starci/eslint-canon-be 1.2.0 → 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/transport.mjs CHANGED
@@ -1,168 +1,168 @@
1
- const normalize = (filename) => String(filename || "").replace(/\\/g, "/")
2
-
3
- /** The decorator's own name, whether it was written `@Controller` or `@Controller("x")`. */
4
- const decoratorName = (node) => {
5
- const expression = node.expression
6
- if (!expression) return null
7
- if (expression.type === "Identifier") return expression.name
8
- if (expression.type === "CallExpression" && expression.callee.type === "Identifier") {
9
- return expression.callee.name
10
- }
11
- return null
12
- }
13
-
14
- /** The route string, or "" for `@Controller()` and for a path given as an object. */
15
- const routeOf = (node) => {
16
- const expression = node.expression
17
- if (!expression || expression.type !== "CallExpression") return ""
18
- const first = expression.arguments[0]
19
- if (first && first.type === "Literal" && typeof first.value === "string") return first.value
20
- return ""
21
- }
22
-
23
- /**
24
- * Which of the four sanctioned reasons this file shows, if any.
25
- *
26
- * READ OFF THE FILE, NOT OFF A LIST. A registry of blessed routes rots the moment somebody adds a
27
- * route and forgets it; the evidence here is the same evidence a reader would use.
28
- */
29
- const reasonFor = (route, filename, text) => {
30
- const path = normalize(filename)
31
- // a probe answers when the app is degraded, before the feature layer is necessarily up
32
- if (/^healthz?$/.test(route) || /\/health(z)?[./]/.test(path)) return "probe"
33
- // something outside posts to a URL we gave it, and it will never send a GraphQL document
34
- if (/webhook/i.test(route) || /webhook/i.test(path)) return "external"
35
- // bytes, not fields: multipart in, stream out
36
- if (/FileInterceptor|FilesInterceptor|AnyFilesInterceptor|StreamableFile|@Res\s*\(|createReadStream/.test(text)) {
37
- return "bytes"
38
- }
39
- // a machine registering itself, with no user session to carry
40
- if (/^(pods|internal|agents)\//.test(route)) return "machine"
41
- // an identity that is not a user session - a platform operator or a service token
42
- if (/^api\/ops(\/|$)/.test(route) || /Operator[A-Za-z]*Guard|ServiceToken|OPS_TOKEN/.test(text)) {
43
- return "operator"
44
- }
45
- return null
46
- }
47
-
48
- /** A REST door exists only where GraphQL cannot go, and the file has to show which case it is. */
49
- export const restDoorNeedsAReason = {
50
- meta: {
51
- type: "problem",
52
- docs: { description: "A @Controller is permitted only where GraphQL cannot serve." },
53
- schema: [],
54
- messages: {
55
- unjustified:
56
- "This REST door shows none of the four reasons a door may not be GraphQL: an external system posting to a fixed URL, a payload of bytes rather than fields, a machine with no user session, or an identity that is not one. A plain JSON read belongs in the GraphQL schema, where the client already is.",
57
- },
58
- },
59
- create(context) {
60
- const filename = context.filename || context.getFilename()
61
- const text = (context.sourceCode || context.getSourceCode()).getText()
62
- return {
63
- Decorator(node) {
64
- if (decoratorName(node) !== "Controller") return
65
- if (reasonFor(routeOf(node), filename, text)) return
66
- context.report({ node, messageId: "unjustified" })
67
- },
68
- }
69
- },
70
- }
71
-
72
- /** A door is a door whatever its transport, and every door in this repository lives in features/. */
73
- export const doorLivesInFeatures = {
74
- meta: {
75
- type: "problem",
76
- docs: { description: "HTTP doors sit under features/, beside the GraphQL ones." },
77
- schema: [],
78
- messages: {
79
- wrongTree:
80
- "A @Controller under src/modules/ is a door parked among the capabilities it calls, where it reads as one and gets imported like one. Every GraphQL door in this repository already lives under features/; transport was never what decided the address.",
81
- },
82
- },
83
- create(context) {
84
- const path = normalize(context.filename || context.getFilename())
85
- if (!/\/src\/modules\//.test(path)) return {}
86
- return {
87
- Decorator(node) {
88
- if (decoratorName(node) !== "Controller") return
89
- context.report({ node, messageId: "wrongTree" })
90
- },
91
- }
92
- },
93
- }
94
-
95
- /** An import specifier's alias prefix for a door. */
96
- const FEATURES_ALIAS = "@features/"
97
-
98
- /** A relative or deep specifier climbing into `features/`, once the alias form is ruled out. */
99
- const FEATURES_SEGMENT = /(^|\/)features\//
100
-
101
- /** Whether an import specifier reaches into `features/`, by alias or by path. */
102
- const reachesFeatures = (specifier) => {
103
- if (typeof specifier !== "string") return false
104
- if (specifier.startsWith(FEATURES_ALIAS)) return true
105
- return FEATURES_SEGMENT.test(specifier)
106
- }
107
-
108
- /**
109
- * Rule 6: "A capability stays under `modules/` and is called by a door, never reached directly."
110
- * A door under `features/` importing a capability under `modules/` is the entire shape TRANSPORT-3
111
- * sets up, and this rule is not that edge - it fires only the other way: a file living under
112
- * `modules/` reaching back into `features/`. That reversal makes the capability the caller and the
113
- * door the thing being reached, which is backwards from what a capability is - it is CALLED, and it
114
- * stays ignorant of who calls it. One capability importing another capability under `modules/` is
115
- * untouched here; this rule polices one edge, not the whole graph.
116
- */
117
- export const noCapabilityImportsFeatures = {
118
- meta: {
119
- type: "problem",
120
- docs: {
121
- description:
122
- "TRANSPORT Rule 6: a capability under modules/ never imports from features/ - a door calls into a capability, never the other way round.",
123
- },
124
- schema: [],
125
- messages: {
126
- reversed:
127
- "`{{specifier}}` reaches from a capability under modules/ into features/, where doors live. A capability is CALLED by a door and stays ignorant of who calls it; importing back into features/ makes the capability the caller, which is the exact edge this law forbids. Move the shared code under modules/, or invert the call so the door reaches in.",
128
- },
129
- },
130
- create(context) {
131
- const path = normalize(context.filename || context.getFilename())
132
- if (!/\/src\/modules\//.test(path)) return {}
133
- const check = (node, specifier) => {
134
- if (reachesFeatures(specifier)) context.report({ node, messageId: "reversed", data: { specifier } })
135
- }
136
- return {
137
- ImportDeclaration(node) {
138
- check(node.source, node.source.value)
139
- },
140
- ExportNamedDeclaration(node) {
141
- if (node.source) check(node.source, node.source.value)
142
- },
143
- ExportAllDeclaration(node) {
144
- if (node.source) check(node.source, node.source.value)
145
- },
146
- }
147
- },
148
- }
149
-
150
- export const rules = {
151
- "rest-door-needs-a-reason": restDoorNeedsAReason,
152
- "door-lives-in-features": doorLivesInFeatures,
153
- "no-capability-imports-features": noCapabilityImportsFeatures,
154
- }
155
-
156
- /**
157
- * `no-capability-imports-features` measures at 9 offenders across 6 files in the reference
158
- * repository (`src/**`, probed against source directly since the published package predates this
159
- * rule) -- every one a capability under `modules/bussiness/**` importing a single shared helper
160
- * published under `@features/api/processors/ai/shared/xp/write-coin-history`. That is standing debt,
161
- * not zero, so it ships at `warn` with the count on record, matching this module's own Exceptions
162
- * section: measured debt arrives at `warn` and flips to `error` once it reaches zero.
163
- */
164
- export const recommended = {
165
- "starci-be/rest-door-needs-a-reason": "error",
166
- "starci-be/door-lives-in-features": "error",
167
- "starci-be/no-capability-imports-features": "warn",
168
- }
1
+ const normalize = (filename) => String(filename || "").replace(/\\/g, "/")
2
+
3
+ /** The decorator's own name, whether it was written `@Controller` or `@Controller("x")`. */
4
+ const decoratorName = (node) => {
5
+ const expression = node.expression
6
+ if (!expression) return null
7
+ if (expression.type === "Identifier") return expression.name
8
+ if (expression.type === "CallExpression" && expression.callee.type === "Identifier") {
9
+ return expression.callee.name
10
+ }
11
+ return null
12
+ }
13
+
14
+ /** The route string, or "" for `@Controller()` and for a path given as an object. */
15
+ const routeOf = (node) => {
16
+ const expression = node.expression
17
+ if (!expression || expression.type !== "CallExpression") return ""
18
+ const first = expression.arguments[0]
19
+ if (first && first.type === "Literal" && typeof first.value === "string") return first.value
20
+ return ""
21
+ }
22
+
23
+ /**
24
+ * Which of the four sanctioned reasons this file shows, if any.
25
+ *
26
+ * READ OFF THE FILE, NOT OFF A LIST. A registry of blessed routes rots the moment somebody adds a
27
+ * route and forgets it; the evidence here is the same evidence a reader would use.
28
+ */
29
+ const reasonFor = (route, filename, text) => {
30
+ const path = normalize(filename)
31
+ // a probe answers when the app is degraded, before the feature layer is necessarily up
32
+ if (/^healthz?$/.test(route) || /\/health(z)?[./]/.test(path)) return "probe"
33
+ // something outside posts to a URL we gave it, and it will never send a GraphQL document
34
+ if (/webhook/i.test(route) || /webhook/i.test(path)) return "external"
35
+ // bytes, not fields: multipart in, stream out
36
+ if (/FileInterceptor|FilesInterceptor|AnyFilesInterceptor|StreamableFile|@Res\s*\(|createReadStream/.test(text)) {
37
+ return "bytes"
38
+ }
39
+ // a machine registering itself, with no user session to carry
40
+ if (/^(pods|internal|agents)\//.test(route)) return "machine"
41
+ // an identity that is not a user session - a platform operator or a service token
42
+ if (/^api\/ops(\/|$)/.test(route) || /Operator[A-Za-z]*Guard|ServiceToken|OPS_TOKEN/.test(text)) {
43
+ return "operator"
44
+ }
45
+ return null
46
+ }
47
+
48
+ /** A REST door exists only where GraphQL cannot go, and the file has to show which case it is. */
49
+ export const restDoorNeedsAReason = {
50
+ meta: {
51
+ type: "problem",
52
+ docs: { description: "A @Controller is permitted only where GraphQL cannot serve." },
53
+ schema: [],
54
+ messages: {
55
+ unjustified:
56
+ "This REST door shows none of the four reasons a door may not be GraphQL: an external system posting to a fixed URL, a payload of bytes rather than fields, a machine with no user session, or an identity that is not one. A plain JSON read belongs in the GraphQL schema, where the client already is.",
57
+ },
58
+ },
59
+ create(context) {
60
+ const filename = context.filename || context.getFilename()
61
+ const text = (context.sourceCode || context.getSourceCode()).getText()
62
+ return {
63
+ Decorator(node) {
64
+ if (decoratorName(node) !== "Controller") return
65
+ if (reasonFor(routeOf(node), filename, text)) return
66
+ context.report({ node, messageId: "unjustified" })
67
+ },
68
+ }
69
+ },
70
+ }
71
+
72
+ /** A door is a door whatever its transport, and every door in this repository lives in features/. */
73
+ export const doorLivesInFeatures = {
74
+ meta: {
75
+ type: "problem",
76
+ docs: { description: "HTTP doors sit under features/, beside the GraphQL ones." },
77
+ schema: [],
78
+ messages: {
79
+ wrongTree:
80
+ "A @Controller under src/modules/ is a door parked among the capabilities it calls, where it reads as one and gets imported like one. Every GraphQL door in this repository already lives under features/; transport was never what decided the address.",
81
+ },
82
+ },
83
+ create(context) {
84
+ const path = normalize(context.filename || context.getFilename())
85
+ if (!/\/src\/modules\//.test(path)) return {}
86
+ return {
87
+ Decorator(node) {
88
+ if (decoratorName(node) !== "Controller") return
89
+ context.report({ node, messageId: "wrongTree" })
90
+ },
91
+ }
92
+ },
93
+ }
94
+
95
+ /** An import specifier's alias prefix for a door. */
96
+ const FEATURES_ALIAS = "@features/"
97
+
98
+ /** A relative or deep specifier climbing into `features/`, once the alias form is ruled out. */
99
+ const FEATURES_SEGMENT = /(^|\/)features\//
100
+
101
+ /** Whether an import specifier reaches into `features/`, by alias or by path. */
102
+ const reachesFeatures = (specifier) => {
103
+ if (typeof specifier !== "string") return false
104
+ if (specifier.startsWith(FEATURES_ALIAS)) return true
105
+ return FEATURES_SEGMENT.test(specifier)
106
+ }
107
+
108
+ /**
109
+ * Rule 6: "A capability stays under `modules/` and is called by a door, never reached directly."
110
+ * A door under `features/` importing a capability under `modules/` is the entire shape TRANSPORT-3
111
+ * sets up, and this rule is not that edge - it fires only the other way: a file living under
112
+ * `modules/` reaching back into `features/`. That reversal makes the capability the caller and the
113
+ * door the thing being reached, which is backwards from what a capability is - it is CALLED, and it
114
+ * stays ignorant of who calls it. One capability importing another capability under `modules/` is
115
+ * untouched here; this rule polices one edge, not the whole graph.
116
+ */
117
+ export const noCapabilityImportsFeatures = {
118
+ meta: {
119
+ type: "problem",
120
+ docs: {
121
+ description:
122
+ "TRANSPORT Rule 6: a capability under modules/ never imports from features/ - a door calls into a capability, never the other way round.",
123
+ },
124
+ schema: [],
125
+ messages: {
126
+ reversed:
127
+ "`{{specifier}}` reaches from a capability under modules/ into features/, where doors live. A capability is CALLED by a door and stays ignorant of who calls it; importing back into features/ makes the capability the caller, which is the exact edge this law forbids. Move the shared code under modules/, or invert the call so the door reaches in.",
128
+ },
129
+ },
130
+ create(context) {
131
+ const path = normalize(context.filename || context.getFilename())
132
+ if (!/\/src\/modules\//.test(path)) return {}
133
+ const check = (node, specifier) => {
134
+ if (reachesFeatures(specifier)) context.report({ node, messageId: "reversed", data: { specifier } })
135
+ }
136
+ return {
137
+ ImportDeclaration(node) {
138
+ check(node.source, node.source.value)
139
+ },
140
+ ExportNamedDeclaration(node) {
141
+ if (node.source) check(node.source, node.source.value)
142
+ },
143
+ ExportAllDeclaration(node) {
144
+ if (node.source) check(node.source, node.source.value)
145
+ },
146
+ }
147
+ },
148
+ }
149
+
150
+ export const rules = {
151
+ "rest-door-needs-a-reason": restDoorNeedsAReason,
152
+ "door-lives-in-features": doorLivesInFeatures,
153
+ "no-capability-imports-features": noCapabilityImportsFeatures,
154
+ }
155
+
156
+ /**
157
+ * `no-capability-imports-features` measures at 9 offenders across 6 files in the reference
158
+ * repository (`src/**`, probed against source directly since the published package predates this
159
+ * rule) -- every one a capability under `modules/bussiness/**` importing a single shared helper
160
+ * published under `@features/api/processors/ai/shared/xp/write-coin-history`. That is standing debt,
161
+ * not zero, so it ships at `warn` with the count on record, matching this module's own Exceptions
162
+ * section: measured debt arrives at `warn` and flips to `error` once it reaches zero.
163
+ */
164
+ export const recommended = {
165
+ "starci-be/rest-door-needs-a-reason": "error",
166
+ "starci-be/door-lives-in-features": "error",
167
+ "starci-be/no-capability-imports-features": "warn",
168
+ }