Files
bot-agents/.agents/memory/imports-aliases-barrels.md
T
Danny Kim 32806120b3 Group the sources into aliased modules and lint with the published plugins (#23)
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>
2026-10-18 15:00:16 +00:00

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.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.
"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 .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.