@aas-dev/stack 0.0.0-stage → 0.1.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.
- package/bin/aas.js +2 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +15 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +75 -0
- package/dist/cursor-hook.d.ts +1 -0
- package/dist/cursor-hook.js +28 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +124 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +35 -0
- package/dist/run.d.ts +18 -0
- package/dist/run.js +99 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +55 -0
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +37 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +166 -0
- package/package.json +54 -3
- package/plugin/aas.d.mts +8 -0
- package/plugin/aas.mjs +567 -0
- package/templates/AGENTS.md +132 -0
- package/templates/hooks.json +11 -0
- package/templates/pnpm-workspace.yaml +12 -0
- package/templates/pre-commit +3 -0
- package/templates/tsconfig.json +8 -0
- package/templates/verify.yml +20 -0
- package/templates/vite.config.ts +3 -0
- package/tsconfig.base.json +71 -0
- package/README.md +0 -3
package/plugin/aas.mjs
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
const markers = [
|
|
2
|
+
"@ts-ignore",
|
|
3
|
+
"@ts-expect-error",
|
|
4
|
+
"@ts-nocheck",
|
|
5
|
+
"eslint-disable",
|
|
6
|
+
"eslint-enable",
|
|
7
|
+
"oxlint-disable",
|
|
8
|
+
"oxlint-enable",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const errorNames = new Set([
|
|
12
|
+
"Error",
|
|
13
|
+
"AggregateError",
|
|
14
|
+
"EvalError",
|
|
15
|
+
"RangeError",
|
|
16
|
+
"ReferenceError",
|
|
17
|
+
"SyntaxError",
|
|
18
|
+
"TypeError",
|
|
19
|
+
"URIError",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const rawSpecifiers = new Set([
|
|
23
|
+
"child_process",
|
|
24
|
+
"fs",
|
|
25
|
+
"http",
|
|
26
|
+
"https",
|
|
27
|
+
"net",
|
|
28
|
+
"node:child_process",
|
|
29
|
+
"node:fs",
|
|
30
|
+
"node:fs/promises",
|
|
31
|
+
"node:http",
|
|
32
|
+
"node:https",
|
|
33
|
+
"node:net",
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
const consoleMethods = new Set(["debug", "error", "info", "log", "warn"])
|
|
37
|
+
|
|
38
|
+
const effectRunMethods = new Set(["runFork", "runPromise", "runPromiseExit", "runSync"])
|
|
39
|
+
|
|
40
|
+
const bannedTestMembers = new Set(["only", "skip", "skipIf", "todo"])
|
|
41
|
+
|
|
42
|
+
const testAliases = new Set(["fdescribe", "fit", "ftest", "xdescribe", "xit", "xtest"])
|
|
43
|
+
|
|
44
|
+
const report = (context, node, message) => {
|
|
45
|
+
context.report({ node, message })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const literalName = (property, computed) => {
|
|
49
|
+
if (!computed && property.type === "Identifier") {
|
|
50
|
+
return property.name
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
computed &&
|
|
54
|
+
(property.type === "Literal" || property.type === "StringLiteral") &&
|
|
55
|
+
typeof property.value === "string"
|
|
56
|
+
) {
|
|
57
|
+
return property.value
|
|
58
|
+
}
|
|
59
|
+
return ""
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isProcessEnv = (node) => {
|
|
63
|
+
if (node.type !== "MemberExpression") {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
return (
|
|
67
|
+
node.object.type === "Identifier" &&
|
|
68
|
+
node.object.name === "process" &&
|
|
69
|
+
literalName(node.property, node.computed) === "env"
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hasName = (node, name) => {
|
|
74
|
+
if (node === null || node === undefined || typeof node !== "object") {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
if (node.type === "Identifier" && node.name === name) {
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
hasName(node.callee, name) ||
|
|
82
|
+
hasName(node.object, name) ||
|
|
83
|
+
hasName(node.property, name) ||
|
|
84
|
+
hasName(node.expression, name)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const blessedSuper = (node) => hasName(node, "TaggedError") || hasName(node, "Service")
|
|
89
|
+
|
|
90
|
+
const specifier = (node) => {
|
|
91
|
+
if (node === null || node === undefined) {
|
|
92
|
+
return ""
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
(node.type === "Literal" || node.type === "StringLiteral") &&
|
|
96
|
+
typeof node.value === "string"
|
|
97
|
+
) {
|
|
98
|
+
return node.value
|
|
99
|
+
}
|
|
100
|
+
return ""
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const chainParts = (callee) => {
|
|
104
|
+
const parts = []
|
|
105
|
+
let current = callee
|
|
106
|
+
while (current.type === "MemberExpression" || current.type === "ChainExpression") {
|
|
107
|
+
if (current.type === "ChainExpression") {
|
|
108
|
+
current = current.expression
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (current.computed || current.property.type !== "Identifier") {
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
parts.push(current.property.name)
|
|
115
|
+
current = current.object
|
|
116
|
+
}
|
|
117
|
+
if (current.type !== "Identifier") {
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
parts.push(current.name)
|
|
121
|
+
parts.reverse()
|
|
122
|
+
return parts
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isItEffect = (parts) => {
|
|
126
|
+
if (parts[0] !== "it" || parts[1] !== "effect") {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
let index = 2
|
|
130
|
+
while (index < parts.length) {
|
|
131
|
+
const part = parts[index]
|
|
132
|
+
if (part !== undefined && bannedTestMembers.has(part)) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
index += 1
|
|
136
|
+
}
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const flagModuleBinding = (context, statement) => {
|
|
141
|
+
if (statement === null || statement === undefined) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (statement.type === "VariableDeclaration" && statement.kind !== "const") {
|
|
145
|
+
report(
|
|
146
|
+
context,
|
|
147
|
+
statement,
|
|
148
|
+
"module-scope let and var are banned; keep state in a Context.Service",
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
const binding = statement.init ?? statement.left
|
|
152
|
+
if (
|
|
153
|
+
(statement.type === "ForStatement" ||
|
|
154
|
+
statement.type === "ForInStatement" ||
|
|
155
|
+
statement.type === "ForOfStatement") &&
|
|
156
|
+
binding !== null &&
|
|
157
|
+
binding !== undefined &&
|
|
158
|
+
binding.type === "VariableDeclaration" &&
|
|
159
|
+
binding.kind !== "const"
|
|
160
|
+
) {
|
|
161
|
+
report(context, binding, "module-scope let and var are banned; keep state in a Context.Service")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const noThrow = {
|
|
166
|
+
meta: { type: "problem" },
|
|
167
|
+
create(context) {
|
|
168
|
+
return {
|
|
169
|
+
ThrowStatement(node) {
|
|
170
|
+
report(context, node, "throw is banned; yield a Schema.TaggedError")
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const noTry = {
|
|
177
|
+
meta: { type: "problem" },
|
|
178
|
+
create(context) {
|
|
179
|
+
return {
|
|
180
|
+
TryStatement(node) {
|
|
181
|
+
report(context, node, "try is banned; failures stay on the Effect channel")
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const noErrorClass = {
|
|
188
|
+
meta: { type: "problem" },
|
|
189
|
+
create(context) {
|
|
190
|
+
return {
|
|
191
|
+
NewExpression(node) {
|
|
192
|
+
const callee = node.callee
|
|
193
|
+
const name =
|
|
194
|
+
callee.type === "Identifier"
|
|
195
|
+
? callee.name
|
|
196
|
+
: callee.type === "MemberExpression"
|
|
197
|
+
? literalName(callee.property, callee.computed)
|
|
198
|
+
: ""
|
|
199
|
+
if (errorNames.has(name)) {
|
|
200
|
+
report(context, node, "new Error is banned; define a Schema.TaggedError")
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const noProcessEnv = {
|
|
208
|
+
meta: { type: "problem" },
|
|
209
|
+
create(context) {
|
|
210
|
+
return {
|
|
211
|
+
MemberExpression(node) {
|
|
212
|
+
if (isProcessEnv(node)) {
|
|
213
|
+
report(
|
|
214
|
+
context,
|
|
215
|
+
node,
|
|
216
|
+
"process.env is banned; declare the variable in .env.schema and read it with Config",
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const noTypeAssertion = {
|
|
225
|
+
meta: { type: "problem" },
|
|
226
|
+
create(context) {
|
|
227
|
+
return {
|
|
228
|
+
TSAsExpression(node) {
|
|
229
|
+
report(
|
|
230
|
+
context,
|
|
231
|
+
node,
|
|
232
|
+
"type assertions are banned, including as const; parse with Schema or use a const type parameter",
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
TSTypeAssertion(node) {
|
|
236
|
+
report(
|
|
237
|
+
context,
|
|
238
|
+
node,
|
|
239
|
+
"angle-bracket assertions are banned; parse with Schema or use a const type parameter",
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const noExplicitAny = {
|
|
247
|
+
meta: { type: "problem" },
|
|
248
|
+
create(context) {
|
|
249
|
+
return {
|
|
250
|
+
TSAnyKeyword(node) {
|
|
251
|
+
report(context, node, "any is banned; use unknown and decode it with Schema")
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const noNonNullAssertion = {
|
|
258
|
+
meta: { type: "problem" },
|
|
259
|
+
create(context) {
|
|
260
|
+
return {
|
|
261
|
+
TSNonNullExpression(node) {
|
|
262
|
+
report(
|
|
263
|
+
context,
|
|
264
|
+
node,
|
|
265
|
+
"non-null assertions are banned; narrow the value or decode it with Schema",
|
|
266
|
+
)
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const textOf = (context) => {
|
|
273
|
+
const source = context.sourceCode
|
|
274
|
+
if (typeof source.text === "string") {
|
|
275
|
+
return source.text
|
|
276
|
+
}
|
|
277
|
+
if (typeof source.getText === "function") {
|
|
278
|
+
return source.getText()
|
|
279
|
+
}
|
|
280
|
+
return ""
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const commentStart = (comment) => {
|
|
284
|
+
const range = comment.range
|
|
285
|
+
if (Array.isArray(range)) {
|
|
286
|
+
const start = range[0]
|
|
287
|
+
return typeof start === "number" ? start : -1
|
|
288
|
+
}
|
|
289
|
+
if (range !== null && range !== undefined && typeof range.start === "number") {
|
|
290
|
+
return range.start
|
|
291
|
+
}
|
|
292
|
+
if (typeof comment.start === "number") {
|
|
293
|
+
return comment.start
|
|
294
|
+
}
|
|
295
|
+
return -1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const isShebang = (comment, text) => {
|
|
299
|
+
if (comment.type === "Shebang" || comment.type === "Hashbang") {
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
return text.startsWith("#!") && commentStart(comment) === 0
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const noComments = {
|
|
306
|
+
meta: { type: "problem" },
|
|
307
|
+
create(context) {
|
|
308
|
+
const text = textOf(context)
|
|
309
|
+
return {
|
|
310
|
+
Program(node) {
|
|
311
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
312
|
+
if (isShebang(comment, text)) {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
const message = "comments are banned; use clearer names or docs"
|
|
316
|
+
if (comment.loc === undefined || comment.loc === null) {
|
|
317
|
+
report(context, node, message)
|
|
318
|
+
} else {
|
|
319
|
+
context.report({ loc: comment.loc, message })
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const noBypassComment = {
|
|
328
|
+
meta: { type: "problem" },
|
|
329
|
+
create(context) {
|
|
330
|
+
return {
|
|
331
|
+
Program(node) {
|
|
332
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
333
|
+
let matched = ""
|
|
334
|
+
for (const marker of markers) {
|
|
335
|
+
if (comment.value.includes(marker)) {
|
|
336
|
+
matched = marker
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (matched === "") {
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
const message = `directive comments are banned (${matched})`
|
|
344
|
+
if (comment.loc === undefined || comment.loc === null) {
|
|
345
|
+
report(context, node, message)
|
|
346
|
+
} else {
|
|
347
|
+
context.report({ loc: comment.loc, message })
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const effectTests = {
|
|
356
|
+
meta: { type: "problem" },
|
|
357
|
+
create(context) {
|
|
358
|
+
const filename = typeof context.filename === "string" ? context.filename : ""
|
|
359
|
+
const isTest = filename.endsWith(".test.ts")
|
|
360
|
+
return {
|
|
361
|
+
ImportDeclaration(node) {
|
|
362
|
+
const source = specifier(node.source)
|
|
363
|
+
if (source === "vitest/config") {
|
|
364
|
+
if (!filename.endsWith("/vitest.config.ts") && !filename.endsWith("\\vitest.config.ts")) {
|
|
365
|
+
report(context, node, "import tests from @effect/vitest")
|
|
366
|
+
}
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
if (source === "vitest" || source.startsWith("vitest/") || source.startsWith("@vitest/")) {
|
|
370
|
+
report(context, node, "import tests from @effect/vitest")
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
CallExpression(node) {
|
|
374
|
+
if (!isTest) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
const parts = chainParts(node.callee)
|
|
378
|
+
const root = parts[0]
|
|
379
|
+
if (root === undefined) {
|
|
380
|
+
if (
|
|
381
|
+
node.callee.type === "Identifier" &&
|
|
382
|
+
(node.callee.name === "it" ||
|
|
383
|
+
node.callee.name === "test" ||
|
|
384
|
+
testAliases.has(node.callee.name))
|
|
385
|
+
) {
|
|
386
|
+
report(context, node, "tests use it.effect from @effect/vitest")
|
|
387
|
+
}
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if (isItEffect(parts)) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (root === "it" || root === "test" || testAliases.has(root)) {
|
|
394
|
+
report(context, node, "tests use it.effect from @effect/vitest")
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
if (root === "describe") {
|
|
398
|
+
let index = 1
|
|
399
|
+
while (index < parts.length) {
|
|
400
|
+
const part = parts[index]
|
|
401
|
+
if (part !== undefined && bannedTestMembers.has(part)) {
|
|
402
|
+
report(context, node, "skipped, focused, and todo tests are banned")
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
index += 1
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const noRawIo = {
|
|
414
|
+
meta: { type: "problem" },
|
|
415
|
+
create(context) {
|
|
416
|
+
return {
|
|
417
|
+
ImportDeclaration(node) {
|
|
418
|
+
if (rawSpecifiers.has(specifier(node.source))) {
|
|
419
|
+
report(
|
|
420
|
+
context,
|
|
421
|
+
node,
|
|
422
|
+
"raw IO imports are banned; use Effect FileSystem, HttpClient, or ChildProcess",
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
ImportExpression(node) {
|
|
427
|
+
if (rawSpecifiers.has(specifier(node.source))) {
|
|
428
|
+
report(
|
|
429
|
+
context,
|
|
430
|
+
node,
|
|
431
|
+
"raw IO imports are banned; use Effect FileSystem, HttpClient, or ChildProcess",
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
CallExpression(node) {
|
|
436
|
+
const callee = node.callee
|
|
437
|
+
if (callee.type === "Identifier" && callee.name === "fetch") {
|
|
438
|
+
report(context, node, "global fetch is banned; use Effect HttpClient")
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
callee.type === "Identifier" &&
|
|
443
|
+
callee.name === "require" &&
|
|
444
|
+
rawSpecifiers.has(specifier(node.arguments[0]))
|
|
445
|
+
) {
|
|
446
|
+
report(
|
|
447
|
+
context,
|
|
448
|
+
node,
|
|
449
|
+
"raw IO imports are banned; use Effect FileSystem, HttpClient, or ChildProcess",
|
|
450
|
+
)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
if (callee.type !== "MemberExpression") {
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
const name = literalName(callee.property, callee.computed)
|
|
457
|
+
if (
|
|
458
|
+
callee.object.type === "Identifier" &&
|
|
459
|
+
callee.object.name === "console" &&
|
|
460
|
+
consoleMethods.has(name)
|
|
461
|
+
) {
|
|
462
|
+
report(context, node, "console is banned; use Effect Console")
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (
|
|
466
|
+
callee.object.type === "Identifier" &&
|
|
467
|
+
(callee.object.name === "globalThis" ||
|
|
468
|
+
callee.object.name === "global" ||
|
|
469
|
+
callee.object.name === "window") &&
|
|
470
|
+
name === "fetch"
|
|
471
|
+
) {
|
|
472
|
+
report(context, node, "global fetch is banned; use Effect HttpClient")
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const noEffectRun = {
|
|
480
|
+
meta: { type: "problem" },
|
|
481
|
+
create(context) {
|
|
482
|
+
return {
|
|
483
|
+
CallExpression(node) {
|
|
484
|
+
const callee = node.callee
|
|
485
|
+
if (callee.type !== "MemberExpression") {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
callee.object.type === "Identifier" &&
|
|
490
|
+
callee.object.name === "Effect" &&
|
|
491
|
+
effectRunMethods.has(literalName(callee.property, callee.computed))
|
|
492
|
+
) {
|
|
493
|
+
report(
|
|
494
|
+
context,
|
|
495
|
+
node,
|
|
496
|
+
"Effect.run* is banned; stay inside Effect and use it.effect in tests",
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const noModuleLet = {
|
|
505
|
+
meta: { type: "problem" },
|
|
506
|
+
create(context) {
|
|
507
|
+
return {
|
|
508
|
+
Program(node) {
|
|
509
|
+
for (const statement of node.body) {
|
|
510
|
+
const declaration =
|
|
511
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement
|
|
512
|
+
flagModuleBinding(context, declaration)
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const noPlainClass = {
|
|
520
|
+
meta: { type: "problem" },
|
|
521
|
+
create(context) {
|
|
522
|
+
return {
|
|
523
|
+
ClassDeclaration(node) {
|
|
524
|
+
if (!blessedSuper(node.superClass)) {
|
|
525
|
+
report(
|
|
526
|
+
context,
|
|
527
|
+
node,
|
|
528
|
+
"plain classes are banned; extend Schema.TaggedError or Context.Service",
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
ClassExpression(node) {
|
|
533
|
+
if (!blessedSuper(node.superClass)) {
|
|
534
|
+
report(
|
|
535
|
+
context,
|
|
536
|
+
node,
|
|
537
|
+
"plain classes are banned; extend Schema.TaggedError or Context.Service",
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const rules = {
|
|
546
|
+
"effect-tests": effectTests,
|
|
547
|
+
"no-bypass-comment": noBypassComment,
|
|
548
|
+
"no-comments": noComments,
|
|
549
|
+
"no-effect-run": noEffectRun,
|
|
550
|
+
"no-error-class": noErrorClass,
|
|
551
|
+
"no-explicit-any": noExplicitAny,
|
|
552
|
+
"no-module-let": noModuleLet,
|
|
553
|
+
"no-non-null-assertion": noNonNullAssertion,
|
|
554
|
+
"no-plain-class": noPlainClass,
|
|
555
|
+
"no-process-env": noProcessEnv,
|
|
556
|
+
"no-raw-io": noRawIo,
|
|
557
|
+
"no-throw": noThrow,
|
|
558
|
+
"no-try": noTry,
|
|
559
|
+
"no-type-assertion": noTypeAssertion,
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export const ruleNames = Object.keys(rules)
|
|
563
|
+
|
|
564
|
+
export default {
|
|
565
|
+
meta: { name: "aas" },
|
|
566
|
+
rules,
|
|
567
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This project uses aas-stack. The ideal is one way to do each thing, and `pnpm verify` is the score. A check that cannot run in this environment gets changed until it can. A check that passes while a bypass still works is too lax.
|
|
4
|
+
|
|
5
|
+
`aas verify` is the gate. `pnpm verify` calls it. On success it prints nothing. On failure it prints `file:line` diagnostics.
|
|
6
|
+
|
|
7
|
+
## The loop
|
|
8
|
+
|
|
9
|
+
`aas verify` runs, in order:
|
|
10
|
+
|
|
11
|
+
1. A drift check. `.cursor/hooks.json`, `.vite-hooks/pre-commit`, `pnpm-workspace.yaml`, `AGENTS.md`, `.github/workflows/verify.yml`, `vite.config.ts`, and `tsconfig.json` must match the templates from `aas init`. `aas sync` rewrites them.
|
|
12
|
+
2. `varlock load`, when `.env.schema` is present, so the schema is valid.
|
|
13
|
+
3. `vp fmt --check` (Oxfmt, through Vite+).
|
|
14
|
+
4. `vp lint` (Oxlint, through Vite+, with the `aas` rules below).
|
|
15
|
+
5. `vp pack` (tsdown, through Vite+) for each library.
|
|
16
|
+
6. `tsc --noEmit` with the `@effect/tsgo` language service.
|
|
17
|
+
7. `vp test` (`@effect/vitest` on the Vitest that ships with Vite+).
|
|
18
|
+
|
|
19
|
+
Vite+ does not replace the Effect typecheck. `vp check`'s type checker is tsgolint and does not run `@effect/language-service`. The patched `tsc` does.
|
|
20
|
+
|
|
21
|
+
The Cursor `beforeShellExecution` hook, the Vite+ pre-commit hook, and GitHub Actions all call this same gate. The hook is `failClosed`. CI does not set `continue-on-error`.
|
|
22
|
+
|
|
23
|
+
`NodeRuntime.runMain` in `packages/cli/src/bin.ts` and `packages/cli/src/cursor-hook.ts` is the process boundary. Everywhere else, stay inside Effect.
|
|
24
|
+
|
|
25
|
+
## Blessed patterns
|
|
26
|
+
|
|
27
|
+
One pattern per concern. The lint rule is what makes the other patterns unavailable.
|
|
28
|
+
|
|
29
|
+
### Errors
|
|
30
|
+
|
|
31
|
+
Yield a `Schema.TaggedError`. Pass the brand parameter `@effect/tsgo` requires.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
export class EmptyText extends Schema.TaggedError<EmptyText, { readonly brand: unique symbol }>()(
|
|
35
|
+
"EmptyText",
|
|
36
|
+
{},
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
yield * new EmptyText({})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`aas/no-throw`, `aas/no-try`, and `aas/no-error-class` reject `throw`, `try`, and `new Error` (including the other built-in error constructors).
|
|
43
|
+
|
|
44
|
+
### Config
|
|
45
|
+
|
|
46
|
+
Declare every variable in `.env.schema`. Read it with `Config`. The schema is the only place a default lives. Do not also call `Config.withDefault`.
|
|
47
|
+
|
|
48
|
+
`aas/no-process-env` rejects `process.env`, `process["env"]`, and `process?.env`.
|
|
49
|
+
|
|
50
|
+
Keep a string tuple without a type assertion:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const literals = <const Values extends readonly string[]>(...values: Values): Values => values
|
|
54
|
+
|
|
55
|
+
export const AppEnv = Config.Literals(literals("development", "test", "production"), "APP_ENV")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Services
|
|
59
|
+
|
|
60
|
+
Extend `Context.Service` and provide a `Layer`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
export class AppConfig extends Context.Service<
|
|
64
|
+
AppConfig,
|
|
65
|
+
{ readonly env: "development" | "test" | "production" }
|
|
66
|
+
>()("aas/AppConfig") {
|
|
67
|
+
static readonly layer = Layer.effect(
|
|
68
|
+
AppConfig,
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const env = yield* AppEnv
|
|
71
|
+
return AppConfig.of({ env })
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`aas/no-plain-class` rejects a class whose superclass is not `Schema.TaggedError` or `Context.Service`. `aas/no-module-let` rejects module-scope `let` and `var`. A binding inside a function is fine. State that outlives a call belongs on a service.
|
|
78
|
+
|
|
79
|
+
### IO
|
|
80
|
+
|
|
81
|
+
Use Effect's `FileSystem`, `HttpClient`, `ChildProcess`, and `Console`. Stay inside Effect. Tests use `it.effect` instead of running the boundary.
|
|
82
|
+
|
|
83
|
+
`aas/no-raw-io` rejects `node:fs`, `node:fs/promises`, `fs`, `node:child_process`, `child_process`, `node:http`, `http`, `node:https`, `https`, `node:net`, `net`, `require` of those modules, dynamic `import` of those modules, global `fetch`, and `console.log` / `error` / `warn` / `info` / `debug`.
|
|
84
|
+
|
|
85
|
+
`aas/no-effect-run` rejects `Effect.runPromise`, `Effect.runSync`, `Effect.runFork`, and `Effect.runPromiseExit`.
|
|
86
|
+
|
|
87
|
+
### Tests
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { assert, describe, it } from "@effect/vitest"
|
|
91
|
+
|
|
92
|
+
it.effect("counts code points", () =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
const counted = yield* countText("👋")
|
|
95
|
+
assert.strictEqual(counted.characters, 1)
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`aas/effect-tests` rejects imports from `vitest` or `@vitest/*`, plain `it()` / `test()`, `it.skip` / `it.only` / `it.todo` / `it.skipIf` / `it.live`, `describe.only`, and the aliases `fit`, `fdescribe`, `ftest`, `xit`, `xdescribe`, `xtest`. `it.effect` (and `it.effect.each` / `it.effect.prop`) is the runner. `describe` without a focus or skip modifier is fine.
|
|
101
|
+
|
|
102
|
+
Assert observable behavior. Do not mock the capability. A test that needs a process or the filesystem uses the Node `ChildProcess` and `FileSystem` layers and really runs them.
|
|
103
|
+
|
|
104
|
+
### Types
|
|
105
|
+
|
|
106
|
+
`aas/no-explicit-any` rejects `any`. `aas/no-type-assertion` rejects `as` and angle-bracket assertions, including `as const`. `aas/no-non-null-assertion` rejects `!`. An import alias (`import { readFile as read }`) is not a type assertion.
|
|
107
|
+
|
|
108
|
+
External data is `unknown` until a `Schema` decodes it. Discriminate unions and exhaust them. Brand a value by parsing it. A `Schema.TaggedError` passes `{ readonly brand: unique symbol }` as its brand parameter. `@effect/tsgo` errors on leaked requirements, floating effects, global `Date` / `fetch` / `console`, Node built-in imports, and `instanceof` on a schema type (`Schema.is` is the check). `unstableApiUsage` is off because the Effect v4 APIs this stack requires are still marked unstable.
|
|
109
|
+
|
|
110
|
+
## Comments
|
|
111
|
+
|
|
112
|
+
`aas/no-comments` rejects a line comment, a block comment, and a JSDoc comment. The diagnostic tells the author to use clearer names or docs. A shebang line is not a comment. `/// <reference>` is a line comment, and `aas/no-bypass-comment` does not list it. `@ts-ignore`, `@ts-expect-error`, and `@ts-nocheck` are already `aas/no-bypass-comment` hits; `aas/no-comments` reports those comments as well. `@ts-check` is not a bypass marker. The fence test scans source text, skipping a leading shebang and quoted spans, so a comment the linter did not report still fails.
|
|
113
|
+
|
|
114
|
+
## Bypasses the fence catches
|
|
115
|
+
|
|
116
|
+
`aas/no-bypass-comment` rejects comments that contain `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `eslint-disable`, `eslint-enable`, `oxlint-disable`, or `oxlint-enable`. A disable comment can silence the statement it covers, so the fence test also scans source text. A block comment that hides every oxlint diagnostic still fails that scan. The fixtures in `packages/fence/fixtures` are ignored by the normal lint so they can exist; `packages/fence` runs `vp lint` on them and asserts each rule id and count. Blessed fixtures must produce zero diagnostics.
|
|
117
|
+
|
|
118
|
+
Git cannot delete `--no-verify`. The agent hook rejects it anyway, including inside `bash -c "..."`. It also rejects a short option cluster that contains `n` on `commit`, `merge`, `rebase`, `cherry-pick`, `am`, and `pull` (`-n`, `-nm`, `-an`), and it rejects `LEFTHOOK=0`, `HUSKY=0`, `VP_GIT_HOOKS=0`, `VITE_GIT_HOOKS=0`, `vp hooks disable`, and `vp.hooks.disabled`. `git log -n 5`, `git push -n`, `git commit -m "hello -n"`, `vp hooks enable`, and `vp hooks status` stay allowed: `-n` is only special on the subcommands above, and a quoted message is not a flag. Unreadable hook input crashes the process, and `failClosed` turns that crash into a deny. The Vite+ pre-commit hook runs `pnpm exec aas verify` for a human commit. CI runs the same command even if the hook was skipped.
|
|
119
|
+
|
|
120
|
+
## Ideal
|
|
121
|
+
|
|
122
|
+
One capability, one error style, one config source, one service style, one IO style, one test style. `@aas-stack/core` is that shape for one capability, `countText`: blank text fails with `EmptyText`, and a counted value reports Unicode code points, whitespace-separated words, and `\n`-separated lines. `AppEnv` and `AppConfig` are the config and service examples. Climb toward more capabilities in that shape. Do not add a second pattern because the first one is inconvenient.
|
|
123
|
+
|
|
124
|
+
## Vendored Effect
|
|
125
|
+
|
|
126
|
+
`pnpm vendor:agent-sources` clones the Effect version pinned by `@aas-stack/core` into `vendor/effect`. The clone is gitignored. `pnpm verify` does not clone.
|
|
127
|
+
|
|
128
|
+
## Not in this commit
|
|
129
|
+
|
|
130
|
+
HTTP with OpenAPI, MCP, and the Executor app adapter are next. The runtime constraint those adapters have to meet is already written in `docs/executor-runtime.md`. `kristoffer-aas/roborock-mcp` is the planned first consumer.
|
|
131
|
+
|
|
132
|
+
rat-stack and pstack are the inspiration for the fence and the TypeScript rules. This repo does not vendor their code, and it does not use XState or Alchemy.
|