Prune notes that don't apply to bot-agents and keep aliases on direct src subfolders
Check / check (pull_request) Successful in 38s
Check / check (pull_request) Successful in 38s
This commit is contained in:
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
`<script>`) keeps the same path and name as before, so behavior is
|
|
||||||
unchanged. Builds that need the file must run the compile first; check
|
|
||||||
that the Containerfile and CI order the steps that way.
|
|
||||||
|
|
||||||
Don't commit a hand-maintained `.js` copy "for convenience", and don't turn
|
|
||||||
it into `.mjs` or `.cjs`.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
description: Apps use the npm lint plugins' configs, port their old deno-lint-plugin.ts rules into scripts/project-lint.ts, never override source files, and refactor instead of raising limits
|
|
||||||
---
|
|
||||||
|
|
||||||
## Plugins
|
|
||||||
|
|
||||||
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-modules`: `modules`
|
|
||||||
- `@cunarist/eslint-plugin-lit`: `core`, `strict`, `dom-ref`,
|
|
||||||
`reactive-controller`, `naming`
|
|
||||||
|
|
||||||
## Repo-local rules
|
|
||||||
|
|
||||||
The local `scripts/deno-lint-plugin.ts` that home, eximium, stockvibe, brain,
|
|
||||||
memona, and memona-home carried is ported into `scripts/project-lint.ts`
|
|
||||||
([[project-lint-plugin]]) under the `project` namespace, same rule names and
|
|
||||||
behavior. Depending on the repo: `no-self-sync`, `no-sync-in-render`,
|
|
||||||
`app-module-conventions`, `always-wildcard-export`, `no-class-extends`,
|
|
||||||
`no-lit-nothing`, `no-title-attribute`, `prefer-wa-components`,
|
|
||||||
`no-satisfies`, `no-unknown`, `restricted-types`, `no-line-lint-ignore`,
|
|
||||||
`no-app-disposables-in-component`, `no-crepe-react-ui`. `@hugoalh` rules map to
|
|
||||||
Oxlint built-ins in `configs.project`.
|
|
||||||
|
|
||||||
memona's `deno-lint-plugin.ts` and its `coding-standards` memory are the house
|
|
||||||
style the explicit-types rules came from; they win any conflict.
|
|
||||||
|
|
||||||
## No overrides, refactor instead
|
|
||||||
|
|
||||||
Apps use **no `overrides` for source files at all** (the rule-registry
|
|
||||||
exception exists only in plugin repos). Oxlint's default limits stay, and
|
|
||||||
violations are refactored without behavior change in the same PR: barrels for
|
|
||||||
`max-dependencies`, splits for `max-lines`/`max-lines-per-function`/
|
|
||||||
`max-classes-per-file`, an options object for `max-params`. Long Lit
|
|
||||||
`render()` templates are split into smaller template functions or
|
|
||||||
components. See [[lint-overrides-policy]].
|
|
||||||
|
|
||||||
If a hand-written exception still looks truly necessary, ask the user first;
|
|
||||||
when they're away, leave it failing or refactor, and log the question.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
description: Frontends build with vp build in Vite app mode, keeping the exact output paths the Rust backend and HTML expect; emptyOutDir false when outDir holds committed files
|
|
||||||
---
|
|
||||||
|
|
||||||
`deno bundle` was replaced by `vp build` in **Vite app mode** (not
|
|
||||||
`build.lib`). Behavior must not change, so the output keeps the file names and
|
|
||||||
locations the backend serves and the HTML references.
|
|
||||||
|
|
||||||
Typical shape for the Rust-backed frontends (home, eximium, stockvibe,
|
|
||||||
memona-home), where `frontend/` builds into `backend/static`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
build: {
|
|
||||||
outDir: "../backend/static",
|
|
||||||
emptyOutDir: false, // the folder holds committed files (index.html, ...)
|
|
||||||
rollupOptions: {
|
|
||||||
input: { bundle: "src/index.ts" },
|
|
||||||
output: { entryFileNames: "js/[name].js" }, // -> js/bundle.js
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Fixed names, no hashes:** set `entryFileNames` (and `assetFileNames`
|
|
||||||
where assets are emitted) explicitly. Vite's hashed defaults would 404,
|
|
||||||
since Rust and the templates request `js/bundle.js` and `css/theme.css`.
|
|
||||||
- **`emptyOutDir: false` whenever `outDir` is outside the Vite root or holds
|
|
||||||
committed files.** Never let it default to true there: it would wipe them.
|
|
||||||
- Output stays gitignored, so `.gitignore`, `.containerignore`, and the Rust
|
|
||||||
side need no edits; home's startup check for `js/bundle.js` and
|
|
||||||
`css/theme.css` keeps passing.
|
|
||||||
- **CSS is never imported from TS** (no `import "./theme.css"`); the HTML
|
|
||||||
links the stylesheet. How the CSS file is produced is per repo; read the
|
|
||||||
repo's `vite.config.ts` before changing it.
|
|
||||||
- Copy whole `js/` and `css/` directories in Containerfiles, not single files,
|
|
||||||
so code-split chunks ship too ([[app-containers-and-ci]]).
|
|
||||||
- Where a repo serves an `index.html` entry (brain's dashboard), use plain app
|
|
||||||
mode with `index.html` as input, `base` set to the mount path, and `outDir:
|
|
||||||
"dist"` with the default `emptyOutDir`.
|
|
||||||
- `vp dev` must work too, and the decorators plugin must cover build, dev, and
|
|
||||||
test ([[decorators-standard-accessor]]).
|
|
||||||
|
|
||||||
Check the repo's own migration PR for its final values before editing; these
|
|
||||||
are the conventions it followed, not a config to paste.
|
|
||||||
@@ -17,15 +17,14 @@ properties, no decorators) and relative `.ts` imports.
|
|||||||
phase. It copies exactly `src/*.ts`, `package.json` (`"type": "module"`
|
phase. It copies exactly `src/*.ts`, `package.json` (`"type": "module"`
|
||||||
makes Node load the sources as ES modules), `profiles.json`, and
|
makes Node load the sources as ES modules), `profiles.json`, and
|
||||||
`prompt.md`. **A new runtime file or subdirectory must be added there**, or
|
`prompt.md`. **A new runtime file or subdirectory must be added there**, or
|
||||||
it is missing at run time. `src/` is flat; a subfolder would not be copied.
|
it is missing at run time. Runtime code in `src/` is flat; a subfolder would
|
||||||
|
not be copied. The only subfolder, `src/testing/`, is test-only.
|
||||||
- `tests/entrypoints.test.ts` runs each phase under real Node with a cleared
|
- `tests/entrypoints.test.ts` runs each phase under real Node with a cleared
|
||||||
environment and expects its first "X is not set" error, which proves the
|
environment and expects its first "X is not set" error, which proves the
|
||||||
whole import graph loads without a build. Keep it in sync with the phases.
|
whole import graph loads without a build. Keep it in sync with the phases.
|
||||||
- **None of the app build conventions apply here:** no `vp build`, no
|
- **None of the app build conventions apply here:** no `vp build`, no
|
||||||
frontend or Rust/Tauri output paths, no Containerfile, no decorators
|
frontend or Rust/Tauri output paths, no Containerfile, no decorators
|
||||||
plugin, no lit-check, no generated JS ([[app-vite-build-output]],
|
plugin, no lit-check, no generated JS. Don't add any of them.
|
||||||
[[app-containers-and-ci]], [[app-generated-js-as-ts]] describe the other
|
|
||||||
apps). Don't add any of them.
|
|
||||||
- **CI** (`.gitea/workflows/check.yml`) runs on the self-hosted runner's
|
- **CI** (`.gitea/workflows/check.yml`) runs on the self-hosted runner's
|
||||||
default image as root: `npm ci`, `npx vp check`, `npx vp test`. It does not
|
default image as root: `npm ci`, `npx vp check`, `npx vp test`. It does not
|
||||||
use `setup-vp`. YAML stays declarative ([[yaml-stays-declarative]]).
|
use `setup-vp`. YAML stays declarative ([[yaml-stays-declarative]]).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ description: Isolation and credential tests need root and use test.skipIf(!IS_RO
|
|||||||
|
|
||||||
Some tests exercise real UID isolation, process cleanup, and `prepare.ts`
|
Some tests exercise real UID isolation, process cleanup, and `prepare.ts`
|
||||||
credential cleanup. They need root and are written as
|
credential cleanup. They need root and are written as
|
||||||
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `src/testing.ts`), so they show
|
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show
|
||||||
as skipped instead of silently passing.
|
as skipped instead of silently passing.
|
||||||
|
|
||||||
- As a normal user `vp test` passes 56 and skips 5. In CI (self-hosted
|
- As a normal user `vp test` passes 56 and skips 5. In CI (self-hosted
|
||||||
@@ -17,9 +17,9 @@ as skipped instead of silently passing.
|
|||||||
`codex`/`claude`/`npx` binaries and a fake Gitea through
|
`codex`/`claude`/`npx` binaries and a fake Gitea through
|
||||||
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
|
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
|
||||||
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
|
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
|
||||||
- `src/testing.ts` holds the shared test helpers and is the `#testing` file
|
- `src/testing/index.ts` holds the shared test helpers (user IDs, temp dirs,
|
||||||
alias. Every test imports it as `#testing`, never `./testing.ts`, and it
|
`runCommand`) behind the `#testing` alias. Every test imports it as
|
||||||
re-exports the source helpers `tests/` needs, since `tests/` may not use
|
`#testing`. It imports no source module, so `tests/` (which may not use
|
||||||
`../`. Tests follow
|
`../`) never reaches internals. Tests follow
|
||||||
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
|
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
|
||||||
`tests/`).
|
`tests/`).
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
description: Keep TC39 standard decorators with accessor; Oxc can't lower them, so scripts/decorators.ts (esbuild Vite plugin) does; never legacy decorators
|
|
||||||
---
|
|
||||||
|
|
||||||
Applies to every repo that uses decorators (the Lit frontends).
|
|
||||||
|
|
||||||
**Decision (2026-10-16):** keep standard (TC39) decorators with `accessor`,
|
|
||||||
exactly as the Deno code had them. No legacy decorators.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
@customElement("app-card")
|
|
||||||
export class AppCard extends LitElement {
|
|
||||||
@property() accessor name = ""; // keep this form
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gotcha, verified 2026-10-16:** Vite 8's Oxc transformer parses standard
|
|
||||||
decorators but can **not** lower them, so `vp build`, `vp dev`, and `vp test`
|
|
||||||
emit decorator syntax browsers and Node don't run. Check again on each Vite+
|
|
||||||
upgrade; when Oxc can lower them, delete the plugin and the `esbuild`
|
|
||||||
devDependency.
|
|
||||||
|
|
||||||
**Fix in use:** `scripts/decorators.ts`, a small Vite plugin backed by
|
|
||||||
`esbuild` (devDependency), registered in `vite.config.ts` `plugins` so it
|
|
||||||
covers build, dev server, and Vitest alike:
|
|
||||||
|
|
||||||
- `enforce: "pre"`, so it runs before Oxc;
|
|
||||||
- transforms only `.ts` modules whose source contains `@`;
|
|
||||||
- `esbuild.transform(code, { loader: "ts", target: "es2022", sourcemap,
|
|
||||||
sourcefile: id })`, returning `{ code, map }`.
|
|
||||||
|
|
||||||
It is tooling under `scripts/`: no tests, and it must pass every lint rule.
|
|
||||||
|
|
||||||
**The options as presented to the user:**
|
|
||||||
|
|
||||||
| Option | What | Why not |
|
|
||||||
| ------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
||||||
| A | Legacy decorators: two tsconfig flags (`experimentalDecorators`, ...) and a codemod removing `accessor` | the user ruled out legacy decorators |
|
|
||||||
| B | Wait for Oxc to lower `accessor` decorators | not available yet; blocks the migration |
|
|
||||||
| C | No decorators: `static properties = {...}` + `declare` fields | rewrites every component |
|
|
||||||
| D | A transform step in the Vite build | chosen, with esbuild as the transformer (above) |
|
|
||||||
|
|
||||||
Don't add `experimentalDecorators` to `tsconfig.json`, and don't strip
|
|
||||||
`accessor` from fields.
|
|
||||||
@@ -9,7 +9,6 @@ description: Work on a branch, plain imperative commits without AI trailers, one
|
|||||||
- Commit messages are plain imperative sentences ("Port the import-check
|
- Commit messages are plain imperative sentences ("Port the import-check
|
||||||
rules to an ESLint plugin"). No conventional-commit prefixes, no AI
|
rules to an ESLint plugin"). No conventional-commit prefixes, no AI
|
||||||
trailers (`Co-Authored-By: Claude`, "Generated with ..."), no emoji.
|
trailers (`Co-Authored-By: Claude`, "Generated with ..."), no emoji.
|
||||||
- A version bump is its own commit titled `Version X.Y.Z` ([[publishing-ci-only]]).
|
|
||||||
- When several agents share a branch, each works in its own `git worktree`,
|
- When several agents share a branch, each works in its own `git worktree`,
|
||||||
runs `git pull --rebase` before pushing, and retries a rejected push after
|
runs `git pull --rebase` before pushing, and retries a rejected push after
|
||||||
rebasing. Remove the worktree and temp branch afterwards.
|
rebasing. Remove the worktree and temp branch afterwards.
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
description: Icons are .ts modules exporting a typed Lit svg template (memona's style), one per file behind an index.ts; never .svg imports as text
|
|
||||||
---
|
|
||||||
|
|
||||||
memona's way of declaring icons is the standard (2026-10-14). An icon is a
|
|
||||||
`.ts` module that exports a Lit `TemplateResult` holding the inline SVG:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// src/icons/lock.ts
|
|
||||||
import { html, type TemplateResult } from "lit";
|
|
||||||
|
|
||||||
/** Font Awesome Free's solid `lock`, as its SVG source. */
|
|
||||||
export const lockIcon: TemplateResult = html`<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 384 512"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="M128 96l0 64 ..." />
|
|
||||||
</svg>`;
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// usage
|
|
||||||
import { lockIcon } from "#icons";
|
|
||||||
render() { return html`<span class="icon">${lockIcon}</span>`; }
|
|
||||||
```
|
|
||||||
|
|
||||||
- One icon per file, camelCase name with an `Icon` suffix, a doc comment
|
|
||||||
saying what it depicts, exported through `src/icons/index.ts`.
|
|
||||||
- Use `fill="currentColor"` so CSS controls the colour.
|
|
||||||
- Keep a third-party licence comment that the SVG source carries (Font
|
|
||||||
Awesome Free's `<!--! ... -->` notice) inside the template.
|
|
||||||
|
|
||||||
**Replaced pattern:** eximium, stockvibe, and memona-home imported Font
|
|
||||||
Awesome `.svg` files as text in `icons.ts`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import lock from "@fortawesome/fontawesome-free/svgs/solid/lock.svg" with { type: "text" }; // no
|
|
||||||
```
|
|
||||||
|
|
||||||
That breaks [[ts-only-files-and-imports]] (non-`.ts` import plus import
|
|
||||||
attributes) and was converted to memona's style during the frontend
|
|
||||||
migration. Before writing new icon code in any repo, read how that repo (or
|
|
||||||
memona) already declares icons and match it.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
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
|
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
|
## `#` aliases own their targets
|
||||||
@@ -11,7 +11,9 @@ 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
|
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/`
|
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
|
||||||
subfolders" rule and the "modules plugin checks only aliased folders" scope):
|
subfolders" rule as the general rule, and the "modules 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
|
- **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
|
folder's `index.ts` owns that folder and everything below it; an alias to
|
||||||
@@ -77,9 +79,24 @@ Relative imports are written with the `.ts` extension.
|
|||||||
|
|
||||||
- `scripts/` has no subfolders and no `index.ts`.
|
- `scripts/` has no subfolders and no `index.ts`.
|
||||||
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
||||||
`import { decorators } from "./scripts/decorators.ts";` in
|
`import projectLint from "./scripts/project-lint.ts";` in
|
||||||
`vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free.
|
`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
|
- 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
|
`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
|
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.
|
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 modules 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 one alias is `"#testing": "./src/testing/index.ts"`.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ covered by the tests, inside the same migration PR.
|
|||||||
| ------------------------- | ------------------------------------------------------- |
|
| ------------------------- | ------------------------------------------------------- |
|
||||||
| `import/max-dependencies` | import a barrel (`#ui`) instead of many direct modules |
|
| `import/max-dependencies` | import a barrel (`#ui`) instead of many direct modules |
|
||||||
| `max-lines` | split the file by responsibility |
|
| `max-lines` | split the file by responsibility |
|
||||||
| `max-lines-per-function` | extract helpers; split a long Lit template into parts |
|
| `max-lines-per-function` | extract helpers |
|
||||||
| `max-classes-per-file` | one class per file |
|
| `max-classes-per-file` | one class per file |
|
||||||
| `max-params` | pass one options object |
|
| `max-params` | pass one options object |
|
||||||
| `prefer-set-has` | `const K: ReadonlySet<string> = new Set([...])`, `.has` |
|
| `prefer-set-has` | `const K: ReadonlySet<string> = new Set([...])`, `.has` |
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
description: lit-analyzer runs from scripts/lit-check.ts, which maps "typescript" to the aliased typescript-6 devDependency; npm run lit-check in CI
|
|
||||||
---
|
|
||||||
|
|
||||||
Applies to Lit repos that ran `cunarist/deno-lit-analyzer` under Deno.
|
|
||||||
|
|
||||||
lit-analyzer is built on TypeScript's JavaScript compiler API and is run
|
|
||||||
against TypeScript 6 (the open question was whether it works with TS 7 /
|
|
||||||
tsgo). The repo installs TS 6 under another name and redirects lit-analyzer
|
|
||||||
to it, so the rest of the toolchain is unaffected (decided 2026-10-16):
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
// package.json
|
|
||||||
"scripts": { "lit-check": "node scripts/lit-check.ts" },
|
|
||||||
"devDependencies": { "typescript-6": "npm:typescript@^6", "lit-analyzer": "..." }
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// scripts/lit-check.ts (shape)
|
|
||||||
import { registerHooks } from "node:module";
|
|
||||||
|
|
||||||
registerHooks({
|
|
||||||
resolve(specifier, context, next) {
|
|
||||||
// when lit-analyzer asks for "typescript", hand it TS 6
|
|
||||||
return next(specifier === "typescript" ? "typescript-6" : specifier, context);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// then run lit-analyzer
|
|
||||||
```
|
|
||||||
|
|
||||||
- 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` (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
|
|
||||||
(e.g. `no-console`) is allowed if truly needed.
|
|
||||||
|
|
||||||
Don't replace this with a global TypeScript downgrade, a second
|
|
||||||
`tsconfig`, or a `.js` wrapper script.
|
|
||||||
@@ -11,15 +11,12 @@ Oxlint plugin with `meta.name` `"project"`, loaded through `jsPlugins` in
|
|||||||
plugin configs: Deno parity rules, extras, per-rule options, and opt-outs,
|
plugin configs: Deno parity rules, extras, per-rule options, and opt-outs,
|
||||||
built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...).
|
built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...).
|
||||||
- **Custom rules**, enabled as `project/<rule>`. Every repo has
|
- **Custom rules**, enabled as `project/<rule>`. Every repo has
|
||||||
`no-disable-comments` and `no-asset-imports`. Apps also port the rules of
|
`no-disable-comments` and `no-asset-imports`; bot-agents has no others.
|
||||||
their old `scripts/deno-lint-plugin.ts` here under the same names and
|
|
||||||
behavior (`no-self-sync`, `app-module-conventions`, `restricted-types`, ...).
|
|
||||||
|
|
||||||
`scripts/` stays flat; don't add `scripts/rules/`. A file that would pass
|
`scripts/` stays flat; don't add `scripts/rules/`. A file that would pass
|
||||||
`max-lines` (300) splits into flat themed files sharing the prefix:
|
`max-lines` (300) splits into flat themed files sharing the prefix:
|
||||||
`scripts/project-lint.ts` keeps the plugin object and the rule list and
|
`scripts/project-lint.ts` keeps the plugin object and the rule list and
|
||||||
imports the others (`project-lint-modules.ts`, `project-lint-types.ts`,
|
imports the others (`project-lint-modules.ts`, `project-lint-types.ts`, ...).
|
||||||
`project-lint-lit.ts`, ...).
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type { DummyRuleMap } from "vite-plus/lint";
|
import type { DummyRuleMap } from "vite-plus/lint";
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
description: Packages publish only from CI when a GitHub release vX.Y.Z is published, via npm trusted publishing; never npm publish by hand; notes live only on the release page
|
|
||||||
---
|
|
||||||
|
|
||||||
Standing convention (2026-10-12), applied to every package:
|
|
||||||
|
|
||||||
- **Only CI publishes.** Publishing a GitHub release `vX.Y.Z` triggers
|
|
||||||
`.github/workflows/publish.yml`. Never run `npm publish` locally and never
|
|
||||||
push a bare `v*` tag. Use the `release` skill, and only when the user asked
|
|
||||||
for a release.
|
|
||||||
- **The workflow** checks that the tag equals `v` + `package.json` `version`,
|
|
||||||
runs the full CI as a reusable workflow (every Node version in the matrix),
|
|
||||||
then `vp pack` and `npm publish --access public --provenance`. A release
|
|
||||||
marked pre-release publishes under the `next` dist-tag, others `latest`.
|
|
||||||
It runs on Node 24 (npm 11, which trusted publishing needs) with no cache.
|
|
||||||
- **Auth is npm trusted publishing** (OIDC, `id-token: write`), which also
|
|
||||||
attaches provenance. npm only allows a trusted publisher on a package that
|
|
||||||
already exists, so the very first release bootstraps with a short-lived
|
|
||||||
token in the `NPM_TOKEN` repo secret (the workflow uses it only when set).
|
|
||||||
Afterwards the user configures the trusted publisher for `publish.yml`,
|
|
||||||
deletes the secret, and sets the package to disallow tokens.
|
|
||||||
- **Release notes live only on the GitHub release page**
|
|
||||||
(`--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/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.
|
|
||||||
- **Before a release**, pack the package and lint a known-bad and a known-good
|
|
||||||
file from separate consumer projects, once through `vp lint` with
|
|
||||||
`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, modules, lit) are published
|
|
||||||
together, with a token the user provides; the old `deno-*` repos are archived
|
|
||||||
only after that, one at a time.
|
|
||||||
@@ -15,12 +15,11 @@ Decided 2026-10-14 for all repos.
|
|||||||
`#plugin` for `./src/plugin/index.ts`), never internals and never `../`,
|
`#plugin` for `./src/plugin/index.ts`), never internals and never `../`,
|
||||||
which is banned in every file. `vp test tests` runs just these
|
which is banned in every file. `vp test tests` runs just these
|
||||||
([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only
|
([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only
|
||||||
`#testing` (`./src/testing.ts`), which re-exports the few source helpers
|
`#testing` (`./src/testing/index.ts`), plain helpers that import no source
|
||||||
those tests need.
|
module.
|
||||||
- **No in-source tests** (`import.meta.vitest`).
|
- **No in-source tests** (`import.meta.vitest`).
|
||||||
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
|
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
|
||||||
production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`,
|
production code (`project-lint.ts`, `protect-source.sh`).
|
||||||
release helpers).
|
|
||||||
- Test files follow every lint rule; no relaxed overrides for tests.
|
- Test files follow every lint rule; no relaxed overrides for tests.
|
||||||
|
|
||||||
## Migrating Deno tests
|
## Migrating Deno tests
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { scenes } from "#data/scenes"; // yes: scenes.ts
|
|||||||
import CSS from TS, not even for Vite. (This also removes most of the
|
import CSS from TS, not even for Vite. (This also removes most of the
|
||||||
`import/no-unassigned-import` hits the `suspicious` category reports.)
|
`import/no-unassigned-import` hits the `suspicious` category reports.)
|
||||||
- **JSON data:** turn it into a `.ts` module exporting a typed constant.
|
- **JSON data:** turn it into a `.ts` module exporting a typed constant.
|
||||||
- **SVG:** a `.ts` module exporting a Lit template; see [[icons-memona-style]].
|
- **SVG:** a `.ts` module exporting the markup.
|
||||||
- **`package.json` fields at runtime:** never `import pkg from
|
- **`package.json` fields at runtime:** never `import pkg from
|
||||||
"./package.json"` and there is no `#package` alias. Write the value out in
|
"./package.json"` and there is no `#package` alias. Write the value out in
|
||||||
`.ts` and add a test in `tests/` that reads `package.json` with `node:fs`
|
`.ts` and add a test in `tests/` that reads `package.json` with `node:fs`
|
||||||
@@ -50,5 +50,4 @@ import { scenes } from "#data/scenes"; // yes: scenes.ts
|
|||||||
- **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is
|
- **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is
|
||||||
fine in tooling and tests.
|
fine in tooling and tests.
|
||||||
- **A tool's CLI file:** prefer the package entry or spawning the package's
|
- **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
|
bin over importing `pkg/cli.js`, which runs the CLI as a side effect.
|
||||||
([[lit-analyzer-ts6]]).
|
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ Inside an area, relative imports of siblings and subfolders are free. `../`
|
|||||||
is banned in every file, owned or not
|
is banned in every file, owned or not
|
||||||
(`import/no-relative-parent-imports`). Files no alias owns need no `index.ts`
|
(`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
|
and no alias, and import `./` siblings and subfolders freely. The one alias
|
||||||
is `#testing` (`./src/testing.ts`): every test imports it by alias, and
|
is `#testing` (`./src/testing/index.ts`): every test imports it by alias, and
|
||||||
`tests/` reaches `src/` only through it. `scripts/` is flat: one file per
|
`tests/` reaches `src/` only through it. Local convention, stricter than the
|
||||||
|
general rule above: in this repo, aliases point only at a direct subfolder of
|
||||||
|
`src/` (`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses
|
||||||
|
`./` relative imports. `scripts/` is flat: one file per
|
||||||
tool, no subfolders and no `index.ts`, imported directly by file path
|
tool, no subfolders and no `index.ts`, imported directly by file path
|
||||||
([.agents/memory/imports-aliases-barrels.md](.agents/memory/imports-aliases-barrels.md)).
|
([.agents/memory/imports-aliases-barrels.md](.agents/memory/imports-aliases-barrels.md)).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
|
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#testing": "./src/testing.ts"
|
"#testing": "./src/testing/index.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// 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";
|
|
||||||
import process from "node:process";
|
|
||||||
|
|
||||||
export function currentUid(): number {
|
|
||||||
return process.getuid?.() ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function currentGid(): number {
|
|
||||||
return process.getgid?.() ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The isolation tests switch UIDs, which only root may do.
|
|
||||||
export const IS_ROOT: boolean = currentUid() === 0;
|
|
||||||
|
|
||||||
// Symlinks and POSIX modes, as these tests use them, need a Unix host.
|
|
||||||
export const IS_WINDOWS: boolean = process.platform === "win32";
|
|
||||||
|
|
||||||
export function makeTempDir(): Promise<string> {
|
|
||||||
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";
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Test support: the current user's IDs, a fresh temporary directory, and a
|
||||||
|
// plain command runner. It imports no source module, so the repository-level
|
||||||
|
// tests in tests/, which reach src/ only through `#testing`, never touch
|
||||||
|
// internals.
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { once } from "node:events";
|
||||||
|
import { mkdtemp } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import { text } from "node:stream/consumers";
|
||||||
|
|
||||||
|
export function currentUid(): number {
|
||||||
|
return process.getuid?.() ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentGid(): number {
|
||||||
|
return process.getgid?.() ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The isolation tests switch UIDs, which only root may do.
|
||||||
|
export const IS_ROOT: boolean = currentUid() === 0;
|
||||||
|
|
||||||
|
// Symlinks and POSIX modes, as these tests use them, need a Unix host.
|
||||||
|
export const IS_WINDOWS: boolean = process.platform === "win32";
|
||||||
|
|
||||||
|
export function makeTempDir(): Promise<string> {
|
||||||
|
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandResult {
|
||||||
|
success: boolean;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs a command with stdin closed and only the given environment, and
|
||||||
|
// collects its stderr.
|
||||||
|
export async function runCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
env: Record<string, string>,
|
||||||
|
): Promise<CommandResult> {
|
||||||
|
const child = spawn(command, args, { env, stdio: ["ignore", "ignore", "pipe"] });
|
||||||
|
const [stderr, closed] = await Promise.all([text(child.stderr), once(child, "close")]);
|
||||||
|
const exitCode: unknown = closed[0];
|
||||||
|
return { success: exitCode === 0, stderr };
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
import { expect, test } from "vite-plus/test";
|
import { expect, test } from "vite-plus/test";
|
||||||
|
|
||||||
import { output } from "#testing";
|
import { runCommand } from "#testing";
|
||||||
|
|
||||||
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
|
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
|
||||||
// whole import graph, stripping types, before the phase reads its first
|
// whole import graph, stripping types, before the phase reads its first
|
||||||
@@ -16,12 +16,7 @@ const PHASES: [phase: string, firstVariable: string][] = [
|
|||||||
|
|
||||||
test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => {
|
test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => {
|
||||||
const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url));
|
const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url));
|
||||||
const result = await output(process.execPath, {
|
const result = await runCommand(process.execPath, [path], { PATH: process.env.PATH ?? "" });
|
||||||
args: [path],
|
|
||||||
env: { PATH: process.env.PATH ?? "" },
|
|
||||||
clearEnv: true,
|
|
||||||
stdin: "null",
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(new TextDecoder().decode(result.stderr)).toContain(`${firstVariable} is not set`);
|
expect(result.stderr).toContain(`${firstVariable} is not set`);
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-8
@@ -4,8 +4,12 @@ import process from "node:process";
|
|||||||
import { assert, expect, test } from "vite-plus/test";
|
import { assert, expect, test } from "vite-plus/test";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
|
|
||||||
import { isMapping, isNotFound, makeTempDir, output } from "#testing";
|
import { makeTempDir, runCommand } from "#testing";
|
||||||
import type { JsonObject } from "#testing";
|
|
||||||
|
type JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
const isMapping = (value: unknown): value is JsonObject =>
|
||||||
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
|
||||||
const mapping = (value: unknown, at: string): JsonObject => {
|
const mapping = (value: unknown, at: string): JsonObject => {
|
||||||
assert(isMapping(value), at);
|
assert(isMapping(value), at);
|
||||||
@@ -73,12 +77,13 @@ test("the source protection script stages immutable action files", async () => {
|
|||||||
const temp = await makeTempDir();
|
const temp = await makeTempDir();
|
||||||
const outputFile = `${temp}/output`;
|
const outputFile = `${temp}/output`;
|
||||||
try {
|
try {
|
||||||
const result = await output("/bin/bash", {
|
const result = await runCommand("/bin/bash", ["scripts/protect-source.sh"], {
|
||||||
args: ["scripts/protect-source.sh"],
|
...process.env,
|
||||||
env: { ACTION_PATH: process.cwd(), GITHUB_OUTPUT: outputFile, RUNNER_TEMP: temp },
|
ACTION_PATH: process.cwd(),
|
||||||
stderr: "piped",
|
GITHUB_OUTPUT: outputFile,
|
||||||
|
RUNNER_TEMP: temp,
|
||||||
});
|
});
|
||||||
assert(result.success, new TextDecoder().decode(result.stderr));
|
assert(result.success, result.stderr);
|
||||||
|
|
||||||
const path = (await readFile(outputFile, "utf8")).trim().slice(5);
|
const path = (await readFile(outputFile, "utf8")).trim().slice(5);
|
||||||
const files = [
|
const files = [
|
||||||
@@ -147,7 +152,7 @@ test("uses the standard Skills installer without custom discovery", async () =>
|
|||||||
assert(runner.includes("skills add obra/superpowers"));
|
assert(runner.includes("skills add obra/superpowers"));
|
||||||
const removed = await stat("superpowers.ts").then(
|
const removed = await stat("superpowers.ts").then(
|
||||||
() => false,
|
() => false,
|
||||||
(error: unknown) => isNotFound(error),
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "ENOENT",
|
||||||
);
|
);
|
||||||
assert(removed, "superpowers.ts should be removed");
|
assert(removed, "superpowers.ts should be removed");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user