From b170f25c7d82b939f052ddb797b135d5f1153f04 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 21:48:20 +0900 Subject: [PATCH 01/27] Document that package.json aliases may target only direct subfolders of src --- .agents/memory/imports-aliases-barrels.md | 11 ++++++++++- .agents/memory/tests-layout-vitest.md | 5 +++-- .agents/skills/migrate-deno-repo/SKILL.md | 4 +++- AGENTS.md | 5 +++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index ea61337..e00b8d9 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -6,7 +6,16 @@ description: Shared code comes through # aliases in package.json imports, each d Shared code is imported through Node subpath imports declared in `package.json` `imports`, never by a relative path that climbs out of a -directory: +directory. + +**Targets (user rule, 2026-10-16):** an alias may point ONLY at a direct +subfolder of `src/`: its `index.ts` (`"#helpers": "./src/helpers/index.ts"`) +or a `#x/*` pattern inside that one folder. Never alias anything outside +`src/` (`scripts/`, root files), `src/` itself (no `#plugin` for +`./src/index.ts`), or a deeper folder (`./src/a/b/`). Code that would need +such an alias uses a relative import instead; `scripts/` and +`vite.config.ts` import each other relatively. Layer rules +(`enforce-layer-order`) must still pass, so reorder entries if needed. ```jsonc "imports": { diff --git a/.agents/memory/tests-layout-vitest.md b/.agents/memory/tests-layout-vitest.md index 163653d..1789756 100644 --- a/.agents/memory/tests-layout-vitest.md +++ b/.agents/memory/tests-layout-vitest.md @@ -11,8 +11,9 @@ Decided 2026-10-14 for all repos. config needed. Packages publish only `dist/`, so colocated tests cost nothing in the tarball. - **Regression and integration tests** live in the top-level `tests/` folder - and use only the public entry point (an alias like `#plugin`), never - internals. `vp test tests` runs just these. + and use only the public entry point through a relative import + (`../src/index.ts`; no alias may target `src/` itself, see + [[imports-aliases-barrels]]), never internals. `vp test tests` runs just these. - **No in-source tests** (`import.meta.vitest`). - **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`, diff --git a/.agents/skills/migrate-deno-repo/SKILL.md b/.agents/skills/migrate-deno-repo/SKILL.md index 2c6230a..79338e7 100644 --- a/.agents/skills/migrate-deno-repo/SKILL.md +++ b/.agents/skills/migrate-deno-repo/SKILL.md @@ -27,7 +27,9 @@ with everything. { sortPackageJson: false }` and the category-based `lint` block. - [ ] Delete `deno.json`, `deno.lock`, and Deno-only files. `mod.ts` → `index.ts`; `deno.json` imports → `package.json` `imports` (`#x/*` - patterns for trailing-slash mappings); `jsr:`/`npm:` → npm packages. + patterns for trailing-slash mappings; aliases may target only direct + subfolders of `src/`, anything else becomes a relative import); + `jsr:`/`npm:` → npm packages. **Code** diff --git a/AGENTS.md b/AGENTS.md index 8536877..122938c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,11 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as `src/*.test.ts`; repository-level tests live in `tests/`. +`src/` is flat, so `package.json` has no `imports` aliases and modules, tests, +and `scripts/` import each other relatively. If one is ever added, it may point +only at a direct subfolder of `src/` (its `index.ts`, or a `#x/*` pattern +inside it); never at `src/` itself, a deeper folder, or anything outside `src/`. + The only config files are `package.json`, `tsconfig.json`, and `vite.config.ts`, plus `package-lock.json`. Repo-local lint rules and the rule list live in `scripts/project-lint.ts`. Lint disable comments are not allowed; -- 2.52.0 From 934a3cfd036c34d04ef32dc6ddadbfc0851c4779 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 23:15:37 +0900 Subject: [PATCH 02/27] Remove Deno lint references from the project lint plugin comments --- scripts/project-lint.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index a62efc2..2da8fd0 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -1,8 +1,6 @@ /** - * `project` — lint rules this repository holds its own source to. Deno lint - * ran here with its `recommended` rules and no plugins; the Oxlint categories - * in `vite.config.ts` plus the rule list below keep at least that coverage. - * Its rules of its own are `no-disable-comments` and `no-non-ts-imports`. + * `project` — lint rules this repository holds its own source to, on top of + * the Oxlint categories in `vite.config.ts`. Its rules of its own are `no-disable-comments` and `no-non-ts-imports`. * * Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry. * @@ -150,9 +148,8 @@ const noNonTsImportsRule: ProjectRule = { }; /** - * This repository's own rule list, spread by `vite.config.ts`: Deno's - * `recommended` rules that sit outside the Oxlint categories, plus the - * template's extras. Built-in Oxlint rule ids sit here too, so + * This repository's own rule list, spread by `vite.config.ts`: rules that + * sit outside the Oxlint categories, plus the template's extras. Built-in Oxlint rule ids sit here too, so * `vite.config.ts` lists no rule by name. */ const rules: DummyRuleMap = { @@ -164,7 +161,7 @@ const rules: DummyRuleMap = { "^_*[^_]*_*$|^[^a-z]*$", { properties: false, classFields: false, onlyDeclarations: false, ignoreDestructuring: true }, ], - // Deno's `recommended` rules outside the categories. + // Rules outside the categories. "no-array-constructor": "error", "no-case-declarations": "error", "no-empty": "error", @@ -189,8 +186,7 @@ const rules: DummyRuleMap = { "typescript/prefer-as-const": "error", "typescript/prefer-namespace-keyword": "error", "typescript/triple-slash-reference": "error", - // Deno's `no-window`, `no-process-global`, and `no-node-globals`: on Node, - // these come from an explicit `node:` import instead of the global scope. + // On Node, these come from an explicit `node:` import instead of the global scope. "no-restricted-globals": [ "error", "window", @@ -205,8 +201,7 @@ const rules: DummyRuleMap = { "typescript/consistent-type-imports": "error", "typescript/no-import-type-side-effects": "error", "typescript/no-non-null-assertion": "error", - // Deno's `ban-untagged-ignore` and `ban-unused-ignore`, made stricter: no - // disable comments at all. + // No disable comments at all. "project/no-disable-comments": "error", "project/no-non-ts-imports": "error", "unicorn/no-abusive-eslint-disable": "error", -- 2.52.0 From 2e09945a6a46591edb85b1f9e970a45466e301c8 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 23:19:03 +0900 Subject: [PATCH 03/27] Document that scripts is flat with no subfolders or index files --- .agents/memory/imports-aliases-barrels.md | 17 +++++++++++++++++ .agents/memory/lint-overrides-policy.md | 4 +++- .agents/skills/migrate-deno-repo/SKILL.md | 3 +++ AGENTS.md | 2 ++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index e00b8d9..3655159 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -55,3 +55,20 @@ so they never reach package consumers. Relative imports inside one directory (`./sibling.ts`) are fine, with the `.ts` extension written out. + +## `scripts/` is flat (user rule, 2026-10-16) + +- `scripts/` has no subfolders and no `index.ts`. Barrels and `index.ts` + entries exist only inside `src/`; delete any `index.ts` outside it. +- Each tool is one file, `scripts/.ts`, imported directly by file: + `import { decorators } from "./scripts/decorators.ts";` in + `vite.config.ts`. +- Code in a former `scripts//` folder is merged or renamed into flat + `scripts/.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 `src/` as an + aliased folder. +- import-check 0.10.0+ checks folder layout only where the importer or the + target sits in a `#`-aliased folder, so `vite.config.ts` → + `./scripts/x.ts` passes without any override. Remove overrides that only + dodged import-check. diff --git a/.agents/memory/lint-overrides-policy.md b/.agents/memory/lint-overrides-policy.md index f94a10d..2b552c9 100644 --- a/.agents/memory/lint-overrides-policy.md +++ b/.agents/memory/lint-overrides-policy.md @@ -11,7 +11,9 @@ practice. off for `src/rules/index.ts`, the rule registry, which must import every rule. It is not a precedent for apps. - **`scripts/**`:** a narrow override is allowed when a tooling script truly - needs it, e.g. `no-console` in a CLI script. + needs it, e.g. `no-console` in a CLI script. Never one that only dodges + import-check (e.g. `no-relative-bypass` for `vite.config.ts`); import-check + ignores folders no `#` alias owns. - **Generated files:** list them in `ignorePatterns`, never `overrides`. - **Anything else needs the user's approval first.** When the user is away, don't add it: refactor, or leave the violation failing and log the question. diff --git a/.agents/skills/migrate-deno-repo/SKILL.md b/.agents/skills/migrate-deno-repo/SKILL.md index 79338e7..36e4373 100644 --- a/.agents/skills/migrate-deno-repo/SKILL.md +++ b/.agents/skills/migrate-deno-repo/SKILL.md @@ -40,6 +40,9 @@ with everything. - [ ] Decorators stay standard with `accessor`; add `scripts/decorators.ts` if the repo has any. Lit repos: `scripts/lit-check.ts` + `npm run lit-check`. +- [ ] `scripts/` is flat: no subfolders, no `index.ts` (barrels live only in + `src/`); `vite.config.ts` imports `./scripts/.ts` directly. A + scripts subfolder that is really an app moves under `src/`. **Lint** diff --git a/AGENTS.md b/AGENTS.md index 122938c..54775b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,8 @@ type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as and `scripts/` import each other relatively. If one is ever added, it may point only at a direct subfolder of `src/` (its `index.ts`, or a `#x/*` pattern inside it); never at `src/` itself, a deeper folder, or anything outside `src/`. +`scripts/` is flat: one file per tool, no subfolders and no `index.ts` +(barrels live only in `src/`), imported directly by file path. The only config files are `package.json`, `tsconfig.json`, and `vite.config.ts`, plus `package-lock.json`. Repo-local lint rules and the rule -- 2.52.0 From ffb7cb9a750782e78cc6ba4b3ab35b48a695882f Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 23:41:04 +0900 Subject: [PATCH 04/27] Rename no-non-ts-imports to no-asset-imports, allow .js subpaths, and compare node types by enum member --- scripts/project-lint.ts | 43 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index 2da8fd0..14d52bb 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -1,6 +1,6 @@ /** * `project` — lint rules this repository holds its own source to, on top of - * the Oxlint categories in `vite.config.ts`. Its rules of its own are `no-disable-comments` and `no-non-ts-imports`. + * the Oxlint categories in `vite.config.ts`. Its rules of its own are `no-disable-comments` and `no-asset-imports`. * * Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry. * @@ -8,7 +8,7 @@ */ import type { LanguageOptions, RuleDefinition, RuleVisitor, TextSourceCode } from "@eslint/core"; -import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; import type { DummyRuleMap } from "vite-plus/lint"; /** The source code both hosts hand a rule, with the comment list. */ @@ -70,12 +70,16 @@ const noDisableCommentsRule: ProjectRule = { /** A module specifier read as a path: relative, absolute, or a `#` alias. */ const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u; +/** Script module extensions an import may name: our `.ts` files and `.js` package subpaths. */ +const SCRIPT_EXTENSIONS: readonly string[] = [".ts", ".js"]; + /** - * Whether `source` names something other than a `.ts` module: a query string - * such as `?raw`, or a final path segment with any other extension. Bare - * package names are left alone, dots and all, as are extensionless paths. + * Whether `source` names an asset rather than a script module: a query string + * such as `?raw`, or a final path segment with an extension other than `.ts` + * or `.js`. Bare package names are left alone, dots and all, as are + * extensionless paths. */ -function isNonTsSource(source: string): boolean { +function isAssetSource(source: string): boolean { if (source.includes("?")) { return true; } @@ -90,30 +94,31 @@ function isNonTsSource(source: string): boolean { if (lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".")) { return false; } - return !lastSegment.endsWith(".ts"); + return !SCRIPT_EXTENSIONS.some((extension: string): boolean => lastSegment.endsWith(extension)); } /** - * `no-non-ts-imports` — rejects importing anything but a `.ts` module: a + * `no-asset-imports` — rejects importing anything but a script module: a * source path, static or a dynamic `import()` with a string literal, ending in - * another extension (`.css`, `.json`, `.svg`, `.js`, `.tsx`) or carrying a - * query string (`?raw`, `?url`), and any import or export with import - * attributes. Bare package names, `#` aliases, and `node:` builtins without an - * extension pass; package subpaths are checked like paths. + * an extension other than `.ts` or `.js` (`.css`, `.json`, `.svg`, `.tsx`) or + * carrying a query string (`?raw`, `?url`), and any import or export with + * import attributes. `.js` covers package subpaths such as + * `lit/decorators.js`. Bare package names, `#` aliases, and `node:` builtins + * without an extension pass; package subpaths are checked like paths. */ -const noNonTsImportsRule: ProjectRule = { +const noAssetImportsRule: ProjectRule = { meta: { type: "problem", - docs: { description: "Disallow importing modules other than .ts files" }, + docs: { description: "Disallow importing assets instead of .ts or .js modules" }, schema: [], messages: { - extension: "Only .ts modules may be imported.", + extension: "Only .ts and .js modules may be imported.", attributes: "Import attributes are not allowed.", }, }, create(context) { function checkSource(source: TSESTree.StringLiteral): void { - if (isNonTsSource(source.value)) { + if (isAssetSource(source.value)) { context.report({ node: source, messageId: "extension" }); } } @@ -136,7 +141,7 @@ const noNonTsImportsRule: ProjectRule = { ExportNamedDeclaration: checkDeclaration, ExportAllDeclaration: checkDeclaration, ImportExpression(node: TSESTree.ImportExpression): void { - if (node.source.type === "Literal" && typeof node.source.value === "string") { + if (node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") { checkSource(node.source); } if (node.options !== null) { @@ -203,7 +208,7 @@ const rules: DummyRuleMap = { "typescript/no-non-null-assertion": "error", // No disable comments at all. "project/no-disable-comments": "error", - "project/no-non-ts-imports": "error", + "project/no-asset-imports": "error", "unicorn/no-abusive-eslint-disable": "error", // Pedantic, but Node's and the web's own types (`Response`, `Stats`, // `RequestInit`) are mutable, so no parameter of theirs can pass; the @@ -219,7 +224,7 @@ const plugin = { meta: { name: "project" }, rules: { "no-disable-comments": noDisableCommentsRule, - "no-non-ts-imports": noNonTsImportsRule, + "no-asset-imports": noAssetImportsRule, }, configs: { project: { rules } }, }; -- 2.52.0 From 662e946fadad7d1f66eef604e7009063faf33670 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 23:41:04 +0900 Subject: [PATCH 05/27] Drop the scripts lint override and accept Oxfmt's standard package.json order --- package.json | 8 ++++---- vite.config.ts | 7 ------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 4d78189..2d3c442 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "bot-agents", - "description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.", "private": true, + "description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.", "type": "module", - "engines": { - "node": ">=24.2" - }, "devDependencies": { "@eslint/core": "^0.17.0", "@types/node": "^24.19.2", "@typescript-eslint/types": "^8.71.2", "vite-plus": "^1.2.0", "yaml": "^2.9.1" + }, + "engines": { + "node": ">=24.2" } } diff --git a/vite.config.ts b/vite.config.ts index 4d27725..1c48b7c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,18 +3,11 @@ import { defineConfig } from "vite-plus"; import projectLint from "./scripts/project-lint.ts"; export default defineConfig({ - // Keep package.json in its hand-written order. - fmt: { sortPackageJson: false }, lint: { plugins: ["typescript", "unicorn", "oxc", "import"], jsPlugins: ["./scripts/project-lint.ts"], categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" }, rules: projectLint.configs.project.rules, - // Repo tooling, not app code: the lint plugin compares AST node `type`s, - // which the AST types declare as an enum. - overrides: [ - { files: ["scripts/**"], rules: { "typescript/no-unsafe-enum-comparison": "off" } }, - ], options: { typeAware: true, typeCheck: true, -- 2.52.0 From 15fe4811813d58d90bc4f687173bd502acca227f Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Fri, 16 Oct 2026 23:41:04 +0900 Subject: [PATCH 06/27] Prune migration notes and align the notes with the current lint setup --- .agents/memory/app-repo-specific-decisions.md | 4 +- .agents/memory/bot-agents-lint-opt-outs.md | 15 ++-- .agents/memory/deno-to-node-migration.md | 45 ----------- .agents/memory/imports-aliases-barrels.md | 7 +- .agents/memory/lint-config-shape.md | 9 +-- .agents/memory/lit-analyzer-ts6.md | 7 +- .agents/memory/package-json-shape.md | 31 ++------ .agents/memory/project-lint-plugin.md | 8 +- .agents/memory/publishing-ci-only.md | 2 +- .agents/memory/toolchain-node-vite-plus.md | 4 +- .agents/memory/ts-only-files-and-imports.md | 15 ++-- .agents/skills/migrate-deno-repo/SKILL.md | 74 ------------------- 12 files changed, 47 insertions(+), 174 deletions(-) delete mode 100644 .agents/memory/deno-to-node-migration.md delete mode 100644 .agents/skills/migrate-deno-repo/SKILL.md diff --git a/.agents/memory/app-repo-specific-decisions.md b/.agents/memory/app-repo-specific-decisions.md index 188bad5..0c693d2 100644 --- a/.agents/memory/app-repo-specific-decisions.md +++ b/.agents/memory/app-repo-specific-decisions.md @@ -12,7 +12,9 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16): consumer (`uses: .../commons/bot-agents@`) on a throwaway PR. - **Profiles are JSON.** `profiles.yml` became `profiles.json` with the same content (encrypted `v1:` values unchanged), read with `JSON.parse`. That - removed the only runtime dependency (`@std/yaml`); keep it dependency-free. + removed the only runtime dependency (`@std/yaml`); keep it free of runtime + dependencies (`@eslint/core` and the other devDependencies serve only the + tooling). - **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. `engines.node` is `>=24.2`, which the user accepted for this repo ([[bot-agents-runtime-no-build]]). diff --git a/.agents/memory/bot-agents-lint-opt-outs.md b/.agents/memory/bot-agents-lint-opt-outs.md index 96f3186..7145c6d 100644 --- a/.agents/memory/bot-agents-lint-opt-outs.md +++ b/.agents/memory/bot-agents-lint-opt-outs.md @@ -1,17 +1,18 @@ --- -description: bot-agents lint has prefer-readonly-parameter-types off (Node/web types can't satisfy it) and one scripts/** override for no-unsafe-enum-comparison; nothing else +description: bot-agents lint has prefer-readonly-parameter-types off (Node/web types can't satisfy it) and no overrides; nothing else --- -The repo follows [[lint-config-shape]] with two recorded exceptions, both -present when the migration PR was approved: +The repo follows [[lint-config-shape]] with one recorded exception, present +when the migration PR was approved: - `typescript/prefer-readonly-parameter-types` is `"off"` in `scripts/project-lint.ts`, as in the explicit-types template, because Node/web types (`Response`, `Stats`, `RequestInit`) can never satisfy it. -- `vite.config.ts` has one `overrides` entry, for `scripts/**` only: - `typescript/no-unsafe-enum-comparison` off, because the lint plugin compares - AST node `type`s, which the AST types declare as an enum. This is the - tooling exception [[lint-overrides-policy]] allows. +- `vite.config.ts` has no `overrides`. The `scripts/**` override for + `typescript/no-unsafe-enum-comparison` was removed (2026-10-16): + `scripts/project-lint.ts` compares node types with `AST_NODE_TYPES` members + from `@typescript-eslint/types` instead of string literals + ([[lint-overrides-policy]]). Oxlint's default size limits apply to `src/`; the migration split `run.ts` into `run-context.ts`, `agents.ts`, and `agent-{claude,codex,opencode}.ts`, diff --git a/.agents/memory/deno-to-node-migration.md b/.agents/memory/deno-to-node-migration.md deleted file mode 100644 index c40fdf2..0000000 --- a/.agents/memory/deno-to-node-migration.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -description: Every repo moved from Deno to Node + Vite+ in Oct 2026 because Cloudflare bought Deno; nothing goes back to Deno ---- - -On 2026-10-12 the user moved every project from Deno to Node.js + Vite+ -(`vp`). Cloudflare bought Deno on 2026-10-09; the runtime gets roughly 12 -months of maintenance and Deno Deploy shuts down around April 2027. None of the -repos used Deploy or KV, so the move is about the toolchain, not hosting. - -Do not reintroduce `deno.json`, `deno.lock`, `Deno.*` APIs, `jsr:` or `npm:` -specifiers, `mod.ts` entries, or `deno task` anywhere, docs and scripts -included. - -## Order the migration followed - -1. The custom Deno lint plugins were ported to npm first, as - `@cunarist/eslint-plugin-explicit-types`, `-import-check`, and `-lit`. - `cunarist/eslint-plugin-explicit-types` is the template every other repo - copies (config shape, `scripts/project-lint.ts`, AGENTS.md, notes). -2. Then `commons/bot-agents` (six repos use it as an action), then the apps: - home, eximium, stockvibe, brain, memona, memona-home. -3. deno-practice, interview-practice, web-landing, and vemium were left out - (archivable). Old `cunarist/deno-*` repos are archived one at a time, and - only after their `eslint-plugin-*` replacement is published to npm. - -## What "migrated" means - -- Same behavior. A migration PR changes tooling, not features. -- Lint coverage is at least what Deno enforced: [[lint-parity-never-shrinks]]. -- Only three config files and only `.ts`: [[toolchain-node-vite-plus]], - [[ts-only-files-and-imports]]. -- Deno.test suites became Vitest: [[tests-layout-vitest]]. - -The custom plugins' Deno names map like this: - -| Deno | npm | -| ----------------------------------------------- | ---------------------------------------- | -| `jsr:@cunarist/deno-lint-plugin-explicit-types` | `@cunarist/eslint-plugin-explicit-types` | -| `jsr:@cunarist/deno-import-check` (`./lint`) | `@cunarist/eslint-plugin-import-check` | -| `jsr:@cunarist/deno-lint-plugin-lit` | `@cunarist/eslint-plugin-lit` | -| `scripts/deno-lint-plugin.ts` (per app) | `scripts/project-lint.ts` (per app) | - -Until those packages are on npm, a repo that depends on them fails `npm ci` in -CI; that is expected and stated in the PR, never worked around by committing -tarballs or `file:` dependencies. diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index 3655159..787a253 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -1,5 +1,5 @@ --- -description: Shared code comes through # aliases in package.json imports, each directory's entry is index.ts, barrels keep imports few; never ../ out of a directory +description: Shared code comes through # aliases in package.json imports, each src/ subfolder's entry is index.ts, barrels keep imports few; never ../ out of a directory --- ## `#` aliases @@ -44,8 +44,9 @@ so they never reach package consumers. ## `index.ts` entries and barrels -- Every directory's entry file is `index.ts`, never `mod.ts` (decided - 2026-10-12; import-check's `enforce-mod-file` now requires `index.ts`). +- Every `src/` subfolder's entry file is `index.ts`, never `mod.ts` (decided + 2026-10-12; import-check's `enforce-mod-file` requires it only in folders a + `#` alias owns). Folders outside `src/`, such as `scripts/`, have none. - A directory used from outside exposes its API through its `index.ts` barrel, and callers import the barrel (`#ui`), not files inside it (`no-barrel-bypass`). diff --git a/.agents/memory/lint-config-shape.md b/.agents/memory/lint-config-shape.md index 5b1172c..84c6fb5 100644 --- a/.agents/memory/lint-config-shape.md +++ b/.agents/memory/lint-config-shape.md @@ -22,11 +22,10 @@ lint: { - **Default limits stay** (2026-10-16): pedantic's `max-lines` 300, `max-lines-per-function` 50, `max-classes-per-file` 1, `max-dependencies` 10, `max-params` 3, `complexity` 20. Don't raise them to - make code pass; refactor (see [[lint-overrides-policy]]). Apps follow this - strictly. The plugin template already turns off `max-lines`, - `max-lines-per-function`, `typescript/prefer-readonly-parameter-types`, and - `typescript/no-unsafe-enum-comparison` because AST visitors fight them; do - not add new opt-outs anywhere without asking the user. + make code pass; refactor (see [[lint-overrides-policy]]). `max-lines` stays + at 300 everywhere, `scripts/` included, with no override (2026-10-16): split + a long file by theme instead. Do not add new opt-outs anywhere without + asking the user. - **`vite.config.ts` names no rule.** Every rule id, built-in ones included, lives in `configs.project` of `scripts/project-lint.ts` ([[project-lint-plugin]]). `vite.config.ts` only spreads configs. diff --git a/.agents/memory/lit-analyzer-ts6.md b/.agents/memory/lit-analyzer-ts6.md index 4e972d3..ebbb795 100644 --- a/.agents/memory/lit-analyzer-ts6.md +++ b/.agents/memory/lit-analyzer-ts6.md @@ -29,9 +29,10 @@ registerHooks({ ``` - Node runs the `.ts` script directly (type stripping), so no build step. -- **Gotcha:** the obvious `await import("lit-analyzer/cli.js")` fails - `project/no-non-ts-imports` (a `.js` subpath). Import the package entry, or - spawn the package's bin with `node:child_process`, instead. +- Import the package entry, or spawn the package's bin with + `node:child_process`, rather than `await import("lit-analyzer/cli.js")`. + `project/no-asset-imports` allows `.js` subpaths, but the CLI file runs on + import and bypasses the package's public entry. - CI runs `npm run lit-check` as its own step. - It is tooling under `scripts/`: no tests; a narrow `overrides` entry for it (e.g. `no-console`) is allowed if truly needed. diff --git a/.agents/memory/package-json-shape.md b/.agents/memory/package-json-shape.md index 9c56212..714bbed 100644 --- a/.agents/memory/package-json-shape.md +++ b/.agents/memory/package-json-shape.md @@ -1,32 +1,13 @@ --- -description: package.json key order is metadata, scripts, exports, imports, devDependencies, dependencies; standard fields only; no peerDependencies or packageManager +description: package.json keys follow Oxfmt's standard order (vp check --fix); standard fields only; no peerDependencies or packageManager --- -## Key order (2026-10-14) +## Key order (2026-10-16) -Metadata first, then `scripts`, `exports`, `imports`, `devDependencies`, -`dependencies`. Oxfmt would re-sort the file, so `vite.config.ts` sets -`fmt: { sortPackageJson: false }`; keep it, and keep the order by hand. - -```jsonc -{ - "name": "@cunarist/eslint-plugin-x", - "version": "0.1.0", - "description": "...", - "keywords": ["eslint", "eslint-plugin", "eslintplugin", "oxlint"], - "license": "MIT", - "repository": { "type": "git", "url": "git+https://github.com/..." }, - "type": "module", - "files": ["dist"], - "engines": { "node": ">=22" }, - "eslint-doc-generator": {/* tool options that can only live here */}, - "scripts": { "docs": "eslint-doc-generator" }, - "exports": { ".": "./dist/index.js" }, - "imports": { "#helpers": "./src/helpers/index.ts" }, - "devDependencies": { "vite-plus": "^1.1.0" }, - "dependencies": {}, -} -``` +Oxfmt's standard `package.json` sorting is accepted: there is no +`fmt: { sortPackageJson: false }`, and `vp check --fix` sets the order. This +replaces the earlier hand-written order (metadata, `scripts`, `exports`, +`imports`, `devDependencies`, `dependencies`). The `imports` key order can carry meaning: import-check's `enforce-layer-order` reads it as the layer order. Never reorder it casually. diff --git a/.agents/memory/project-lint-plugin.md b/.agents/memory/project-lint-plugin.md index 1d24338..14df71c 100644 --- a/.agents/memory/project-lint-plugin.md +++ b/.agents/memory/project-lint-plugin.md @@ -11,11 +11,15 @@ Oxlint plugin with `meta.name` `"project"`, loaded through `jsPlugins` in plugin configs: Deno parity rules, extras, per-rule options, and opt-outs, built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...). - **Custom rules**, enabled as `project/`. Every repo has - `no-disable-comments` and `no-non-ts-imports`. Apps also port the rules of + `no-disable-comments` and `no-asset-imports`. Apps also port the rules of their old `scripts/deno-lint-plugin.ts` here under the same names and behavior (`no-self-sync`, `app-module-conventions`, `restricted-types`, ...). -Rules and plugin stay in this one file; don't add `scripts/rules/`. +`scripts/` stays flat; don't add `scripts/rules/`. A file that would pass +`max-lines` (300) splits into flat themed files sharing the prefix: +`scripts/project-lint.ts` keeps the plugin object and the rule list and +imports the others (`project-lint-modules.ts`, `project-lint-types.ts`, +`project-lint-lit.ts`, ...). ```ts import type { DummyRuleMap } from "vite-plus/lint"; diff --git a/.agents/memory/publishing-ci-only.md b/.agents/memory/publishing-ci-only.md index 35b07e8..0c2541d 100644 --- a/.agents/memory/publishing-ci-only.md +++ b/.agents/memory/publishing-ci-only.md @@ -23,7 +23,7 @@ Standing convention (2026-10-12), applied to every package: (`--generate-notes`, edited there). There is no `CHANGELOG.md`; never add one. - **The version lives in two places in plugin packages:** `package.json` and - `meta.version` in `src/index.ts`; `tests/package-meta.test.ts` fails if they + `meta.version` in `src/plugin/index.ts`; `tests/package-meta.test.ts` fails if they differ. - **Before 1.0**, a change that breaks users (renamed rule id, a rule that fires on more code, removed behavior) is a minor bump. diff --git a/.agents/memory/toolchain-node-vite-plus.md b/.agents/memory/toolchain-node-vite-plus.md index bce938e..86cff76 100644 --- a/.agents/memory/toolchain-node-vite-plus.md +++ b/.agents/memory/toolchain-node-vite-plus.md @@ -44,8 +44,8 @@ before any push. CI installs the toolchain with `voidzero-dev/setup-vp`. - `vp check` does **not** report `isolatedDeclarations` errors (TS9010); only `vp pack` does, so packages run `vp pack` in CI on every PR. -- Oxfmt sorts `package.json` keys by default, which destroys the required key - order. Set `fmt: { sortPackageJson: false }` ([[package-json-shape]]). +- Oxfmt sorts `package.json` keys by default; that order is accepted, so + there is no `fmt` option for it ([[package-json-shape]]). - Oxlint and Oxfmt skip what `.gitignore` lists; that is the only reason `dist/` and `node_modules/` are not linted. Keep build output gitignored. - Vite+ bundles `vite.config.ts` itself and resolves `#` imports in it. diff --git a/.agents/memory/ts-only-files-and-imports.md b/.agents/memory/ts-only-files-and-imports.md index e365a93..8ac48f8 100644 --- a/.agents/memory/ts-only-files-and-imports.md +++ b/.agents/memory/ts-only-files-and-imports.md @@ -1,5 +1,5 @@ --- -description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included) and source imports only .ts; CSS comes from HTML, JSON/SVG become .ts modules +description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included); imports name only .ts files or .js package subpaths; CSS comes from HTML, JSON/SVG become .ts modules --- The user's rule, from the Deno days and restated for Node (2026-10-12): "No @@ -12,12 +12,14 @@ plain `.ts`. Build output in a gitignored folder is the only JS. `noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX file, stop and flag it instead of adding one. -## Only `.ts` imports (2026-10-14) +## No asset imports (2026-10-14, widened 2026-10-16) -`project/no-non-ts-imports` (in `scripts/project-lint.ts`) fails: +`.ts` and `.js` imports are allowed: our own files stay `.ts`, and `.js` +covers package subpaths such as `lit/decorators.js`. +`project/no-asset-imports` (in `scripts/project-lint.ts`) fails: - a relative, absolute, `#`, or package-subpath specifier whose last segment - has a non-`.ts` extension: `.css`, `.json`, `.svg`, `.js`, `.mjs`, `.tsx`; + has any other extension: `.css`, `.json`, `.svg`, `.mjs`, `.tsx`; - any `?query` suffix (`?raw`, `?url`, `?inline`); - any import attributes (`with { type: "json" }`, `with { type: "text" }`). @@ -44,5 +46,6 @@ import { scenes } from "#data/scenes"; // yes: scenes.ts and fails on drift (the plugin repos do this for `meta.name`/`meta.version`). - **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is fine in tooling and tests. -- **A tool's `.js` entry:** don't `import("pkg/cli.js")`; use the package - entry or spawn the package's bin ([[lit-analyzer-ts6]]). +- **A tool's CLI file:** prefer the package entry or spawning the package's + bin over importing `pkg/cli.js`, which runs the CLI as a side effect + ([[lit-analyzer-ts6]]). diff --git a/.agents/skills/migrate-deno-repo/SKILL.md b/.agents/skills/migrate-deno-repo/SKILL.md deleted file mode 100644 index 36e4373..0000000 --- a/.agents/skills/migrate-deno-repo/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: migrate-deno-repo -description: Checklist for moving a Deno repo (or a leftover Deno part of one) to Node 22+ + Vite+ with npm, same behavior, full lint parity, one PR ---- - -Template: `cunarist/eslint-plugin-explicit-types` on `main`. Read its -`AGENTS.md`, `vite.config.ts`, `scripts/project-lint.ts`, `package.json`, and -CI before starting, and copy their shape. Never ask the user mid-way when -they're away: decide by the memories, record the choice in the PR. - -Branch `node-migration` (apps) or `initial-port` (new plugin repos); one PR -with everything. - -**Inventory first** - -- [ ] List every `deno.json` task, import-map entry, lint rule (tags, - includes, `@hugoalh`, custom plugins), test, `Deno.*` call, JSON/CSS/SVG - import, and every place the output is consumed (Rust, HTML, - Containerfile, CI). - -**Toolchain** - -- [ ] `package.json` in the required key order, `engines.node ">=22"`, no - `packageManager`, no `peerDependencies`; `npm install`; commit - `package-lock.json`. -- [ ] `tsconfig.json` from the template; `vite.config.ts` with `fmt: - { sortPackageJson: false }` and the category-based `lint` block. -- [ ] Delete `deno.json`, `deno.lock`, and Deno-only files. `mod.ts` → - `index.ts`; `deno.json` imports → `package.json` `imports` (`#x/*` - patterns for trailing-slash mappings; aliases may target only direct - subfolders of `src/`, anything else becomes a relative import); - `jsr:`/`npm:` → npm packages. - -**Code** - -- [ ] Only `.ts` files; no non-`.ts` imports: CSS via ``, JSON and SVG - as `.ts` modules, icons in memona's style. -- [ ] `Deno.*` → `node:*` / `process`. Generated JS written as `.ts`, built, - gitignored. -- [ ] Decorators stay standard with `accessor`; add `scripts/decorators.ts` - if the repo has any. Lit repos: `scripts/lit-check.ts` + `npm run - lit-check`. -- [ ] `scripts/` is flat: no subfolders, no `index.ts` (barrels live only in - `src/`); `vite.config.ts` imports `./scripts/.ts` directly. A - scripts subfolder that is really an app moves under `src/`. - -**Lint** - -- [ ] `scripts/project-lint.ts` copied; old local Deno lint rules ported into - it; parity mapping for every former rule; `id-match`; no disable - comments; no source `overrides`; refactor size-rule hits. - -**Tests** - -- [ ] `Deno.test` → Vitest (codemod, then finish TODOs); unit tests next to - code, integration in `tests/`; no case dropped. - -**Build, ship, CI** - -- [ ] `vp build` in app mode with the same output paths, or `vp pack` for - packages. Containerfile node stage on `node:24-alpine`. CI with - `setup-vp`: `vp check`, build, `vp test` (+ lit-check). Packages: - release-triggered `publish.yml`. -- [ ] `AGENTS.md` with the damem header, `.agents/` notes from the damem kit, - README updated (Node.js 22+, no Deno instructions). `damem doctor` - clean. - -**Finish** - -- [ ] `vp check`, `vp test`, build all pass locally; the app runs. -- [ ] Commit in plain imperative sentences; open the PR (summary, changes, - parity mapping, deviations/risks, "CI fails until @cunarist/eslint-plugin-* - are published" if relevant, verification, footer line). -- [ ] Run `review-cycle`. Never merge, publish, or archive. -- 2.52.0 From bfa82698015da9089a0f4af277b18ce047949e98 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 14:45:52 +0900 Subject: [PATCH 07/27] Route tests through a #testing alias, ban ../ imports, and document the alias ownership rule --- .agents/memory/bot-agents-tests-root-only.md | 5 +- .agents/memory/imports-aliases-barrels.md | 94 +++++++++++--------- .agents/memory/lint-overrides-policy.md | 9 +- .agents/memory/package-json-shape.md | 6 ++ .agents/memory/tests-layout-vitest.md | 11 ++- AGENTS.md | 12 ++- package.json | 3 + scripts/project-lint.ts | 2 + src/auth.test.ts | 2 +- src/isolation.test.ts | 2 +- src/post.test.ts | 2 +- src/runtime.test.ts | 2 +- src/testing.ts | 9 +- tests/entrypoints.test.ts | 2 +- tests/workflow.test.ts | 7 +- 15 files changed, 103 insertions(+), 65 deletions(-) diff --git a/.agents/memory/bot-agents-tests-root-only.md b/.agents/memory/bot-agents-tests-root-only.md index e3cb44c..388a705 100644 --- a/.agents/memory/bot-agents-tests-root-only.md +++ b/.agents/memory/bot-agents-tests-root-only.md @@ -17,6 +17,9 @@ as skipped instead of silently passing. `codex`/`claude`/`npx` binaries and a fake Gitea through `protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that for changes to the phase flow ([[bot-agents-runtime-no-build]]). -- `src/testing.ts` holds the shared test helpers; tests follow +- `src/testing.ts` holds the shared test helpers and is the `#testing` file + alias. Every test imports it as `#testing`, never `./testing.ts`, and it + re-exports the source helpers `tests/` needs, since `tests/` may not use + `../`. Tests follow [[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in `tests/`). diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index 787a253..db8b0b7 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -1,75 +1,85 @@ --- -description: Shared code comes through # aliases in package.json imports, each src/ subfolder's entry is index.ts, barrels keep imports few; never ../ out of a directory +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; no folder names in the rule --- -## `#` aliases +## `#` aliases own their targets Shared code is imported through Node subpath imports declared in -`package.json` `imports`, never by a relative path that climbs out of a -directory. +`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. -**Targets (user rule, 2026-10-16):** an alias may point ONLY at a direct -subfolder of `src/`: its `index.ts` (`"#helpers": "./src/helpers/index.ts"`) -or a `#x/*` pattern inside that one folder. Never alias anything outside -`src/` (`scripts/`, root files), `src/` itself (no `#plugin` for -`./src/index.ts`), or a deeper folder (`./src/a/b/`). Code that would need -such an alias uses a relative import instead; `scripts/` and -`vite.config.ts` import each other relatively. Layer rules -(`enforce-layer-order`) must still pass, so reorder entries if needed. +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 and the "import-check checks only aliased folders" scope): + +- **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. import-check'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.** +- **`../` is banned in every file**, owned or not (`no-parent-import`). + 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": { - "#helpers": "./src/helpers/index.ts", - "#components/*": "./src/components/*" + "#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 -import { typeNameTail } from "#helpers"; // yes -import { typeNameTail } from "../helpers/ast.ts"; // no: import/no-relative-parent-imports +// 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 ``` -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. - -- Deno's trailing-slash mappings (`"#components/"`) don't exist in Node; they - become patterns, `"#components/*": "./src/components/*"`. - Aliases point at `.ts` files only. There is no `#package` alias ([[ts-only-files-and-imports]]). -- In repos linted by import-check, the key order of `imports` is the layer - order `enforce-layer-order` checks; add a new alias at the right layer. +- 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. -## `index.ts` entries and barrels +## Barrels -- Every `src/` subfolder's entry file is `index.ts`, never `mod.ts` (decided - 2026-10-12; import-check's `enforce-mod-file` requires it only in folders a - `#` alias owns). Folders outside `src/`, such as `scripts/`, have none. -- A directory used from outside exposes its API through its `index.ts` - barrel, and callers import the barrel (`#ui`), not files inside it - (`no-barrel-bypass`). +- 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 inside one directory (`./sibling.ts`) are fine, with the -`.ts` extension written out. +Relative imports are written with the `.ts` extension. ## `scripts/` is flat (user rule, 2026-10-16) -- `scripts/` has no subfolders and no `index.ts`. Barrels and `index.ts` - entries exist only inside `src/`; delete any `index.ts` outside it. +- `scripts/` has no subfolders and no `index.ts`. - Each tool is one file, `scripts/.ts`, imported directly by file: `import { decorators } from "./scripts/decorators.ts";` in - `vite.config.ts`. + `vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free. - Code in a former `scripts//` folder is merged or renamed into flat `scripts/.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 `src/` as an - aliased folder. -- import-check 0.10.0+ checks folder layout only where the importer or the - target sits in a `#`-aliased folder, so `vite.config.ts` → - `./scripts/x.ts` passes without any override. Remove overrides that only - dodged import-check. + scratch dir). A subfolder that is really an app moves under an alias. diff --git a/.agents/memory/lint-overrides-policy.md b/.agents/memory/lint-overrides-policy.md index 2b552c9..e35f788 100644 --- a/.agents/memory/lint-overrides-policy.md +++ b/.agents/memory/lint-overrides-policy.md @@ -11,9 +11,7 @@ practice. off for `src/rules/index.ts`, the rule registry, which must import every rule. It is not a precedent for apps. - **`scripts/**`:** a narrow override is allowed when a tooling script truly - needs it, e.g. `no-console` in a CLI script. Never one that only dodges - import-check (e.g. `no-relative-bypass` for `vite.config.ts`); import-check - ignores folders no `#` alias owns. + needs it, e.g. `no-console` in a CLI script. - **Generated files:** list them in `ignorePatterns`, never `overrides`. - **Anything else needs the user's approval first.** When the user is away, don't add it: refactor, or leave the violation failing and log the question. @@ -45,3 +43,8 @@ pedantic hits in 274 files, of which `max-lines-per-function` 206, `max-lines` 145, `max-classes-per-file` 59, `max-dependencies` 45 (median 14 imports, worst 29). The user chose refactoring over raising limits or matching memona's old hugoalh limits (2048 lines per file, no function limit). + +Alias rule: an alias may point anywhere and owns its target (a folder's +`index.ts` owns the folder and below, any other file owns only itself); no +two aliases may own the same file, and `../` is banned everywhere +([[imports-aliases-barrels]]). diff --git a/.agents/memory/package-json-shape.md b/.agents/memory/package-json-shape.md index 714bbed..297bee3 100644 --- a/.agents/memory/package-json-shape.md +++ b/.agents/memory/package-json-shape.md @@ -30,3 +30,9 @@ The `imports` key order can carry meaning: import-check's Refresh the lockfile with `npm install` (or `npm install --package-lock-only` for a version bump) and commit it with the change. + +Alias rule: an alias may point anywhere and owns its target (a folder's +`index.ts` owns the folder and everything below it; any other file owns only +itself). No two aliases may own the same file, outside an area only the exact +alias reaches it, and `../` is banned in every file +([[imports-aliases-barrels]]). diff --git a/.agents/memory/tests-layout-vitest.md b/.agents/memory/tests-layout-vitest.md index 1789756..739b5c6 100644 --- a/.agents/memory/tests-layout-vitest.md +++ b/.agents/memory/tests-layout-vitest.md @@ -1,5 +1,5 @@ --- -description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry; scripts/ has no tests +description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry (through its # alias, never ../); scripts/ has no tests --- Decided 2026-10-14 for all repos. @@ -11,9 +11,12 @@ Decided 2026-10-14 for all repos. config needed. Packages publish only `dist/`, so colocated tests cost nothing in the tarball. - **Regression and integration tests** live in the top-level `tests/` folder - and use only the public entry point through a relative import - (`../src/index.ts`; no alias may target `src/` itself, see - [[imports-aliases-barrels]]), never internals. `vp test tests` runs just these. + and use only the public entry point, imported through its `#` alias (e.g. + `#plugin` for `./src/plugin/index.ts`), never internals and never `../`, + which is banned in every file. `vp test tests` runs just these + ([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only + `#testing` (`./src/testing.ts`), which re-exports the few source helpers + those tests need. - **No in-source tests** (`import.meta.vitest`). - **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`, diff --git a/AGENTS.md b/AGENTS.md index 54775b3..357b130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,10 +19,14 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as `src/*.test.ts`; repository-level tests live in `tests/`. -`src/` is flat, so `package.json` has no `imports` aliases and modules, tests, -and `scripts/` import each other relatively. If one is ever added, it may point -only at a direct subfolder of `src/` (its `index.ts`, or a `#x/*` pattern -inside it); never at `src/` itself, a deeper folder, or anything outside `src/`. +An alias in `package.json` `imports` owns its target: a folder's `index.ts` +owns that folder and everything below it, any other file owns only that file. +No file may be owned by 2 aliases. Outside an area, only the exact alias +reaches it (no deeper `#` specifier, no relative path); inside it, relative +imports of siblings and subfolders are free. `../` is banned in every file +(`import/no-relative-parent-imports`). The one alias is `#testing` +(`./src/testing.ts`): every test imports it by alias, and `tests/` reaches +`src/` only through it. Other `src/` files import `./` siblings freely. `scripts/` is flat: one file per tool, no subfolders and no `index.ts` (barrels live only in `src/`), imported directly by file path. diff --git a/package.json b/package.json index 2d3c442..b1e0a87 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "private": true, "description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.", "type": "module", + "imports": { + "#testing": "./src/testing.ts" + }, "devDependencies": { "@eslint/core": "^0.17.0", "@types/node": "^24.19.2", diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index 14d52bb..663997e 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -203,6 +203,8 @@ const rules: DummyRuleMap = { ], // The explicit-types template's extras. "import/no-cycle": "error", + // `../` is banned in every file; anything above a file needs a `#` alias. + "import/no-relative-parent-imports": "error", "typescript/consistent-type-imports": "error", "typescript/no-import-type-side-effects": "error", "typescript/no-non-null-assertion": "error", diff --git a/src/auth.test.ts b/src/auth.test.ts index 67432ac..1a7f334 100644 --- a/src/auth.test.ts +++ b/src/auth.test.ts @@ -9,7 +9,7 @@ import { retryInvalidToken, stageCodexAuth, } from "./auth.ts"; -import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts"; +import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing"; test("retries once after an invalid refresh token", async () => { let attempts = 0; diff --git a/src/isolation.test.ts b/src/isolation.test.ts index 3b8cd78..fbc6451 100644 --- a/src/isolation.test.ts +++ b/src/isolation.test.ts @@ -10,7 +10,7 @@ import { readProfileFile, } from "./isolation.ts"; import type { RuntimeProfile } from "./runtime.ts"; -import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "./testing.ts"; +import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing"; const profile = (uid: number, home: string): RuntimeProfile => ({ index: uid - 20_000, diff --git a/src/post.test.ts b/src/post.test.ts index 737dcab..4a8c914 100644 --- a/src/post.test.ts +++ b/src/post.test.ts @@ -6,7 +6,7 @@ import { isNotFound } from "./errors.ts"; import { postRuntime } from "./post.ts"; import type { PostContext } from "./post.ts"; import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts"; -import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts"; +import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing"; const context: PostContext = { apiUrl: "https://gitea.example/api/v1", diff --git a/src/runtime.test.ts b/src/runtime.test.ts index ff059fc..5f5cd9d 100644 --- a/src/runtime.test.ts +++ b/src/runtime.test.ts @@ -7,7 +7,7 @@ import { assert, expect, test } from "vite-plus/test"; import { output } from "./command.ts"; import { prepareRuntime } from "./runtime.ts"; import type { RuntimeProfile } from "./runtime.ts"; -import { IS_ROOT, makeTempDir } from "./testing.ts"; +import { IS_ROOT, makeTempDir } from "#testing"; const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"; const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA"; diff --git a/src/testing.ts b/src/testing.ts index 0e96f38..24e185e 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -1,4 +1,6 @@ -// Test support: the current user's IDs and a fresh temporary directory. +// Test support: the current user's IDs and a fresh temporary directory, plus +// the source helpers that the repository-level tests in tests/ need. Those +// tests sit outside src/ and reach it only through the `#testing` alias. import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -21,3 +23,8 @@ export const IS_WINDOWS: boolean = process.platform === "win32"; export function makeTempDir(): Promise { return mkdtemp(join(tmpdir(), "bot-agents-test-")); } + +export { output } from "./command.ts"; +export { isNotFound } from "./errors.ts"; +export { isMapping } from "./json.ts"; +export type { JsonObject } from "./json.ts"; diff --git a/tests/entrypoints.test.ts b/tests/entrypoints.test.ts index 68c10ae..587fe31 100644 --- a/tests/entrypoints.test.ts +++ b/tests/entrypoints.test.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url"; import { expect, test } from "vite-plus/test"; -import { output } from "../src/command.ts"; +import { output } from "#testing"; // Each phase runs as `node src/.ts` with no build step. Node loads the // whole import graph, stripping types, before the phase reads its first diff --git a/tests/workflow.test.ts b/tests/workflow.test.ts index 23ff4d0..5a868e7 100644 --- a/tests/workflow.test.ts +++ b/tests/workflow.test.ts @@ -4,11 +4,8 @@ import process from "node:process"; import { assert, expect, test } from "vite-plus/test"; import { parse } from "yaml"; -import { output } from "../src/command.ts"; -import { isNotFound } from "../src/errors.ts"; -import { isMapping } from "../src/json.ts"; -import type { JsonObject } from "../src/json.ts"; -import { makeTempDir } from "../src/testing.ts"; +import { isMapping, isNotFound, makeTempDir, output } from "#testing"; +import type { JsonObject } from "#testing"; const mapping = (value: unknown, at: string): JsonObject => { assert(isMapping(value), at); -- 2.52.0 From d2d3cbffdb15a2f61001bf72ce62931fe49c53ce Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 15:43:47 +0900 Subject: [PATCH 08/27] Switch to the renamed @cunarist/eslint-plugin-modules --- .agents/memory/app-lint-and-local-rules.md | 2 +- .agents/memory/imports-aliases-barrels.md | 4 ++-- .agents/memory/lint-config-shape.md | 2 +- .agents/memory/lint-parity-never-shrinks.md | 2 +- .agents/memory/package-json-shape.md | 2 +- .agents/memory/publishing-ci-only.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.agents/memory/app-lint-and-local-rules.md b/.agents/memory/app-lint-and-local-rules.md index 90f953d..2db4707 100644 --- a/.agents/memory/app-lint-and-local-rules.md +++ b/.agents/memory/app-lint-and-local-rules.md @@ -8,7 +8,7 @@ Apps consume the published plugins as npm devDependencies, through `jsPlugins` and the configs equivalent to what their Deno setup enabled: - `@cunarist/eslint-plugin-explicit-types`: `concrete`, `naming`, `absence` -- `@cunarist/eslint-plugin-import-check`: `import-check` +- `@cunarist/eslint-plugin-modules`: `modules` - `@cunarist/eslint-plugin-lit`: `core`, `strict`, `dom-ref`, `reactive-controller`, `naming` diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index db8b0b7..6af2d06 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -11,13 +11,13 @@ 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 and the "import-check checks only aliased folders" scope): +subfolders" rule and the "the modules plugin checks only aliased folders" scope): - **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. import-check's + alias's area, and no 2 aliases with the same target. the modules 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 diff --git a/.agents/memory/lint-config-shape.md b/.agents/memory/lint-config-shape.md index 84c6fb5..ffe5e0f 100644 --- a/.agents/memory/lint-config-shape.md +++ b/.agents/memory/lint-config-shape.md @@ -38,7 +38,7 @@ lint: { Hand-written code is never ignored. - **No lint disable comments**: [[lint-no-disable-comments]]. - **Parity:** [[lint-parity-never-shrinks]]. **Naming:** [[naming-id-match]]. -- Don't add a second import sorter; import-check's `enforce-import-order` is +- Don't add a second import sorter; the modules plugin's `enforce-import-order` is the only one, and Oxfmt's `sortImports` stays off. `perf` came last: in the template it flagged nine `.includes` on constant diff --git a/.agents/memory/lint-parity-never-shrinks.md b/.agents/memory/lint-parity-never-shrinks.md index 0be5e4d..8e09a23 100644 --- a/.agents/memory/lint-parity-never-shrinks.md +++ b/.agents/memory/lint-parity-never-shrinks.md @@ -29,7 +29,7 @@ Before removing any line from `configs.project`, find what else covers it. in packages. - **Vacuous:** JSX rules (no `.tsx` exists) and Deno-only rules. - **`@hugoalh` rules** map to Oxlint built-ins in `configs.project`. -- **import-check's CLI cycle check** → `import/no-cycle`. +- **deno-import-check's CLI cycle check** → `import/no-cycle`. The template's list in `configs.project` is the baseline to copy: `curly`, `guard-for-in`, `no-console`, `no-empty`, `no-regex-spaces`, diff --git a/.agents/memory/package-json-shape.md b/.agents/memory/package-json-shape.md index 297bee3..62346d4 100644 --- a/.agents/memory/package-json-shape.md +++ b/.agents/memory/package-json-shape.md @@ -9,7 +9,7 @@ Oxfmt's standard `package.json` sorting is accepted: there is no replaces the earlier hand-written order (metadata, `scripts`, `exports`, `imports`, `devDependencies`, `dependencies`). -The `imports` key order can carry meaning: import-check's +The `imports` key order can carry meaning: the modules plugin's `enforce-layer-order` reads it as the layer order. Never reorder it casually. ## Fields diff --git a/.agents/memory/publishing-ci-only.md b/.agents/memory/publishing-ci-only.md index 0c2541d..b212c64 100644 --- a/.agents/memory/publishing-ci-only.md +++ b/.agents/memory/publishing-ci-only.md @@ -32,6 +32,6 @@ Standing convention (2026-10-12), applied to every package: `jsPlugins` and once through ESLint 9 with the configs. On the lit plugin this caught two bugs that 313 unit tests missed. -The cunarist lint plugins (explicit-types, import-check, lit) are published +The cunarist lint plugins (explicit-types, modules, lit) are published together, with a token the user provides; the old `deno-*` repos are archived only after that, one at a time. -- 2.52.0 From 6b87ff66b918516aa7daad012ee845f45b7509eb Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 15:46:05 +0900 Subject: [PATCH 09/27] Fix the quoted old rule name in the alias notes --- .agents/memory/imports-aliases-barrels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/memory/imports-aliases-barrels.md b/.agents/memory/imports-aliases-barrels.md index 6af2d06..92cd60c 100644 --- a/.agents/memory/imports-aliases-barrels.md +++ b/.agents/memory/imports-aliases-barrels.md @@ -11,7 +11,7 @@ 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 and the "the modules plugin checks only aliased folders" scope): +subfolders" rule and the "modules plugin checks only aliased folders" scope): - **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 -- 2.52.0 From c778c00068b8c3fe2b97033be516126cb6deac13 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 15:59:45 +0900 Subject: [PATCH 10/27] Allow .mts and .mjs imports in no-asset-imports --- .agents/memory/lit-analyzer-ts6.md | 2 +- .agents/memory/ts-only-files-and-imports.md | 19 +++++++------ scripts/project-lint.ts | 31 +++++++++++++-------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.agents/memory/lit-analyzer-ts6.md b/.agents/memory/lit-analyzer-ts6.md index ebbb795..abfae86 100644 --- a/.agents/memory/lit-analyzer-ts6.md +++ b/.agents/memory/lit-analyzer-ts6.md @@ -31,7 +31,7 @@ registerHooks({ - Node runs the `.ts` script directly (type stripping), so no build step. - Import the package entry, or spawn the package's bin with `node:child_process`, rather than `await import("lit-analyzer/cli.js")`. - `project/no-asset-imports` allows `.js` subpaths, but the CLI file runs on + `project/no-asset-imports` allows `.js` (and `.mts`/`.mjs`) subpaths, but the CLI file runs on import and bypasses the package's public entry. - CI runs `npm run lit-check` as its own step. - It is tooling under `scripts/`: no tests; a narrow `overrides` entry for it diff --git a/.agents/memory/ts-only-files-and-imports.md b/.agents/memory/ts-only-files-and-imports.md index 8ac48f8..f488cd1 100644 --- a/.agents/memory/ts-only-files-and-imports.md +++ b/.agents/memory/ts-only-files-and-imports.md @@ -1,5 +1,5 @@ --- -description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included); imports name only .ts files or .js package subpaths; CSS comes from HTML, JSON/SVG become .ts modules +description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included); imports name only .ts files or .js/.mts/.mjs package subpaths; CSS comes from HTML, JSON/SVG become .ts modules --- The user's rule, from the Deno days and restated for Node (2026-10-12): "No @@ -12,19 +12,22 @@ plain `.ts`. Build output in a gitignored folder is the only JS. `noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX file, stop and flag it instead of adding one. -## No asset imports (2026-10-14, widened 2026-10-16) +## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17) -`.ts` and `.js` imports are allowed: our own files stay `.ts`, and `.js` -covers package subpaths such as `lit/decorators.js`. -`project/no-asset-imports` (in `scripts/project-lint.ts`) fails: +`project/no-asset-imports` (in `scripts/project-lint.ts`; it was +`no-non-ts-imports` before 2026-10-16) fails: - a relative, absolute, `#`, or package-subpath specifier whose last segment - has any other extension: `.css`, `.json`, `.svg`, `.mjs`, `.tsx`; + has an extension other than `.ts`, `.js`, `.mts`, or `.mjs`: `.css`, + `.json`, `.svg`, `.jsx`, `.tsx`, and every other asset; - any `?query` suffix (`?raw`, `?url`, `?inline`); - any import attributes (`with { type: "json" }`, `with { type: "text" }`). -Bare packages (`lit`, `@types/json-schema`, `lodash.debounce`), `#` aliases, -and `node:` builtins pass. `@fortawesome/x/svgs/a.svg` fails. +`.js`, `.mts`, and `.mjs` are allowed (user decision, 2026-10-17) for package +subpaths such as `lit/decorators.js` or `pdfjs-dist/legacy/build/pdf.mjs`; +our own files are still `.ts` only. `.jsx` and `.tsx` stay banned. Bare +packages (`lit`, `@types/json-schema`, `lodash.debounce`), `#` aliases, and +`node:` builtins pass. `@fortawesome/x/svgs/a.svg` fails. ```ts import "./theme.css"; // no diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index 663997e..13da199 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -70,14 +70,18 @@ const noDisableCommentsRule: ProjectRule = { /** A module specifier read as a path: relative, absolute, or a `#` alias. */ const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u; -/** Script module extensions an import may name: our `.ts` files and `.js` package subpaths. */ -const SCRIPT_EXTENSIONS: readonly string[] = [".ts", ".js"]; +/** + * Script module extensions an import may name: our `.ts` files and `.js`, + * `.mts`, or `.mjs` package subpaths. `.cts`, `.cjs`, `.tsx`, and `.jsx` are + * not among them. + */ +const SCRIPT_EXTENSIONS: ReadonlySet = new Set([".ts", ".js", ".mts", ".mjs"]); /** * Whether `source` names an asset rather than a script module: a query string - * such as `?raw`, or a final path segment with an extension other than `.ts` - * or `.js`. Bare package names are left alone, dots and all, as are - * extensionless paths. + * such as `?raw`, or a final path segment whose extension, from its last dot, + * is not `.ts`, `.js`, `.mts`, or `.mjs`. Bare package names are left alone, + * dots and all, as are extensionless paths. */ function isAssetSource(source: string): boolean { if (source.includes("?")) { @@ -94,25 +98,28 @@ function isAssetSource(source: string): boolean { if (lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".")) { return false; } - return !SCRIPT_EXTENSIONS.some((extension: string): boolean => lastSegment.endsWith(extension)); + const extension: string = lastSegment.slice(lastSegment.lastIndexOf(".")); + return !SCRIPT_EXTENSIONS.has(extension); } /** * `no-asset-imports` — rejects importing anything but a script module: a * source path, static or a dynamic `import()` with a string literal, ending in - * an extension other than `.ts` or `.js` (`.css`, `.json`, `.svg`, `.tsx`) or - * carrying a query string (`?raw`, `?url`), and any import or export with - * import attributes. `.js` covers package subpaths such as - * `lit/decorators.js`. Bare package names, `#` aliases, and `node:` builtins + * an extension other than `.ts`, `.js`, `.mts`, or `.mjs` (`.css`, `.json`, + * `.svg`, `.jsx`, `.tsx`, `.cjs`) or carrying a query string (`?raw`, `?url`), + * and any import or export with import attributes. `.js`, `.mts`, and `.mjs` + * cover package subpaths such as `lit/decorators.js` or + * `pdfjs-dist/legacy/build/pdf.mjs`; our own files are still `.ts` only. Bare + * package names, `#` aliases, and `node:` builtins * without an extension pass; package subpaths are checked like paths. */ const noAssetImportsRule: ProjectRule = { meta: { type: "problem", - docs: { description: "Disallow importing assets instead of .ts or .js modules" }, + docs: { description: "Disallow importing assets instead of .ts, .js, .mts, or .mjs modules" }, schema: [], messages: { - extension: "Only .ts and .js modules may be imported.", + extension: "Only .ts, .js, .mts and .mjs modules may be imported.", attributes: "Import attributes are not allowed.", }, }, -- 2.52.0 From 166476dee92b8c3bbf0c0ca120610a779c91e8c4 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 16:02:20 +0900 Subject: [PATCH 11/27] State alias rule v2 in AGENTS.md --- AGENTS.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 357b130..fdbb1cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,16 +19,20 @@ dependency. Keep the sources to erasable syntax and `.ts` imports, which type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as `src/*.test.ts`; repository-level tests live in `tests/`. -An alias in `package.json` `imports` owns its target: a folder's `index.ts` -owns that folder and everything below it, any other file owns only that file. -No file may be owned by 2 aliases. Outside an area, only the exact alias -reaches it (no deeper `#` specifier, no relative path); inside it, relative -imports of siblings and subfolders are free. `../` is banned in every file -(`import/no-relative-parent-imports`). The one alias is `#testing` -(`./src/testing.ts`): every test imports it by alias, and `tests/` reaches -`src/` only through it. Other `src/` files import `./` siblings freely. -`scripts/` is flat: one file per tool, no subfolders and no `index.ts` -(barrels live only in `src/`), imported directly by file path. +A `#` alias in `package.json` `imports` 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, so no alias inside another alias's area and no 2 +aliases with the same target. Outside an area, only the exact alias reaches +it; a deeper alias specifier or a relative path into the area is a bypass. +Inside an area, relative imports of siblings and subfolders are free. `../` +is banned in every file, owned or not +(`import/no-relative-parent-imports`). Files no alias owns need no `index.ts` +and no alias, and import `./` siblings and subfolders freely. The one alias +is `#testing` (`./src/testing.ts`): every test imports it by alias, and +`tests/` reaches `src/` only through it. `scripts/` is flat: one file per +tool, no subfolders and no `index.ts`, imported directly by file path +([.agents/memory/imports-aliases-barrels.md](.agents/memory/imports-aliases-barrels.md)). The only config files are `package.json`, `tsconfig.json`, and `vite.config.ts`, plus `package-lock.json`. Repo-local lint rules and the rule -- 2.52.0 From 999a089374f13f7501cc60856f60b17d63c8917f Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sat, 17 Oct 2026 18:12:47 +0900 Subject: [PATCH 12/27] Prune notes that don't apply to bot-agents and keep aliases on direct src subfolders --- .agents/memory/app-containers-and-ci.md | 36 -------------- .agents/memory/app-generated-js-as-ts.md | 20 -------- .agents/memory/app-lint-and-local-rules.md | 41 ---------------- .agents/memory/app-vite-build-output.md | 43 ----------------- ...s.md => bot-agents-migration-decisions.md} | 0 .agents/memory/bot-agents-runtime-no-build.md | 7 ++- .agents/memory/bot-agents-tests-root-only.md | 10 ++-- .../memory/decorators-standard-accessor.md | 44 ----------------- .agents/memory/git-pr-workflow.md | 1 - .agents/memory/icons-memona-style.md | 43 ----------------- .agents/memory/imports-aliases-barrels.md | 23 +++++++-- .agents/memory/lint-overrides-policy.md | 2 +- .agents/memory/lit-analyzer-ts6.md | 41 ---------------- .agents/memory/project-lint-plugin.md | 7 +-- .agents/memory/publishing-ci-only.md | 37 --------------- .agents/memory/tests-layout-vitest.md | 7 ++- .agents/memory/ts-only-files-and-imports.md | 5 +- AGENTS.md | 7 ++- package.json | 2 +- src/testing.ts | 30 ------------ src/testing/index.ts | 47 +++++++++++++++++++ tests/entrypoints.test.ts | 11 ++--- tests/workflow.test.ts | 21 +++++---- 23 files changed, 105 insertions(+), 380 deletions(-) delete mode 100644 .agents/memory/app-containers-and-ci.md delete mode 100644 .agents/memory/app-generated-js-as-ts.md delete mode 100644 .agents/memory/app-lint-and-local-rules.md delete mode 100644 .agents/memory/app-vite-build-output.md rename .agents/memory/{app-repo-specific-decisions.md => bot-agents-migration-decisions.md} (100%) delete mode 100644 .agents/memory/decorators-standard-accessor.md delete mode 100644 .agents/memory/icons-memona-style.md delete mode 100644 .agents/memory/lit-analyzer-ts6.md delete mode 100644 .agents/memory/publishing-ci-only.md delete mode 100644 src/testing.ts create mode 100644 src/testing/index.ts diff --git a/.agents/memory/app-containers-and-ci.md b/.agents/memory/app-containers-and-ci.md deleted file mode 100644 index 58f77cc..0000000 --- a/.agents/memory/app-containers-and-ci.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -description: Containerfiles build the frontend in a node:24-alpine stage with npm ci and vp build; CI uses setup-vp and runs vp check, vp build, vp test (plus lit-check) ---- - -## Containerfiles - -- The frontend build stage is `docker.io/library/node:24-alpine`, replacing - `denoland/deno:alpine`. Vite+ publishes `linux-x64-musl` binaries, so Alpine - works. (`ghcr.io/voidzero-dev/vite-plus` is a glibc alternative that would - need a pinned digest; not used.) -- The stage copies `package.json` and `package-lock.json` first, runs - `npm ci`, then copies the sources and configs and runs `npx vp build`. -- The runtime stage copies the built `js/` and `css/` directories (not single - files) from the node stage. -- No Deno binary, `deno install`, or `deno task` remains in any stage. - -```dockerfile -FROM docker.io/library/node:24-alpine AS frontend -WORKDIR /src/frontend -COPY frontend/package.json frontend/package-lock.json ./ -RUN npm ci -COPY frontend/ ./ -RUN npx vp build -``` - -## CI - -- GitHub repos (temeddix): `.github/workflows`. Capsulizers repos (Gitea): - `.gitea/workflows`. Don't move a repo's CI between hosts. -- Install with `voidzero-dev/setup-vp`, then run `vp check`, `vp build`, and - `vp test`; Lit repos also run `npm run lit-check` - ([[lit-analyzer-ts6]]). -- Repos that ran CI on a self-hosted runner keep it. -- Until `@cunarist/eslint-plugin-*` are on npm, `npm ci` fails in CI. That is - expected and stated in the PR; run `npm install` once they are published - and commit the refreshed lockfile. diff --git a/.agents/memory/app-generated-js-as-ts.md b/.agents/memory/app-generated-js-as-ts.md deleted file mode 100644 index 39b0b4a..0000000 --- a/.agents/memory/app-generated-js-as-ts.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -description: Code that must ship as standalone JS (memona framed_find.js, brain stream-recovery.js) is written as .ts, compiled at build time, and its output gitignored ---- - -Some apps ship plain JavaScript that runs outside the main bundle: memona's -`framed_find.js` and brain's `stream-recovery.js`. Under the TS-only rule -([[ts-only-files-and-imports]]) they are handled like this (2026-10-16): - -- The source is a `.ts` file, linted and type-checked like everything else. -- The `.js` is produced at build time by the existing build (a Vite input or - a build step in `vite.config.ts` or `scripts/`), never committed. -- The output path is gitignored and, if it lands in a linted folder, listed - in `lint.ignorePatterns`; never in `overrides`, never with disable comments. -- Whatever loads the file (Rust `include_str!`, a container `COPY`, an HTML - `