32806120b3
Groups the flat `src/` into cohesive modules and turns on the published lint plugins. No behavior change. ## Module layout Each module is a direct `src/` subfolder with an `index.ts` barrel and a `#` alias in `package.json` `imports` (layer order, top first): | Alias | Files | | --- | --- | | `#prepare` | `index.ts` (the prepare phase entry point) | | `#run` | `index.ts` (the run phase entry point) | | `#post` | `index.ts` (the post phase entry point), `post.ts`, `post.test.ts` | | `#agents` | `auth.ts`, `opencode.ts` (Codex / Claude Code sign-in, OpenCode args and output) | | `#processes` | `child.ts`, `isolation.ts` | | `#runtime` | `runtime.ts`, `credentials.ts` | | `#profiles` | `profiles.ts` | | `#common` | `errors.ts`, `json.ts` | | `#testing` | `index.ts` (unchanged) | Every top-level entry of `src/` is an aliased module folder. Tests stay next to their code as `*.test.ts`. Each phase's `index.ts` is its entry point, and `action.yml` now runs `node src/<phase>/index.ts`. The post phase's logic moved to `src/post/post.ts` so its test can import it; `index.ts` keeps the env reading and the `import.meta.main` call. `run`'s prompt URL is now `../../prompt.md`. `scripts/protect-source.sh` now copies `src/` recursively (it used to copy `src/*.ts` only), and the workflow test checks that module files get staged too. ## Lint plugins - New devDependencies: `eslint-plugin-explicit-types@^0.6.1` and `eslint-plugin-import-structure@^0.10.2`, both in `jsPlugins`. The lockfile comes from a real `npm install`. - Enabled configs: `importStructure.configs.modules`, `explicitTypes.configs.concrete`, `explicitTypes.configs.absence`. - **explicit-types `naming` is off** (the other app repos turn it on). Its `upper-snake-string-unions` and `no-upper-snake-object-keys` rules fire on wire tokens this repo doesn't choose: profile types `"claude" | "codex" | "opencode"` from `profiles.json`, failure reasons posted to Gitea, Gitea's `"new" | "old"` side, and environment variable names such as `PATH`. Satisfying them would change behavior or need opt-outs. This is logged as an open question for the user. Lint fixes, all type-level: - `Pick<BotProfile, "type" | "model">` became a named `ProfileIdentity` interface that `BotProfile`, `ProfileFailure`, and `RuntimeProfile` extend. `RuntimeOutcome` is now `RuntimeSuccess | RuntimeFailure`. - Inline object types became named interfaces. - `Record<string, string>` env maps became `NodeJS.ProcessEnv`. `JsonObject` and the verdict table became `NodeJS.Dict<…>`. - `T | undefined` locals and returns became `T | null` with matching checks. `typeof fetch` became a `Fetcher` type. - Claude's result fields are held in camelCase (`isError`, `apiErrorStatus`) after parsing, because `id-match` rejects snake_case interface members. - Duplicate import sources are merged (`import-structure/no-duplicate-import-source`). ## Verification - `vp check`: clean. - `vp test`: 61 passed and 5 skipped as a normal user. All 66 passed as root in `node:24-bookworm`. - Each phase (`node src/<phase>/index.ts`) run from a `protect-source.sh` copy loads its full alias graph under Node. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #23 Reviewed-by: bot <noreply@capsulizers.com> Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
106 lines
5.1 KiB
Markdown
106 lines
5.1 KiB
Markdown
---
|
|
description: An alias owns its target (folder index.ts = folder and below, any other file = that file); no overlap; outside an area only the exact alias reaches it; ../ is banned in every file; locally, aliases target only direct src/ subfolders
|
|
---
|
|
|
|
## `#` aliases own their targets
|
|
|
|
Shared code is imported through Node subpath imports declared in
|
|
`package.json` `imports`. Node, TypeScript, Vite, Vite+'s config bundler, and
|
|
Oxlint's plugin loader all resolve them natively, so `.ts` source runs
|
|
unbuilt. `vp pack` inlines them, so they never reach package consumers.
|
|
|
|
One rule for every file, never tied to folder or file names (user rule
|
|
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
|
|
subfolders" rule as the general rule, and the "import-structure plugin
|
|
checks only aliased folders" scope; this repo keeps the stricter rule as a
|
|
local convention, see the last section):
|
|
|
|
- **An alias may point anywhere** and owns its target. An alias to a
|
|
folder's `index.ts` owns that folder and everything below it; an alias to
|
|
any other file owns only that file.
|
|
- **No overlap.** No file may be owned by 2 aliases: no alias inside another
|
|
alias's area, and no 2 aliases with the same target. The import-structure
|
|
plugin's `enforce-layer-order` reports it once at the top of every file 2
|
|
aliases claim. Fix by merging the aliases or moving files apart.
|
|
- **Outside an area, only the exact alias reaches it.** A deeper alias
|
|
specifier (`#a/one.ts`, `no-barrel-bypass`) and a relative path into the
|
|
area (`./a/one.ts`, `no-relative-bypass`; `./b/f.ts` to a file alias,
|
|
`prefer-alias-import`, fixed to the alias) are bypasses.
|
|
- **Inside an area, relative imports of siblings and subfolders are free.**
|
|
- **A `../` import is banned in every file**, owned or not (`no-parent-import`).
|
|
Runtime file URLs (`new URL("../prompt.md", import.meta.url)`) are not
|
|
imports and are allowed.
|
|
Anything above a file needs an alias.
|
|
- **Files no alias owns** need no `index.ts` and no alias, and import `./`
|
|
siblings and subfolders freely.
|
|
- Unchanged: no `/*` subpath patterns, the key order of `imports` is the
|
|
layer order `enforce-layer-order` checks (add a new alias at the right
|
|
layer), and entry files are `index.ts` only.
|
|
|
|
```jsonc
|
|
"imports": {
|
|
"#a": "./a/index.ts", // owns a/**
|
|
"#f": "./b/f.ts", // owns only b/f.ts
|
|
"#a-sub": "./a/sub/index.ts", // no: inside #a
|
|
"#a2": "./a/index.ts" // no: same target as #a
|
|
}
|
|
```
|
|
|
|
```ts
|
|
// in main.ts, which no alias owns
|
|
import { a } from "#a"; // yes
|
|
import { one } from "#a/one.ts"; // no: deeper alias specifier
|
|
import { one } from "./a/one.ts"; // no: relative path into #a
|
|
import { f } from "#f"; // yes
|
|
import { f } from "./b/f.ts"; // no: relative path to a file alias
|
|
import { g } from "./b/g.ts"; // yes: unowned
|
|
// in a/one.ts, owned by #a
|
|
import { two } from "./two.ts"; // yes: sibling
|
|
import { deep } from "./sub/deep.ts"; // yes: subfolder of the same area
|
|
import { x } from "../x.ts"; // no: no ../ anywhere
|
|
```
|
|
|
|
- Aliases point at `.ts` files only. There is no `#package` alias
|
|
([[ts-only-files-and-imports]]).
|
|
- Lint `jsPlugins` entries in `vite.config.ts` stay path strings, even when
|
|
the file has an alias.
|
|
- Keep test-only helpers in their own alias (e.g. `#testing`) so they never
|
|
land in a bundle.
|
|
|
|
## Barrels
|
|
|
|
- An area used from outside exposes its API through its alias target, and
|
|
callers import the alias (`#ui`), not files inside it.
|
|
- Barrels are also the standard fix for `import/max-dependencies`: one barrel
|
|
import replaces many direct ones ([[lint-overrides-policy]]).
|
|
- `import/no-cycle` is on; a barrel must not create a cycle.
|
|
|
|
Relative imports are written with the `.ts` extension.
|
|
|
|
## `scripts/` is flat (user rule, 2026-10-16)
|
|
|
|
- `scripts/` has no subfolders and no `index.ts`.
|
|
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
|
`import projectLint from "./scripts/project-lint.ts";` in
|
|
`vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free.
|
|
- Code in a former `scripts/<tool>/` folder is merged or renamed into flat
|
|
`scripts/<name>.ts` files. Non-TS assets (HTML, images, fixtures) move to
|
|
where they belong (next to the tool's output, `.agents/`, or a gitignored
|
|
scratch dir). A subfolder that is really an app moves under an alias.
|
|
|
|
## Local convention: aliases target direct `src/` subfolders
|
|
|
|
This is a convention of this repo only, stricter than the general rule above
|
|
(which the published import-structure plugin checks and which allows more).
|
|
It is not a plugin rule.
|
|
|
|
- Every `package.json` `imports` alias points only at the `index.ts` of a
|
|
direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`.
|
|
No alias to a single file, a deeper folder, or anything outside `src/`.
|
|
Here the aliases are `#prepare`, `#run`, `#post`, `#agents`, `#processes`, `#runtime`, `#profiles`,
|
|
`#common`, and `#testing`, in that layer order.
|
|
- Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias
|
|
of its own and imports its own files by `./` relative paths. It reaches
|
|
`src/` only through the aliases, since `../` is banned.
|
|
- No lint rule checks this; check `imports` by hand when adding an alias.
|