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>
5.1 KiB
description
| 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.tsowns 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-orderreports 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.tsto 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.tsand no alias, and import./siblings and subfolders freely. - Unchanged: no
/*subpath patterns, the key order ofimportsis the layer orderenforce-layer-orderchecks (add a new alias at the right layer), and entry files areindex.tsonly.
"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
}
// 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
.tsfiles only. There is no#packagealias (ts-only-files-and-imports). - Lint
jsPluginsentries invite.config.tsstay 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-cycleis 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 noindex.ts.- Each tool is one file,
scripts/<name>.ts, imported directly by file:import projectLint from "./scripts/project-lint.ts";invite.config.ts. No alias ownsscripts/, so these./imports are free. - Code in a former
scripts/<tool>/folder is merged or renamed into flatscripts/<name>.tsfiles. 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.jsonimportsalias points only at theindex.tsof a direct subfolder ofsrc/, e.g."#helpers": "./src/helpers/index.ts". No alias to a single file, a deeper folder, or anything outsidesrc/. 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 reachessrc/only through the aliases, since../is banned. - No lint rule checks this; check
importsby hand when adding an alias.