Replace NodeJS dict aliases with maps and named field reads #24

Merged
temeddix merged 20 commits from honest-dict-types into main 2026-10-20 05:01:51 +00:00
4 changed files with 97 additions and 12 deletions
Showing only changes of commit 8b9aedf911 - Show all commits
@@ -21,13 +21,11 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16):
- **Published lint plugins** (2026-10-18, after they reached npm):
`eslint-plugin-import-structure` (`modules` config) and
`eslint-plugin-explicit-types` (`concrete` and `absence` configs) are
devDependencies, enabled in `vite.config.ts`. explicit-types' `naming`
config is left off: its `upper-snake-string-unions` and
`no-upper-snake-object-keys` rules fire on wire tokens this repo does not
name (profile types such as `"claude"`, failure reasons posted to Gitea,
environment variable names), which cannot change without changing
behavior. Open question for the user. `eslint-plugin-lit-robust` does not apply
(no Lit).
devDependencies, enabled in `vite.config.ts` through their `all` configs.
`upper-snake-string-unions` was off while the user decided; since
2026-10-19 it is on and profile types, failure reasons, and anchor sides
are `UPPER_SNAKE` ([[enum-values-upper-snake]]). `eslint-plugin-lit-robust`
does not apply (no Lit).
- **Stricter input checks were accepted** in the migration: Gitea `/user` and
comment responses, the runtime manifest, and the outcome's profile type are
validated; some type errors became `TypeError` with the same messages.
+48
View File
@@ -0,0 +1,48 @@
---
description: Enum-like string values are UPPER_SNAKE everywhere this repo owns the format, JSON included; external standards are converted at the boundary with explicit switch functions
---
Decided 2026-10-19 (all repos). An enum-like string value, a member of a
string-literal union, is `UPPER_SNAKE` wherever this repo owns the format:
TypeScript unions, `profiles.json`, the runtime outcome file, and the
`anchors.json` the agent writes per `prompt.md`. explicit-types
`upper-snake-string-unions` enforces it and stays on.
A value an outside party defines (a CLI flag, an environment variable name,
an HTTP API field, a human-readable message) is converted at the boundary by
an explicit `switch` function, never by renaming our own values to match it:
```ts
export interface ProfileIdentity {
type: "CLAUDE" | "CODEX" | "OPENCODE";
model: string;
}
// The agent name the skills installer knows each profile type by.
function skillsAgent(type: RuntimeProfile["type"]): string {
switch (type) {
case "CLAUDE":
return "claude-code";
case "CODEX":
return "codex";
case "OPENCODE":
return "opencode";
}
}
```
In this repo:
- Profile `type`: `CLAUDE`, `CODEX`, `OPENCODE` (also in `profiles.json`).
`skillsAgent()` (run phase) and `tokenVariable()` (profiles) map them to the
installer's agent name and the credential environment variable;
`defaultModel()` (credentials) picks a model.
- `FailureReason`: `SIGN_IN_FAILED`, `USAGE_EXHAUSTED`, `FAILED`; `toMessage()`
gives the words that logs and posted comments show.
- Anchor `side`: `NEW` or `OLD` (`prompt.md` tells the agent so); the Gitea
review call turns it into `new_position` / `old_position`.
A lookup object keyed by enum values would need `UPPER_SNAKE` keys, which
explicit-types `no-upper-snake-object-keys` bans, so use a `switch`. TypeScript
checks such a switch for exhaustiveness through the declared return type; no
`default:` branch ([[lint-config]]).
+32
View File
@@ -47,5 +47,37 @@ rules: {
overrides: projectOverrides,
```
## Rule decisions (2026-10-19, all repos)
- `typescript/promise-function-async` is on with `require-await` and
`typescript/return-await`. A function that returns a promise is `async`
and awaits it: `async f() { const x = await g(); return x; }`. Test
doubles do the same (`const resolved = await Promise.resolve(v); return
resolved;`), and `expect(promise).rejects` takes the promise itself.
`src/processes/child.ts` is exempt in `projectOverrides`: `exitCode` must
return the very promise it marks handled, or a failed spawn would surface
as an unhandled rejection before callers await it.
- `init-declarations` is on: an uninitialized `let` becomes
`let x: T | null = null;` (`unknown` already includes `null`); a `try` that
assigns and a `catch` that exits still narrow it.
- `no-use-before-define` is on with only `{ functions: false }`.
- `no-await-in-loop` is off (sequential awaits are intentional) and
`unicorn/no-array-reduce` is on: sequential steps are plain `for` loops,
never `reduce` chains or recursion.
- `consistent-return`, `typescript/switch-exhaustiveness-check`, and
`default-case` are off: TypeScript's checks cover them, and no `default:`
branch is added to an exhaustive switch.
- `unknown` is allowed; `any` is banned and the `typescript/no-unsafe-*`
rules are on. Untrusted data is `unknown` plus a type guard.
- `unicorn/no-array-for-each` (use `for...of`),
`unicorn/prefer-structured-clone`, and `typescript/prefer-nullish-coalescing`
(defaults) stay on.
- explicit-types `upper-snake-string-unions` is on
([[enum-values-upper-snake]]); `unicorn/custom-error-definition` is on, so a
custom error has a constructor that sets `this.name`.
- `src/post/` and `src/profiles/` (code and tests) are exempt from
`project/prefer-ascii` in `projectOverrides`: posted Gitea text shows emoji
verdict marks and a middle dot.
A new category hit is fixed in the code; an off or exception is added only
for a real collision or churn, with its reason. Rules: [[project-lint-plugin]].
+12 -5
View File
@@ -11,12 +11,19 @@ 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/<rule>`. Every repo has
`no-disable-comments` and `no-asset-imports`; bot-agents has no others.
`no-disable-comments` and `no-asset-imports`. Since 2026-10-19 every repo
also has `prefer-ascii`, `no-nan` (a "no number" value is `null`, never
`NaN`), and the hugoalh `recommended` ports from
eslint-plugin-import-structure's own lint: `no-duplicate-typeofs`,
`no-empty-yield`, `no-unsafe-number`, `no-useless-continue`,
`no-duplicate-interfaces`, `no-duplicate-types`, `no-useless-type`, and
`no-modifier-private`. `no-duplicate-awaits` and `no-duplicate-voids` are
covered by `unicorn/no-unnecessary-await` and
`typescript/no-meaningless-void-operator`, and `no-enum` by explicit-types.
`scripts/` stays flat; don't add `scripts/rules/`. If the plugin ever needs
more files, they sit flat beside it and share the prefix
(`project-lint-modules.ts`, ...); `scripts/project-lint.ts` keeps the plugin
object and the rule list.
`scripts/` stays flat; don't add `scripts/rules/`. All rules live in the one
`scripts/project-lint.ts` (no file splitting), next to the plugin object and
the rule list.
```ts
import type { DummyRuleMap } from "vite-plus/lint";