@aas-dev/stack 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +97 -2
- 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/doctor.d.ts +5 -0
- package/dist/doctor.js +162 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +5 -0
- package/dist/explain.d.ts +4 -0
- package/dist/explain.js +48 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/package-script.d.ts +3 -0
- package/dist/package-script.js +42 -0
- package/dist/plugin/aas.js +379 -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 +49 -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 +76 -0
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +37 -0
- package/dist/verify.d.ts +4 -0
- package/dist/verify.js +190 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +76 -0
- package/package.json +54 -3
- package/plugin/aas.d.mts +8 -0
- package/plugin/aas.mjs +572 -0
- package/templates/AGENTS.md +147 -0
- package/templates/hooks.json +11 -0
- package/templates/pnpm-workspace.yaml +23 -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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This project uses `@aas-dev/stack`. The command is `aas`. 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`, `AGENTS.md`, `.github/workflows/verify.yml`, `vite.config.ts`, and `tsconfig.json` must match the templates from `aas init` byte for byte. `pnpm-workspace.yaml` must carry the same pins. Extra `allowBuilds` keys are allowed on that file only. `aas sync` rewrites the managed files and keeps those extra keys.
|
|
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. `effect-tsgo patch`, then `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` is the process boundary. Everywhere else, stay inside Effect.
|
|
24
|
+
|
|
25
|
+
`tsconfig.json` extends `@aas-dev/stack/tsconfig.base.json`, which sets `noEmit`. `tsc` typechecks. A library build is `vp pack`, not `tsc` emit.
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
`aas init` writes the managed files, adds a `verify` script of `aas verify` when `package.json` has none, and runs `pnpm install` when `pnpm-lock.yaml` already exists so the lockfile picks up the template overrides. When `core.hooksPath` is already set, init prints that path and does not run `vp hooks enable`.
|
|
30
|
+
|
|
31
|
+
`aas sync` rewrites the managed files from the templates.
|
|
32
|
+
|
|
33
|
+
`aas explain` lists every rule. `aas explain <rule>` prints why that ban exists and the approved alternative. A rule id is `aas/no-throw` or `no-throw`.
|
|
34
|
+
|
|
35
|
+
`aas doctor` checks the Node and pnpm pins, the hooks path, lockfile override freshness, the patched `tsc`, and managed-file drift. It prints each check. It exits non-zero when one fails, and the line says what to run.
|
|
36
|
+
|
|
37
|
+
## Blessed patterns
|
|
38
|
+
|
|
39
|
+
One pattern per concern. The lint rule is what makes the other patterns unavailable.
|
|
40
|
+
|
|
41
|
+
### Errors
|
|
42
|
+
|
|
43
|
+
Yield a `Schema.TaggedError`. Pass the brand parameter `@effect/tsgo` requires.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export class EmptyText extends Schema.TaggedError<EmptyText, { readonly brand: unique symbol }>()(
|
|
47
|
+
"EmptyText",
|
|
48
|
+
{},
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
yield * new EmptyText({})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`aas/no-throw`, `aas/no-try`, and `aas/no-error-class` reject `throw`, `try`, and `new Error` (including the other built-in error constructors).
|
|
55
|
+
|
|
56
|
+
### Config
|
|
57
|
+
|
|
58
|
+
Read values with `Config`. A value the project has to supply is declared in `.env.schema`, and that schema is the only place a literal default lives. Do not call `Config.withDefault`.
|
|
59
|
+
|
|
60
|
+
A computed runtime fallback does not belong in `.env.schema` and does not read `process.env`. `Config.orElse` runs the next `Config` when the key is absent. `Config.succeed` inside that callback builds the value when it is needed:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { homedir } from "node:os"
|
|
64
|
+
import { Config } from "effect"
|
|
65
|
+
|
|
66
|
+
export const dataDir = Config.String("DATA_DIR").pipe(
|
|
67
|
+
Config.orElse(() => Config.succeed(`${homedir()}/.aas`)),
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`aas/no-process-env` rejects `process.env`, `process["env"]`, and `process?.env`.
|
|
72
|
+
|
|
73
|
+
Keep a string tuple without a type assertion:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const literals = <const Values extends readonly string[]>(...values: Values): Values => values
|
|
77
|
+
|
|
78
|
+
export const AppEnv = Config.Literals(literals("development", "test", "production"), "APP_ENV")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Services
|
|
82
|
+
|
|
83
|
+
Extend `Context.Service` and provide a `Layer`.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export class AppConfig extends Context.Service<
|
|
87
|
+
AppConfig,
|
|
88
|
+
{ readonly env: "development" | "test" | "production" }
|
|
89
|
+
>()("aas/AppConfig") {
|
|
90
|
+
static readonly layer = Layer.effect(
|
|
91
|
+
AppConfig,
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const env = yield* AppEnv
|
|
94
|
+
return AppConfig.of({ env })
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`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.
|
|
101
|
+
|
|
102
|
+
### IO
|
|
103
|
+
|
|
104
|
+
Use Effect's `FileSystem`, `HttpClient`, `ChildProcess`, and `Console`. Stay inside Effect. Tests use `it.effect` instead of running the boundary.
|
|
105
|
+
|
|
106
|
+
`aas/no-raw-io` rejects `node:fs`, `node:fs/promises`, `fs`, `node:child_process`, `child_process`, `node:http`, `http`, `node:https`, `https`, `require` of those modules, dynamic `import` of those modules, global `fetch`, and `console.log` / `error` / `warn` / `info` / `debug`.
|
|
107
|
+
|
|
108
|
+
Effect 4.0.6 has no TCP or TLS client. `node:net`, `net`, `node:tls`, and `tls` are allowed only in a file whose name ends with `.adapter.ts`. That file is the Node entry for those sockets. `node:fs`, HTTP, child processes, `fetch`, and `console` stay banned there too. No command, flag, or comment grants the exception.
|
|
109
|
+
|
|
110
|
+
`aas/no-effect-run` rejects `Effect.runPromise`, `Effect.runSync`, `Effect.runFork`, and `Effect.runPromiseExit`.
|
|
111
|
+
|
|
112
|
+
### Tests
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { assert, describe, it } from "@effect/vitest"
|
|
116
|
+
|
|
117
|
+
it.effect("counts code points", () =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const counted = yield* countText("👋")
|
|
120
|
+
assert.strictEqual(counted.characters, 1)
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`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.
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
### Types
|
|
130
|
+
|
|
131
|
+
`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.
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
## Comments
|
|
136
|
+
|
|
137
|
+
`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.
|
|
138
|
+
|
|
139
|
+
## Bypasses the fence catches
|
|
140
|
+
|
|
141
|
+
`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.
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
## Ideal
|
|
146
|
+
|
|
147
|
+
One capability, one error style, one config source, one service style, one IO style, one test style. Blank text fails with a `Schema.TaggedError`. A counted value reports Unicode code points, whitespace-separated words, and `\n`-separated lines. `Config` and `Context.Service` are the config and service shapes. Climb toward more capabilities in that shape. Do not add a second pattern because the first one is inconvenient.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
packages:
|
|
2
|
+
- "packages/*"
|
|
3
|
+
|
|
4
|
+
verifyDepsBeforeRun: error
|
|
5
|
+
|
|
6
|
+
# Exact versions are the pin. The release-age gate would refuse the Effect 4
|
|
7
|
+
# and oxlint versions this stack just chose, so it stays off.
|
|
8
|
+
minimumReleaseAge: 0
|
|
9
|
+
|
|
10
|
+
overrides:
|
|
11
|
+
"vite@*": "npm:@voidzero-dev/vite-plus-core@1.4.0"
|
|
12
|
+
"vitest@*": "5.0.5"
|
|
13
|
+
|
|
14
|
+
peerDependencyRules:
|
|
15
|
+
allowAny:
|
|
16
|
+
- vite
|
|
17
|
+
- vitest
|
|
18
|
+
allowedVersions:
|
|
19
|
+
vite: "*"
|
|
20
|
+
vitest: "*"
|
|
21
|
+
|
|
22
|
+
allowBuilds:
|
|
23
|
+
esbuild: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: verify
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
verify:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
|
+
with:
|
|
14
|
+
version: 11.3.0
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
cache: pnpm
|
|
19
|
+
- run: pnpm install --frozen-lockfile
|
|
20
|
+
- run: pnpm exec aas verify
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"allowUnreachableCode": false,
|
|
18
|
+
"allowUnusedLabels": false,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"verbatimModuleSyntax": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"erasableSyntaxOnly": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"skipLibCheck": true,
|
|
25
|
+
"types": ["node"],
|
|
26
|
+
"noEmit": true,
|
|
27
|
+
"plugins": [
|
|
28
|
+
{
|
|
29
|
+
"name": "@effect/language-service",
|
|
30
|
+
"namespaceImportPackages": ["effect"],
|
|
31
|
+
"diagnosticSeverity": {
|
|
32
|
+
"anyUnknownInErrorContext": "error",
|
|
33
|
+
"unsafeEffectTypeAssertion": "error",
|
|
34
|
+
"floatingEffect": "error",
|
|
35
|
+
"floatingEffectInVitest": "error",
|
|
36
|
+
"missingEffectContext": "error",
|
|
37
|
+
"missingEffectError": "error",
|
|
38
|
+
"missingLayerContext": "error",
|
|
39
|
+
"leakingRequirements": "error",
|
|
40
|
+
"globalErrorInEffectCatch": "error",
|
|
41
|
+
"globalErrorInEffectFailure": "error",
|
|
42
|
+
"unknownInEffectCatch": "error",
|
|
43
|
+
"tryCatchInEffectGen": "error",
|
|
44
|
+
"returnEffectInGen": "error",
|
|
45
|
+
"runEffectInsideEffect": "error",
|
|
46
|
+
"schemaSyncInEffect": "error",
|
|
47
|
+
"instanceOfSchema": "error",
|
|
48
|
+
"newPromise": "error",
|
|
49
|
+
"asyncFunction": "error",
|
|
50
|
+
"globalConsole": "error",
|
|
51
|
+
"globalConsoleInEffect": "error",
|
|
52
|
+
"globalDate": "error",
|
|
53
|
+
"globalDateInEffect": "error",
|
|
54
|
+
"globalFetch": "error",
|
|
55
|
+
"globalFetchInEffect": "error",
|
|
56
|
+
"globalRandom": "error",
|
|
57
|
+
"globalRandomInEffect": "error",
|
|
58
|
+
"globalTimers": "error",
|
|
59
|
+
"globalTimersInEffect": "error",
|
|
60
|
+
"cryptoRandomUUID": "error",
|
|
61
|
+
"cryptoRandomUUIDInEffect": "error",
|
|
62
|
+
"nodeBuiltinImport": "error",
|
|
63
|
+
"strictEffectProvide": "off",
|
|
64
|
+
"lazyEffect": "off",
|
|
65
|
+
"strictBooleanExpressions": "off",
|
|
66
|
+
"unstableApiUsage": "off"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|