Merge remote-tracking branch 'origin/master' into codex/experimental-plugin-group-note

This commit is contained in:
Tianyi Cui
2026-07-30 01:38:34 +08:00
99 changed files with 1436 additions and 558 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-06-11-quality-gates.md
2026-06-11-quality-gates.md: e1af110387936d644208dc1829fde4a4fdf8a3f9 2026-06-11-quality-gates.md: 60db7ba5cfa8184c0fcce764aa027f32a9b721ab
2026-06-11-quality-gates.zh.md: a4e57b7a08ecf20babb33b55d8c94414df1b10b1 2026-06-11-quality-gates.zh.md: a5ac7cd831255d479f7a1d75586785e546877fba

View File

@@ -15,11 +15,11 @@ This codebase is developed primarily by coding agents. Agents follow enforced ga
Every mechanically checkable AGENTS.md promise gets a command that exits non-zero. CI invokes the exhaustive set, while Git hooks reserve their latency budget for cheap local defects: Every mechanically checkable AGENTS.md promise gets a command that exits non-zero. CI invokes the exhaustive set, while Git hooks reserve their latency budget for cheap local defects:
- Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); examples, tests, and scripts typecheck in CI via the root no-emit `tsconfig.json` while package/vendor code stays behind its own project-reference boundary. - Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); examples, tests, and scripts typecheck in CI via the root no-emit `tsconfig.json` while package/vendor code stays behind its own project-reference boundary.
- ESLint strict-type-checked + @stylistic (the house style, enforced), including file-local duplicated logic checks; vendored code excluded. - [Oxlint](2026-07-29-oxlint-linter.md) with type-aware TypeScript rules plus the @stylistic and SonarJS compatibility plugins, enforcing the house style and file-local duplicated-logic checks; vendored code excluded.
- jscpd detects cross-file clones in package production TypeScript and repository scripts; narrow source-range exceptions document deliberately parallel implementations. - jscpd detects cross-file clones in package production TypeScript and repository scripts; narrow source-range exceptions document deliberately parallel implementations.
- Per-file 100% coverage on `packages/*/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion. - Per-file 100% coverage on `packages/*/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations. - knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations.
- lefthook pre-commit fixes staged lint, rejects staged whitespace, and checks the vendor manifest; pre-push runs incremental typecheck. CI runs the full matrix on node 22.19/24/26 plus built application smokes for the Headless, TUI, ACP, JSON-RPC, workflow, and code-runtime entry paths. - lefthook pre-commit applies formatting-only ESLint fixes before Oxlint validation and native fixes, rejects staged whitespace, and checks the vendor manifest; pre-push runs incremental typecheck. CI runs the full matrix on node 22.19/24/26 plus built application smokes for the Headless, TUI, ACP, JSON-RPC, workflow, and code-runtime entry paths.
## Consequences ## Consequences

View File

@@ -15,11 +15,11 @@ Status: implemented
每条可机械检查的 AGENTS.md 承诺都有一个以非零状态退出的命令。CI 执行完整集合,而 Git 钩子将延迟预算留给可低成本发现的本地缺陷: 每条可机械检查的 AGENTS.md 承诺都有一个以非零状态退出的命令。CI 执行完整集合,而 Git 钩子将延迟预算留给可低成本发现的本地缺陷:
- 最严格的 TypeScript 配置(`noUncheckedIndexedAccess``exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录的 no-emit `tsconfig.json` 在 CI 中进行类型检查而包package/vendor 代码保持在各自 project-reference 边界之后。 - 最严格的 TypeScript 配置(`noUncheckedIndexedAccess``exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录的 no-emit `tsconfig.json` 在 CI 中进行类型检查而包package/vendor 代码保持在各自 project-reference 边界之后。
- ESLint strict-type-checked + @stylistic(作为强制执行统一代码风格),包括文件内重复逻辑检查vendor 代码排除在外。 - [Oxlint](2026-07-29-oxlint-linter.md) 配合类型感知的 TypeScript 规则以及 @stylistic 和 SonarJS 兼容插件,强制执行统一代码风格文件内重复逻辑检查vendor 代码排除在外。
- jscpd 检测包的生产 TypeScript 代码与仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。 - jscpd 检测包的生产 TypeScript 代码与仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。
- `packages/*/*/src` 下按文件 100% 覆盖率v8不可达的防御性守卫使用 `/* v8 ignore */ ` 并注明理由,而非删除。 - `packages/*/*/src` 下按文件 100% 覆盖率v8不可达的防御性守卫使用 `/* v8 ignore */ ` 并注明理由,而非删除。
- knip死代码/依赖、publint包的正确性、workspace 约束workspace 规则private、cordis peer+dev、统一版本、ESM以及对构建出的包声明文件进行 NodeNext 消费方类型检查。 - knip死代码/依赖、publint包的正确性、workspace 约束workspace 规则private、cordis peer+dev、统一版本、ESM以及对构建出的包声明文件进行 NodeNext 消费方类型检查。
- lefthook pre-commit 修复已暂存文件的 lint 问题、拒绝已暂存的空白问题并检查 vendor manifestpre-push 运行增量类型检查。CI 在 Node 22.19/24/26 上运行完整矩阵,并对 Headless、TUI、ACPAgent Client Protocol、JSON-RPC、工作流和代码运行时入口路径执行已构建应用的冒烟测试。 - lefthook pre-commit 先应用仅用于格式化的 ESLint 修复,再执行 Oxlint 验证和原生修复,拒绝已暂存的空白问题并检查 vendor manifestpre-push 运行增量类型检查。CI 在 Node 22.19/24/26 上运行完整矩阵,并对 Headless、TUI、ACPAgent Client Protocol、JSON-RPC、工作流和代码运行时入口路径执行已构建应用的冒烟测试。
## 后果 ## 后果

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
2026-07-06-parallel-pre-push-gates.md: d86642b7feb82908ec792db0c6a3da403cfc79fd 2026-07-06-parallel-pre-push-gates.md: 538e52c5318fb6d4eab2e8786513a08c1ff0ec55
2026-07-06-parallel-pre-push-gates.zh.md: 0425cf1a01b56604a07be366dd46d3920c5fb487 2026-07-06-parallel-pre-push-gates.zh.md: e93eec8757c20c8154703d9bdfd2f0c805e6a26c

View File

@@ -14,7 +14,7 @@ Aggregate jobs such as documentation synchronization hide long sequential chains
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) owns the bounded scheduler used by CI, `doc-sync`, and the opt-in `check:all` command. It expands named modes into leaf gates, rejects empty or ambiguous dependency graphs before starting a child, respects artifact dependencies, buffers attributable output, reports exit and signal outcomes independently, and accepts `DSH_GATE_CONCURRENCY` when a caller needs a different worker bound. [scripts/run-gates.ts](../../../../scripts/run-gates.ts) owns the bounded scheduler used by CI, `doc-sync`, and the opt-in `check:all` command. It expands named modes into leaf gates, rejects empty or ambiguous dependency graphs before starting a child, respects artifact dependencies, buffers attributable output, reports exit and signal outcomes independently, and accepts `DSH_GATE_CONCURRENCY` when a caller needs a different worker bound.
The Node 24 consumer job is one seven-gate mode rather than a shell-owned process pool. Its default worker count equals its gate count while dependencies control readiness: `publint` precedes built-package invariant validation, and snapshot replay, NodeNext type checks, built-bin smokes, and lint wait for that validation. Lint waits because the invariant verifier temporarily stages package views that ESLint must not traverse; source compatibility checks can overlap the validation chain. The Node 24 consumer job is one seven-gate mode rather than a shell-owned process pool. Its default worker count equals its gate count while dependencies control readiness: `publint` precedes built-package invariant validation, and snapshot replay, NodeNext type checks, built-bin smokes, and lint wait for that validation. Lint waits because the invariant verifier temporarily stages package views that the linter must not traverse; source compatibility checks can overlap the validation chain.
[scripts/publint-all.ts](../../../../scripts/publint-all.ts) discovers packages from `packages/<group>/<pkg>` and runs `publint` with a worker pool sized from `availableParallelism()`. `DSH_PUBLINT_CONCURRENCY` can cap or raise the worker count for local machines and CI runners with different resource profiles. Results are buffered per package and printed in deterministic package order, so parallel execution does not scramble each package's log block. [scripts/publint-all.ts](../../../../scripts/publint-all.ts) discovers packages from `packages/<group>/<pkg>` and runs `publint` with a worker pool sized from `availableParallelism()`. `DSH_PUBLINT_CONCURRENCY` can cap or raise the worker count for local machines and CI runners with different resource profiles. Results are buffered per package and printed in deterministic package order, so parallel execution does not scramble each package's log block.

View File

@@ -14,7 +14,7 @@ Status: implemented
[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 拥有 CI、`doc-sync` 和选择启用的 `check:all` 命令所使用的有界调度器。它将具名模式展开为叶子门禁,在启动子进程前拒绝空的或有歧义的依赖图,遵守产物依赖,缓冲可归因的输出,分别报告退出结果与信号结果,并在调用方需要不同 worker 上限时接受 `DSH_GATE_CONCURRENCY` [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 拥有 CI、`doc-sync` 和选择启用的 `check:all` 命令所使用的有界调度器。它将具名模式展开为叶子门禁,在启动子进程前拒绝空的或有歧义的依赖图,遵守产物依赖,缓冲可归因的输出,分别报告退出结果与信号结果,并在调用方需要不同 worker 上限时接受 `DSH_GATE_CONCURRENCY`
Node 24 消费方 job 采用单个包含七道门禁的模式,而非由 shell 管理的进程池。其默认 worker 数等于门禁数,但门禁是否就绪由依赖关系控制:`publint` 先于已构建包不变式验证运行快照回放、NodeNext 类型检查、built-bin 冒烟测试和 lint 则等待该验证完成。lint 之所以等待,是因为不变式验证器会临时暂存包视图,而 ESLint 不得遍历这些视图;源码兼容性检查可以与这条验证链重叠运行。 Node 24 消费方 job 采用单个包含七道门禁的模式,而非由 shell 管理的进程池。其默认 worker 数等于门禁数,但门禁是否就绪由依赖关系控制:`publint` 先于已构建包不变式验证运行快照回放、NodeNext 类型检查、built-bin 冒烟测试和 lint 则等待该验证完成。lint 之所以等待,是因为不变式验证器会临时暂存包视图,而 linter 不得遍历这些视图;源码兼容性检查可以与这条验证链重叠运行。
[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包,并以根据 `availableParallelism()` 确定大小的 worker 池运行 `publint``DSH_PUBLINT_CONCURRENCY` 可以针对资源配置不同的本地机器和 CI runner 限制或提高 worker 数量。结果按包缓冲,并按确定性的包顺序打印,因此并行执行不会打乱各包的日志块。 [scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包,并以根据 `availableParallelism()` 确定大小的 worker 池运行 `publint``DSH_PUBLINT_CONCURRENCY` 可以针对资源配置不同的本地机器和 CI runner 限制或提高 worker 数量。结果按包缓冲,并按确定性的包顺序打印,因此并行执行不会打乱各包的日志块。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
2026-07-22-evidence-based-larger-hosted-runners.md: 67fc7ded5cffc6a219665f135a4c9e1cc4752691 2026-07-22-evidence-based-larger-hosted-runners.md: 983d5520bd73fc3cf82c37bf0d4a9ff1c6e6f51c
2026-07-22-evidence-based-larger-hosted-runners.zh.md: 71c5c067361b57fab5aae9e9ffa3850a30609db3 2026-07-22-evidence-based-larger-hosted-runners.zh.md: a86dcf2c60d7b950e7557e84ef6993e712a2ce09

View File

@@ -18,7 +18,7 @@ The required primary path depends on those enterprise pools. Standard GitHub-hos
The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture. The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture.
Linux primary work uses three independent 32-core jobs. Coverage runs alone with its own worker bound, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from ESLint discovery because the artifact check removes them while these processes overlap. The pnpm store and ESLint cache are restored without putting cache uploads on the pull-request critical path. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time. Linux primary work uses three independent 32-core jobs. Coverage runs alone with its own worker bound, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from Oxlint discovery because the artifact check removes them while these processes overlap. The pnpm store is restored without putting cache uploads on the pull-request critical path; Oxlint has no repository-managed result cache. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time.
The gate dependencies remain explicit. Coverage consumes source and does not wait for build. Documentation typechecking builds its complete project-reference graph once. Snapshot replay and publication consumers wait for emitted output, while Node-version compatibility jobs exercise runtime-sensitive source loading without repeating the primary source-graph typecheck. PTY and subprocess suites keep their bounded inner concurrency rather than inheriting the runner's core count. The gate dependencies remain explicit. Coverage consumes source and does not wait for build. Documentation typechecking builds its complete project-reference graph once. Snapshot replay and publication consumers wait for emitted output, while Node-version compatibility jobs exercise runtime-sensitive source loading without repeating the primary source-graph typecheck. PTY and subprocess suites keep their bounded inner concurrency rather than inheriting the runner's core count.

View File

@@ -18,7 +18,7 @@ Status: implemented
原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除因此未使用的诊断路径无法继续维系第二套 CI 架构。 原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除因此未使用的诊断路径无法继续维系第二套 CI 架构。
Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib``packages/*/*/lib``vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 ESLint 的文件发现范围因为这些进程重叠执行时产物检查会删除这些目录。pnpm store 和 ESLint 缓存会得到恢复,但缓存上传不会进入拉取请求关键路径。性能报告采用每个作业从 `startedAt``completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。 Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib``packages/*/*/lib``vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 Oxlint 的文件发现范围因为这些进程重叠执行时产物检查会删除这些目录。pnpm store 会得到恢复,但缓存上传不会进入拉取请求关键路径Oxlint 没有由仓库管理的结果缓存。性能报告采用每个作业从 `startedAt``completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查只构建一次完整的 project-reference 图。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。 门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查只构建一次完整的 project-reference 图。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-fast-local-git-hooks.md
2026-07-22-fast-local-git-hooks.md: a07af1cd424c86f7fa80ea946cd5012362cc66eb 2026-07-22-fast-local-git-hooks.md: 838024c4293372b1430d357774feb06cd9742b9b
2026-07-22-fast-local-git-hooks.zh.md: 78d4ea8980476609a9140737a75152eba123b308 2026-07-22-fast-local-git-hooks.zh.md: 460acf5270c075a808c6a4dc42635a808c7cd192

View File

@@ -12,7 +12,7 @@ Fast hooks still need to reject cheap, high-confidence defects before work leave
## Decision ## Decision
[lefthook.yml](../../../../lefthook.yml) keeps both hooks as bounded local checkpoints. Pre-commit runs sequentially: ESLint fixes and re-stages changed JavaScript and TypeScript, `git diff --cached --check` rejects staged whitespace errors, and the vendor manifest guard checks vendored-source metadata. Pre-push invokes the repository TypeScript binary directly in incremental build mode. [lefthook.yml](../../../../lefthook.yml) keeps both hooks as bounded local checkpoints. Pre-commit runs sequentially: a formatting-only ESLint config fixes and re-stages changed JavaScript and TypeScript, [Oxlint](2026-07-29-oxlint-linter.md) validates those files and applies native safe fixes, `git diff --cached --check` rejects staged whitespace errors, and the vendor manifest guard checks vendored-source metadata. Pre-push invokes the repository TypeScript binary directly in incremental build mode.
Neither hook runs tests, snapshots, documentation checks, builds, hygiene, or the gate scheduler. The opt-in `check:all` package script selects the `check-all` scheduler inventory in [scripts/run-gates.ts](../../../../scripts/run-gates.ts) independently of the hooks; it is a contributor command, not an agent instruction. Neither hook runs tests, snapshots, documentation checks, builds, hygiene, or the gate scheduler. The opt-in `check:all` package script selects the `check-all` scheduler inventory in [scripts/run-gates.ts](../../../../scripts/run-gates.ts) independently of the hooks; it is a contributor command, not an agent instruction.
@@ -27,10 +27,10 @@ This decision supersedes the local-hook portion of [Parallel pre-push gates](202
- **Keep the full pre-push suite and optimize its scheduler** — preserves the earliest exhaustive signal but still repeats agent-selected evidence and CI, while unrelated failures continue blocking publication. - **Keep the full pre-push suite and optimize its scheduler** — preserves the earliest exhaustive signal but still repeats agent-selected evidence and CI, while unrelated failures continue blocking publication.
- **Remove pre-push entirely** — makes pushes cheapest but loses the fast cross-file guarantee that TypeScript provides after several commits. - **Remove pre-push entirely** — makes pushes cheapest but loses the fast cross-file guarantee that TypeScript provides after several commits.
- **Keep typecheck in pre-commit** — catches type errors earlier but charges every intermediate commit instead of one push; staged lint already covers the commit-local syntax and style boundary. - **Keep typecheck in pre-commit** — catches type errors earlier but charges every intermediate commit instead of one push; staged lint already covers the commit-local syntax and style boundary.
- **Make staged lint check-only** — avoids hook-side mutation, but contributors intentionally retain the existing auto-fix workflow; Lefthook's `stage_fixed` owns re-staging so the command does not duplicate `git add`. - **Make staged lint check-only** — avoids hook-side mutation, but contributors intentionally retain the auto-fix workflow; the formatting-only pass and Lefthook's `stage_fixed` preserve it without making ESLint a repository correctness runner or duplicating `git add`.
## Consequences ## Consequences
Normal commits take the staged-file lint critical path, and warm pushes take the incremental typecheck critical path. Contributors retain a one-command opt-in rehearsal without widening the hook critical paths or the agent-required validation set. Hook latency is observed in development and PR evidence rather than enforced by a timing test whose result would depend on host load and cache state. Normal commits take the staged formatter-and-lint critical path, and warm pushes take the incremental typecheck critical path. Contributors retain a one-command opt-in rehearsal without widening the hook critical paths or the agent-required validation set. Hook latency is observed in development and PR evidence rather than enforced by a timing test whose result would depend on host load and cache state.
Local publication no longer proves the exhaustive repository matrix. Agents must select relevant behavioral evidence, reviewers must evaluate whether that selection matches the diff, and CI supplies the comprehensive signal once per pushed revision. Local publication no longer proves the exhaustive repository matrix. Agents must select relevant behavioral evidence, reviewers must evaluate whether that selection matches the diff, and CI supplies the comprehensive signal once per pushed revision.

View File

@@ -12,7 +12,7 @@ agent智能体已经会运行能够覆盖自身改动的测试和检查
## 决策 ## 决策
[lefthook.yml](../../../../lefthook.yml) 将两个钩子都保留为有界的本地检查点。Pre-commit 按顺序运行ESLint 修复改动过的 JavaScript 和 TypeScript 文件并重新暂存,`git diff --cached --check` 拒绝暂存 diff 中的空白错误vendor manifest元数据清单守卫检查 vendor 源码元数据。Pre-push 直接调用仓库内的 TypeScript 二进制,并启用增量构建模式。 [lefthook.yml](../../../../lefthook.yml) 将两个钩子都保留为有界的本地检查点。Pre-commit 按顺序运行:仅用于格式化的 ESLint 配置修复改动过的 JavaScript 和 TypeScript 文件并重新暂存,[Oxlint](2026-07-29-oxlint-linter.md) 验证这些文件并应用原生安全修复,`git diff --cached --check` 拒绝暂存 diff 中的空白错误vendor manifest元数据清单守卫检查 vendor 源码元数据。Pre-push 直接调用仓库内的 TypeScript 二进制,并启用增量构建模式。
两个钩子都不运行测试、快照、文档检查、构建、`hygiene` 或门禁调度器。可选运行的 `check:all` 包脚本独立于这些钩子,从 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中选择 `check-all` 调度器清单;它是贡献者命令,而非对 agent 的指令。 两个钩子都不运行测试、快照、文档检查、构建、`hygiene` 或门禁调度器。可选运行的 `check:all` 包脚本独立于这些钩子,从 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中选择 `check-all` 调度器清单;它是贡献者命令,而非对 agent 的指令。
@@ -27,10 +27,10 @@ agent 检查待推送的 diff并仅运行一次能够覆盖其行为的最小
- **保留全量 pre-push 套件并优化其调度器**——能够最早提供全面信号,但仍会重复 agent 已选取的证据和 CI且无关失败仍会阻塞推送。 - **保留全量 pre-push 套件并优化其调度器**——能够最早提供全面信号,但仍会重复 agent 已选取的证据和 CI且无关失败仍会阻塞推送。
- **完全移除 pre-push**——推送成本最低,但会失去 TypeScript 在多个提交之后提供的快速跨文件保证。 - **完全移除 pre-push**——推送成本最低,但会失去 TypeScript 在多个提交之后提供的快速跨文件保证。
- **在 pre-commit 中保留类型检查**——更早捕获类型错误,但每次中间提交都要承担开销,而不是只在推送时运行一次;暂存文件 lint 已经覆盖提交本身的语法与风格边界。 - **在 pre-commit 中保留类型检查**——更早捕获类型错误,但每次中间提交都要承担开销,而不是只在推送时运行一次;暂存文件 lint 已经覆盖提交本身的语法与风格边界。
- **将暂存文件 lint 设为仅检查模式**——避免钩子修改文件,但贡献者有意保留现有的自动修复工作流Lefthook 的 `stage_fixed` 负责重新暂存,因此命令无需重复执行 `git add` - **将暂存文件 lint 设为仅检查模式**——避免钩子修改文件,但贡献者有意保留自动修复工作流;仅用于格式化的流程和 Lefthook 的 `stage_fixed` 会保留该工作流,而不会让 ESLint 成为仓库正确性检查运行器,也无需重复执行 `git add`
## 结果 ## 结果
普通提交的关键路径是暂存文件 lint缓存已预热时推送的关键路径是增量类型检查。贡献者仍可选择用一条命令完整演练且不会扩展钩子关键路径或 agent 必须运行的验证集合。钩子耗时只作为开发观察数据和 PRPull Request证据记录不设置会受主机负载与缓存状态影响的计时测试。 普通提交的关键路径是暂存文件格式化与 lint缓存已预热时推送的关键路径是增量类型检查。贡献者仍可选择用一条命令完整演练且不会扩展钩子关键路径或 agent 必须运行的验证集合。钩子耗时只作为开发观察数据和 PRPull Request证据记录不设置会受主机负载与缓存状态影响的计时测试。
从本地推送成功不再能证明仓库完整矩阵已通过。agent 必须选择相关的行为证据,评审人必须判断该选择是否与 diff 相符CI 则对每个推送版本提供一次全面信号。 从本地推送成功不再能证明仓库完整矩阵已通过。agent 必须选择相关的行为证据,评审人必须判断该选择是否与 diff 相符CI 则对每个推送版本提供一次全面信号。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-29-oxlint-linter.md
2026-07-29-oxlint-linter.md: 41a50a9d08819809f954aa99007081f270692f38
2026-07-29-oxlint-linter.zh.md: 1ad72a00cb921ed688363583d56634f52b355b4e

View File

@@ -0,0 +1,45 @@
# Agent Note: Oxlint as the repository linter
Status: implemented
English | [中文](2026-07-29-oxlint-linter.zh.md)
## Problem
The repository needs type-aware TypeScript correctness rules, consistent formatting, and file-local duplicate-logic checks across its owned source. ESLint supplied those checks through a JavaScript parser, a project service, and multiple plugins, but a clean lint run spent about one minute on the local migration baseline and required an 8 GiB Node heap, CI result caches, and separately tuned ESLint concurrency.
A faster runner cannot justify losing rules. The migration must preserve the strict type-checked preset, repository overrides, inline suppressions, @stylistic fixes, SonarJS checks, host/client TypeScript separation, and the vendor exclusion.
## Decision
The root [`.oxlintrc.json`](../../../../.oxlintrc.json) is the authoritative repository lint configuration. The `lint` package script, gate scheduler, CI, and lefthook invoke Oxlint through [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) for repository-wide, type-aware, or staged validation. The `lint:fix` script and lefthook first invoke the formatting-only [`eslint.format.config.mjs`](../../../../eslint.format.config.mjs), then run Oxlint. The direct `eslint` and `@typescript-eslint/parser` development dependencies exist only for this parser-without-project formatting pass; their exact versions pin the tested parser/fixer pairing, and that config contains no correctness or type-aware rules.
`options.typeAware` enables `oxlint-tsgolint`. Its backend performs per-file TypeScript-project discovery: package sources use their package projects, host tests/examples/website use `tsconfig.host.json`, and client tests plus `scripts/client-bundle-purity.spec.ts` use `tsconfig.client.json`. The program-less root solution is never flattened. Oxlint's `--tsconfig` override affects import resolution but is ignored by type-aware linting, so this repository does not set it. The configuration explicitly carries the migrated strict-type-checked rules and repository overrides instead of enabling broad Oxlint categories whose contents may change. `typescript/no-unnecessary-condition` remains enabled from Oxlint's nursery set because it was an enforced repository rule before migration.
Oxlint's JavaScript-plugin compatibility layer runs `@stylistic/eslint-plugin` and `eslint-plugin-sonarjs` so the existing formatting and file-local duplicate-logic rules remain enforced. The compatibility layer reports `@stylistic` violations but does not execute their fixers, so the formatting-only ESLint pass owns only the corresponding auto-fixes; an executable parity check keeps those fixable rule definitions aligned while `max-len` remains validation-only. Owned-source suppressions use `oxlint-*` directives and the `typescript/*` namespace, and unused directives remain warnings; vendored sources keep their upstream directives because Oxlint excludes `vendor/**`.
CI does not restore or save a lint-result cache. `DSH_OXLINT_THREADS` makes the shared runner pass the same bound to Oxlint's `--threads` option and the type-aware backend's `GOMAXPROCS` environment variable; ordinary local runs use both defaults. Pre-commit applies the formatting-only ESLint fixes, runs Oxlint validation and native safe fixes, accepts selections containing only ignored files, and re-stages the result through lefthook.
## Verification
The migrated configuration reports the same clean owned-source baseline after resolving two analyzer differences: one redundant test assertion was removed, while one structural cast required by `tsc` carries a narrow Oxlint suppression. A one-time audit against the exact deleted ESLint configuration blob established source 88-to-88, examples 87-to-87, and tests 83-to-83 after the rule-name translations. The committed fingerprint pins those audited Oxlint profiles and the complete override shape; it neither executes the deleted configuration nor propagates later upstream preset changes. Evaluating `typescript-eslint@8.61.0` also confirms that `strictTypeChecked` did not enable `@typescript-eslint/no-empty-function`; the deleted tests-only `off` entry was inert.
Executable contract tests require type-aware diagnostics from the package, host, and client projects; assert the client-only script's project; reject unmatched fallback analysis; and exercise the Stylistic, SonarJS, and nursery compatibility paths. They also pin unused-suppression reporting, ignored-only staged selections, formatter/validator rule parity, and final formatted bytes. Runner tests pin both worker controls, and typecheck confirms that migration-driven source edits preserve the TypeScript programs.
## Alternatives considered
**Run both linters repository-wide.** Every correctness rule is available through Oxlint's native rules, nursery rule, or JavaScript-plugin compatibility layer. A repository-wide ESLint fallback would preserve the slower project-service setup and two correctness configurations without adding a check; the retained ESLint pass is deliberately limited to project-free staged formatting.
**Rely on compatibility-layer fixes.** The layer reports the established `@stylistic` rules but does not apply their fixes under either Oxlint fix mode. Keeping the narrow staged formatter preserves the contributor contract without broadening ESLint back into a repository linter.
**Drop @stylistic or SonarJS rules that are not native.** This would remove dependencies but weaken the mechanical quality contract. The compatibility layer preserves those rules until native replacements can be evaluated as a separate decision.
**Replace @stylistic with Oxfmt during the migration.** A formatter migration would change output beyond the lint-engine boundary and create a repository-wide formatting diff. Keeping the established rules makes this change reviewable and leaves formatter selection independent.
## Consequences
Local migration measurements reduced a clean type-aware lint run from about 61 seconds to about 8 seconds without a result cache. The exact ratio is host-dependent and is not a performance guarantee.
Type-aware diagnostics now come from the TypeScript Go analyzer bundled through `oxlint-tsgolint`, so edge-case inference can differ from typescript-eslint even when `tsc` accepts the same program. Lint and typecheck remain separate required evidence.
The JavaScript-plugin compatibility API and staged formatter are additional boundaries to maintain. Commits pay one project-free ESLint startup before Oxlint, and the root development graph retains ESLint plus the TypeScript parser. Repository-wide validation, type-aware analysis, cache policy, worker control, and inline directives remain Oxlint-owned.

View File

@@ -0,0 +1,45 @@
# Agent Note: 使用 Oxlint 作为仓库 linter
Status: implemented
[English](2026-07-29-oxlint-linter.md) | 中文
## 问题
仓库的自有源码需要类型感知的 TypeScript 正确性规则、一致的格式以及文件内重复逻辑检查。ESLint 通过 JavaScript 解析器、项目服务和多个插件提供这些检查,但在本地迁移基线上,一次无问题的 lint 运行约需 1 分钟,并且需要 8 GiB Node 堆、CI 结果缓存和单独调优的 ESLint 并发度。
不能以提高运行速度为由丢失规则。迁移必须保留严格类型检查预设、仓库覆盖配置、内联抑制指令、@stylistic 修复、SonarJS 检查、host/client TypeScript 隔离和 vendor 排除规则。
## 决策
根目录的 [`.oxlintrc.json`](../../../../.oxlintrc.json) 是仓库 lint 配置的权威来源。`lint`package脚本、门禁调度器、CI 和 lefthook 通过 [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) 调用 Oxlint进行全仓库、类型感知或暂存验证。`lint:fix` 脚本和 lefthook 先调用仅用于格式化的 [`eslint.format.config.mjs`](../../../../eslint.format.config.mjs),再运行 Oxlint。直接的 `eslint``@typescript-eslint/parser` 开发依赖仅用于这次不加载项目的格式化流程;其精确版本锁定经过测试的解析器与修复器配对,该配置不包含正确性规则或类型感知规则。
`options.typeAware` 启用 `oxlint-tsgolint`。其后端按文件发现 TypeScript 项目包源码使用各自的包项目host 测试、示例和网站使用 `tsconfig.host.json`client 测试及 `scripts/client-bundle-purity.spec.ts` 使用 `tsconfig.client.json`。不含程序的根解决方案绝不会被扁平化。Oxlint 的 `--tsconfig` 覆盖项会影响导入解析,但类型感知 lint 会忽略它,因此本仓库不设置该选项。该配置显式载入迁移后的严格类型检查规则和仓库覆盖配置,而不启用内容可能发生变化的 Oxlint 宽泛类别。`typescript/no-unnecessary-condition` 仍从 Oxlint 的 nursery 规则集中启用,因为它在迁移前就是仓库强制执行的规则。
Oxlint 的 JavaScript 插件兼容层运行 `@stylistic/eslint-plugin``eslint-plugin-sonarjs`,从而继续强制执行现有的格式和文件内重复逻辑规则。兼容层会报告 `@stylistic` 违规,但不会执行其修复器,因此仅用于格式化的 ESLint 流程只负责相应的自动修复;一项可执行检查确保这些可修复规则定义保持一致,而 `max-len` 仅用于验证。自有源码中的抑制指令使用 `oxlint-*` 指令和 `typescript/*` 命名空间未使用的指令仍作为警告报告vendor 源码保留其上游指令,因为 Oxlint 会排除 `vendor/**`
CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 使共享运行器将同一上限传给 Oxlint 的 `--threads` 选项和类型感知后端的 `GOMAXPROCS` 环境变量普通本地运行对两者均采用默认值。Pre-commit 应用仅用于格式化的 ESLint 修复,运行 Oxlint 验证和原生安全修复,接受仅含已忽略文件的文件选择,并通过 lefthook 重新暂存结果。
## 验证
解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。以已删除 ESLint 配置的精确 blob 为基准进行的一次性审核在完成规则名映射后确认:源码为 88 项对 88 项,示例为 87 项对 87 项,测试为 83 项对 83 项。已提交的指纹锁定这些经审核的 Oxlint 规则配置及完整的覆盖结构;它既不执行已删除的配置,也不纳入后续的上游预设变更。对 `typescript-eslint@8.61.0` 的评估还确认,`strictTypeChecked` 并未启用 `@typescript-eslint/no-empty-function`;已删除、仅用于测试的 `off` 条目不起作用。
可执行契约测试要求包、host 和 client 项目产生类型感知诊断,断言 client 专用脚本所用的项目,拒绝未匹配的回退分析,并检验 Stylistic、SonarJS 和 nursery 兼容路径。它们还锁定未使用抑制指令的报告行为、仅选择已忽略暂存文件的情况、格式化器与验证器之间的规则一致性,以及最终格式化后的字节。运行器测试锁定两项工作线程控制,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。
## 考虑过的替代方案
**在全仓库范围内同时运行两个 linter。** 所有正确性规则均可通过 Oxlint 原生规则、nursery 规则或 JavaScript 插件兼容层获得。在全仓库范围启用 ESLint 回退会保留较慢的项目服务初始化和两套正确性配置,却不会增加任何检查;保留的 ESLint 流程被刻意限制为不加载项目的暂存文件格式化。
**依赖兼容层修复。** 兼容层会报告既有的 `@stylistic` 规则,但在 Oxlint 的两种修复模式下都不会应用这些规则的修复。保留窄范围的暂存文件格式化器,可以在不将 ESLint 扩张回仓库 linter 的情况下维持贡献者契约。
**移除尚无原生实现的 @stylistic 或 SonarJS 规则。** 这会移除依赖,但也会削弱机械质量契约。兼容层会保留这些规则,直到能够通过单独决策评估原生替代规则。
**迁移期间用 Oxfmt 替换 @stylistic。** 格式化器迁移会产生超出 lint 引擎边界的输出变化,并带来全仓库格式 diff。保留既有规则可使本次变更便于评审并让格式化器选择保持独立。
## 结果
本地迁移测量显示,不使用结果缓存时,一次无问题的类型感知 lint 运行从约 61 秒缩短至约 8 秒。确切比例因主机而异,不构成性能保证。
类型感知诊断现在来自通过 `oxlint-tsgolint` 捆绑的 TypeScript Go 分析器,因此即使 `tsc` 接受同一程序,边界场景下的类型推断也可能与 typescript-eslint 不同。lint 与类型检查仍是两项相互独立的必要证据。
JavaScript 插件兼容 API 和暂存文件格式化器是需要维护的额外边界。每次提交在 Oxlint 之前需要启动一次不加载项目的 ESLint根目录开发依赖图仍保留 ESLint 和 TypeScript 解析器。全仓库验证、类型感知分析、缓存政策、工作线程控制和内联指令仍由 Oxlint 负责。

View File

@@ -180,10 +180,9 @@ jobs:
|| 'dsh-enterprise-ubuntu-latest-32core-test' }} || 'dsh-enterprise-ubuntu-latest-32core-test' }}
name: node 24 / snapshots and artifacts name: node 24 / snapshots and artifacts
env: env:
DSH_ESLINT_CACHE: '1'
DSH_ESLINT_CONCURRENCY: '8'
DSH_GATE_CONCURRENCY: '8' DSH_GATE_CONCURRENCY: '8'
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1' DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
DSH_OXLINT_THREADS: '8'
DSH_PUBLINT_CONCURRENCY: '8' DSH_PUBLINT_CONCURRENCY: '8'
# Failover halves snapshot concurrency for the shared 64-core VM. # Failover halves snapshot concurrency for the shared 64-core VM.
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }} DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
@@ -200,13 +199,6 @@ jobs:
- name: Restore built tree - name: Restore built tree
run: tar -xzf "$RUNNER_TEMP/node-24-built-tree.tar.gz" run: tar -xzf "$RUNNER_TEMP/node-24-built-tree.tar.gz"
- uses: actions/cache/restore@v4
with:
path: .cache/eslint
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
dest: ${{ runner.temp }}/setup-pnpm dest: ${{ runner.temp }}/setup-pnpm
@@ -443,7 +435,7 @@ jobs:
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
echo "path=$store_path" >> "$GITHUB_OUTPUT" echo "path=$store_path" >> "$GITHUB_OUTPUT"
# Master refreshes the caches that pull requests restore without saving. # Master refreshes the pnpm store cache that pull requests restore without saving.
# The store cache stays a hand-rolled actions/cache step rather than # The store cache stays a hand-rolled actions/cache step rather than
# setup-node's `cache: pnpm`: the enterprise pull-request jobs above # setup-node's `cache: pnpm`: the enterprise pull-request jobs above
# restore exactly this key and path, and setup-node's built-in cache # restore exactly this key and path, and setup-node's built-in cache
@@ -456,13 +448,6 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
- uses: actions/cache@v4
with:
path: .cache/eslint
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
- name: Install (immutable) - name: Install (immutable)
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -474,8 +459,8 @@ jobs:
DSH_ARCHIVE_BASE_REF: ${{ github.event.before }} DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
DSH_COVERAGE_MAX_WORKERS: '1' DSH_COVERAGE_MAX_WORKERS: '1'
DSH_E2E_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1'
DSH_ESLINT_CACHE: '1'
DSH_GATE_CONCURRENCY: '1' DSH_GATE_CONCURRENCY: '1'
DSH_OXLINT_THREADS: '1'
DSH_PUBLINT_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1'
DSH_SNAPSHOT_MAX_CONCURRENCY: '1' DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci run: pnpm run check:ci
@@ -528,8 +513,8 @@ jobs:
DSH_ARCHIVE_BASE_REF: ${{ github.event.before }} DSH_ARCHIVE_BASE_REF: ${{ github.event.before }}
DSH_COVERAGE_MAX_WORKERS: '1' DSH_COVERAGE_MAX_WORKERS: '1'
DSH_E2E_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1'
DSH_ESLINT_CACHE: '1'
DSH_GATE_CONCURRENCY: '1' DSH_GATE_CONCURRENCY: '1'
DSH_OXLINT_THREADS: '1'
DSH_PUBLINT_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1'
DSH_SNAPSHOT_MAX_CONCURRENCY: '1' DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci run: pnpm run check:ci
@@ -582,15 +567,6 @@ jobs:
with: with:
node-version: ${{ env.PRIMARY_NODE_VERSION }} node-version: ${{ env.PRIMARY_NODE_VERSION }}
# Master refreshes the small cache that pull requests restore without
# putting package-store extraction back on the Windows critical path.
- uses: actions/cache@v4
with:
path: .cache/eslint
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
- name: Install (immutable) - name: Install (immutable)
shell: pwsh shell: pwsh
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -600,8 +576,8 @@ jobs:
env: env:
DSH_COVERAGE_MAX_WORKERS: '1' DSH_COVERAGE_MAX_WORKERS: '1'
DSH_E2E_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1'
DSH_ESLINT_CACHE: '1'
DSH_GATE_CONCURRENCY: '1' DSH_GATE_CONCURRENCY: '1'
DSH_OXLINT_THREADS: '1'
DSH_PUBLINT_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1'
DSH_SNAPSHOT_MAX_CONCURRENCY: '1' DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci run: pnpm run check:ci
@@ -776,14 +752,6 @@ jobs:
console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length, console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length,
memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))" memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))"
- uses: actions/cache@v4
if: matrix.platform == 'linux'
with:
path: .cache/eslint
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-
- name: Install and prepare Linux - name: Install and prepare Linux
if: matrix.platform == 'linux' if: matrix.platform == 'linux'
run: | run: |
@@ -807,9 +775,8 @@ jobs:
if: matrix.platform == 'linux' if: matrix.platform == 'linux'
env: env:
DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }} DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }}
DSH_ESLINT_CACHE: '1'
DSH_ESLINT_CONCURRENCY: ${{ matrix.workers }}
DSH_GATE_CONCURRENCY: ${{ matrix.workers }} DSH_GATE_CONCURRENCY: ${{ matrix.workers }}
DSH_OXLINT_THREADS: ${{ matrix.workers }}
DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }} DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }}
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }} DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }}
run: pnpm run check:ci run: pnpm run check:ci

3
.gitignore vendored
View File

@@ -13,6 +13,9 @@ examples/*/.sessions/
coverage/ coverage/
.doc-typecheck-*/ .doc-typecheck-*/
.node-next-types-*/ .node-next-types-*/
.oxlint-contract-*/
.oxlintrc.contract-*.json
oxlint-contract-*.ts
.humanize/ .humanize/
tmp/ tmp/
.claude/commands/ .claude/commands/

303
.oxlintrc.json Normal file
View File

@@ -0,0 +1,303 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [],
"categories": {
"correctness": "off"
},
"options": {
"reportUnusedDisableDirectives": "warn",
"typeAware": true
},
"env": {
"builtin": true
},
"ignorePatterns": [
"**/lib/**",
"**/node_modules/**",
"**/.sessions/**",
".claude/**", // Harness-local state belongs to other checkouts, not this checkout's sources.
"**/.doc-typecheck-*/**",
"**/.node-next-types-*/**",
"**/.oxlint-contract-*/**", // Scratch files created by the executable lint-contract tests.
"**/oxlint-contract-*", // Flat probes use real TypeScript project include paths.
"packages/typert/generator/tests/fixtures/type-model/**", // tsgolint rejects this fixture's preserved project shapes before rules run.
"website/.generated/**",
"vendor/**", // Vendored source keeps upstream style and idioms.
"native/**", // The imported landlock-run subtree has its own gates; see native/README.md.
"**/*.js",
"**/*.mjs",
"**/*.config.ts", // Tool and app configs are outside the repository TypeScript programs.
"packages/client/tsdown.client.ts" // Shared client build preset, also outside a TypeScript program.
],
"overrides": [
{
// Shared strict type-aware rules. Source/test differences stay in the short overrides below.
"files": [
"packages/*/*/src/**/*.{ts,tsx}",
"packages/*/*/tests/**/*.{ts,tsx}",
"apps/*/src/**/*.{ts,tsx}",
"apps/*/tests/**/*.{ts,tsx}",
"examples/**/*.{ts,tsx}",
"scripts/**/*.{ts,tsx}",
"website/**/*.{ts,tsx}"
],
"rules": {
"no-var": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-array-constructor": "error",
"no-unused-expressions": "error",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"no-useless-constructor": "error",
"typescript/await-thenable": "error",
"typescript/ban-ts-comment": [
"error",
{
"minimumDescriptionLength": 10
}
],
"typescript/no-array-delete": "error",
"typescript/no-base-to-string": "error",
"typescript/no-confusing-void-expression": "error",
"typescript/no-deprecated": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-dynamic-delete": "error",
"typescript/no-empty-object-type": "off", // Merge-extensible maps intentionally use empty object types.
"typescript/no-explicit-any": "error", // Every intentional any needs a narrow suppression with rationale.
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-extraneous-class": "error",
// Lost promises in the agent loop are the repository's highest-value linted bug class.
"typescript/no-floating-promises": "error",
"typescript/no-for-in-array": "error",
"typescript/no-implied-eval": "error",
"typescript/no-invalid-void-type": "off", // Event signatures intentionally use void in source.
"typescript/no-meaningless-void-operator": "error",
"typescript/no-misused-new": "error",
"typescript/no-misused-promises": "error",
"typescript/no-misused-spread": "error",
"typescript/no-mixed-enums": "error",
"typescript/no-namespace": "off", // Cordis Config namespaces are the repository idiom.
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-redundant-type-constituents": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-boolean-literal-compare": "error",
"typescript/no-unnecessary-template-expression": "error",
"typescript/no-unnecessary-type-arguments": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unnecessary-type-conversion": "error",
"typescript/no-unnecessary-type-parameters": "error",
"typescript/no-unsafe-argument": "error",
"typescript/no-unsafe-assignment": "error",
"typescript/no-unsafe-call": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-enum-comparison": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-unsafe-member-access": "error",
"typescript/no-unsafe-return": "error",
"typescript/no-unsafe-unary-minus": "error",
"typescript/no-useless-default-assignment": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-literal-enum-member": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/prefer-reduce-type-parameter": "error",
"typescript/prefer-return-this-type": "error",
"typescript/related-getter-setter-pairs": "error",
"typescript/restrict-plus-operands": [
"error",
{
"allowAny": false,
"allowBoolean": false,
"allowNullish": false,
"allowNumberAndString": false,
"allowRegExp": false
}
],
"typescript/return-await": [
"error",
"error-handling-correctness-only"
],
"typescript/triple-slash-reference": "error",
"typescript/unbound-method": "error",
"typescript/unified-signatures": "error",
"typescript/use-unknown-in-catch-callback-variable": "error",
"no-void": "off" // void foo() marks deliberate fire-and-forget arrow listeners.
},
"plugins": [
"typescript"
]
},
{
"files": [
"packages/*/*/src/**/*.{ts,tsx}",
"apps/*/src/**/*.{ts,tsx}",
"examples/**/*.{ts,tsx}",
"scripts/**/*.{ts,tsx}",
"website/**/*.{ts,tsx}"
],
"rules": {
"typescript/no-non-null-assertion": "error",
"typescript/no-unnecessary-condition": [
"error",
{
"allowConstantLoopConditions": true
}
],
"typescript/only-throw-error": "error",
"typescript/require-await": "error",
"typescript/restrict-template-expressions": [
"error",
{
"allowNumber": true,
"allowBoolean": true
}
],
"typescript/switch-exhaustiveness-check": [
"error",
{
"considerDefaultExhaustiveForUnions": true
}
]
},
"plugins": [
"typescript"
]
},
{
"files": [
"examples/**/*.ts"
],
"rules": {
"typescript/require-await": "off" // Demo callbacks conform to async interfaces without awaiting.
},
"plugins": [
"typescript"
]
},
{
"files": [
"packages/*/*/tests/**/*.{ts,tsx}",
"apps/*/tests/**/*.{ts,tsx}",
"examples/*/tests/**/*.{ts,tsx}",
"scripts/**/*.spec.{ts,tsx}"
],
"rules": {
"typescript/no-invalid-void-type": "error",
"typescript/no-non-null-assertion": "off", // Assertions commonly follow an expect() that proves presence.
"typescript/no-unnecessary-condition": "off",
"typescript/only-throw-error": "off", // Tests deliberately exercise non-Error throws.
"typescript/require-await": "off", // Mock execute() implementations must retain async signatures.
"typescript/restrict-template-expressions": "off"
},
"plugins": [
"typescript"
]
},
{
"files": [
"packages/**/*.{ts,tsx}",
"apps/**/*.{ts,tsx}",
"examples/**/*.{ts,tsx}",
"scripts/**/*.{ts,tsx}",
"website/**/*.{ts,tsx}"
],
"rules": {
"sonarjs/duplicates-in-character-class": "error",
"sonarjs/no-all-duplicated-branches": "error",
"sonarjs/no-duplicate-in-composite": "error",
"sonarjs/no-duplicate-test-title": "error",
"sonarjs/no-identical-conditions": "error",
"sonarjs/no-identical-expressions": "error",
"sonarjs/no-identical-functions": "error",
"sonarjs/no-duplicated-branches": "error"
},
"jsPlugins": [
"eslint-plugin-sonarjs"
]
},
{
"files": [
"packages/**/*.{ts,tsx}",
"apps/**/*.{ts,tsx}",
"examples/**/*.{ts,tsx}",
"scripts/**/*.{ts,tsx}",
"website/**/*.{ts,tsx}"
],
"rules": {
"@stylistic/indent": [
"error",
2
],
"@stylistic/semi": [
"error",
"never"
],
"@stylistic/quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"@stylistic/comma-dangle": [
"error",
"always-multiline"
],
"@stylistic/eol-last": [
"error",
"always"
],
"@stylistic/no-trailing-spaces": "error",
"@stylistic/object-curly-spacing": [
"error",
"always"
],
"@stylistic/arrow-parens": [
"error",
"as-needed",
{
"requireForBlockBody": true
}
],
"@stylistic/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "none"
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
// Validation-only: line length has no safe formatter fix.
"@stylistic/max-len": [
"error",
{
"code": 140,
"ignoreUrls": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}
]
},
"jsPlugins": [
"@stylistic/eslint-plugin"
]
}
]
}

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write docs/cookbook/adding-a-package.md
adding-a-package.md: 1859310965538b35a353ee05c94b01d1093a3e43 adding-a-package.md: 2dd9165c4b5a7e04ecc7af0507f364fe89b294bb
adding-a-package.zh.md: 22f574a0469609e44f5c55957560ff0f04b9a053 adding-a-package.zh.md: 79f022531de500eed1d53b0915ee933b047121ff

View File

@@ -37,7 +37,7 @@ In-package relative imports use explicit `.ts` specifiers in source (for example
A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract. A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`, `scripts/check-workspace-constraints.ts`. Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `vitest.config.ts`, `.oxlintrc.json`, `scripts/check-workspace-constraints.ts`.
## 3. Decide the package topology ## 3. Decide the package topology

View File

@@ -37,7 +37,7 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset`packages/client/tsdown.client.ts`——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。 `packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset`packages/client/tsdown.client.ts`——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts``tsdown.config.ts``vitest.config.ts``eslint.config.mjs``scripts/check-workspace-constraints.ts` 以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts``tsdown.config.ts``vitest.config.ts``.oxlintrc.json``scripts/check-workspace-constraints.ts`
## 3. 确定包拓扑 ## 3. 确定包拓扑

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write docs/cookbook/adding-a-vendored-package.md
adding-a-vendored-package.md: 71ca9fccc9418348784dbb6668127242e4fb45d2 adding-a-vendored-package.md: a951a96f62d2ea3aa693a24d83bf46a1a12070cd
adding-a-vendored-package.zh.md: c340630aebeda0ec293a835cdfc8d15d71cd7801 adding-a-vendored-package.zh.md: 878adbb203f8c79db0f127cb1ac58cd9e7a09171

View File

@@ -42,7 +42,7 @@ Local relative imports/exports in vendored TypeScript source use explicit `.ts`
| `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications | | `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications |
| `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) | | `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) |
Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`. Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `.oxlintrc.json`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
## 3. Mind the manifest guard ## 3. Mind the manifest guard

View File

@@ -42,7 +42,7 @@ vendored TypeScript 源码中的本地相对导入/导出在复制后使用显
| `vendor/README.md` | 添加一行 manifest 表格行dir、npm name、version、upstream repo、commit SHA并记录所有本地修改 | | `vendor/README.md` | 添加一行 manifest 表格行dir、npm name、version、upstream repo、commit SHA并记录所有本地修改 |
| `scripts/publint-all.ts` | 仅当该 vendored 包本身从此仓库发布时才需要vendored 依赖通常不发布——跳过) | | `scripts/publint-all.ts` | 仅当该 vendored 包本身从此仓库发布时才需要vendored 依赖通常不发布——跳过) |
以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces`vendor/*`)、`tsdown.config.ts``vitest.config.ts``eslint.config.mjs`。只有当构建形态偏离根默认值时(双 ESM/CJS 或多入口——参见 `vendor/schemastery``vendor/logger-console`),才需要单独的 `vendor/<dir>/tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。 以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces`vendor/*`)、`tsdown.config.ts``vitest.config.ts``.oxlintrc.json`。只有当构建形态偏离根默认值时(双 ESM/CJS 或多入口——参见 `vendor/schemastery``vendor/logger-console`),才需要单独的 `vendor/<dir>/tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。
## 3. 注意 manifest 守卫 ## 3. 注意 manifest 守卫

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/development.md # pnpm run verify-translation-pairing --write docs/development.md
development.md: 0a18e29d3da4f694707521e230017e6b22cad740 development.md: 859de959dc5d93c2f0ddbe5c7f700d4bdaf9e09b
development.zh.md: 885b51c701267215cc50d31ecd1694ae2c9af9ca development.zh.md: faa6a07731deed77663727b3ee1f0fe060580c53

View File

@@ -83,7 +83,7 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook is configured in `lefthook.yml` as a fast local checkpoint: lefthook is configured in `lefthook.yml` as a fast local checkpoint:
- `pre-commit` runs staged-file ESLint fixes, checks the staged diff for whitespace errors, and runs the vendor manifest guard. - `pre-commit` applies formatting-only ESLint fixes, validates the staged files with Oxlint and applies its native fixes, checks the staged diff for whitespace errors, and runs the vendor manifest guard.
- `pre-push` runs only the incremental repository typecheck (`tsc -b` over the root solution, covering both the host and client aggregates). - `pre-push` runs only the incremental repository typecheck (`tsc -b` over the root solution, covering both the host and client aggregates).
The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code. The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.
@@ -106,8 +106,8 @@ pnpm run test:coverage # unit tests with per-file coverage gates
pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY
pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks
pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates
pnpm run lint # eslint . pnpm run lint # oxlint .
pnpm run lint:fix # eslint . --fix pnpm run lint:fix # formatting-only ESLint, then oxlint . --fix
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events.md + services.md from source pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events.md + services.md from source
pnpm run verify-cordis-catalog # fail if either cordis catalog is stale pnpm run verify-cordis-catalog # fail if either cordis catalog is stale

View File

@@ -83,7 +83,7 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点: lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:
- `pre-commit` 运行对暂存文件的 ESLint 修复,检查暂存 diff 中的空白错误,并运行 vendor manifest元数据清单守卫 - `pre-commit` 应用仅用于格式化的 ESLint 修复,使用 Oxlint 验证暂存文件并应用其原生修复,然后检查暂存 diff 中的空白错误,并运行 vendor manifest元数据清单守卫
- `pre-push` 只运行仓库增量类型检查(对根 solution 执行 `tsc -b`,覆盖 host 与 client 两个聚合)。 - `pre-push` 只运行仓库增量类型检查(对根 solution 执行 `tsc -b`,覆盖 host 与 client 两个聚合)。
vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md` vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`
@@ -106,8 +106,8 @@ pnpm run test:coverage # unit tests with per-file coverage gates
pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY
pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks
pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates
pnpm run lint # eslint . pnpm run lint # oxlint .
pnpm run lint:fix # eslint . --fix pnpm run lint:fix # formatting-only ESLint, then oxlint . --fix
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events.md + services.md from source pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events.md + services.md from source
pnpm run verify-cordis-catalog # fail if either cordis catalog is stale pnpm run verify-cordis-catalog # fail if either cordis catalog is stale

View File

@@ -1,201 +0,0 @@
import stylistic from '@stylistic/eslint-plugin'
import sonarjs from 'eslint-plugin-sonarjs'
import tseslint from 'typescript-eslint'
// Strict type-aware correctness rules plus repository formatting. Tests/examples relax deliberate
// mock unsafety; vendored sources retain upstream style and receive only selected safety checks.
export default tseslint.config(
{
ignores: [
'**/lib/**',
'**/node_modules/**',
'**/.sessions/**',
'.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources
'**/.doc-typecheck-*/**',
'**/.node-next-types-*/**',
'website/.generated/**',
'vendor/**', // vendored source keeps upstream style and idioms
'native/**', // imported landlock-run subtree: self-contained workspace with its own gates (native/README.md)
'**/*.js',
'**/*.mjs',
'*.config.ts', // root tool configs (vitest, tsdown) — no project service
'apps/*/*.config.ts', // app build configs — outside their project programs
'**/tsdown.config.ts', // package build configs — in no tsconfig program, and TS syntax breaks the parserless fallback
'packages/client/tsdown.client.ts', // shared client build preset, same standing
],
},
// --- our packages: full strictness -------------------------------------
{
files: [
'packages/*/*/src/**/*.{ts,tsx}',
'apps/*/src/**/*.{ts,tsx}',
'examples/**/*.{ts,tsx}',
'scripts/**/*.{ts,tsx}',
'website/**/*.{ts,tsx}',
],
extends: [
...tseslint.configs.strictTypeChecked,
],
languageOptions: {
parserOptions: {
// One project service resolves each file to its owning tsconfig and shares dependency
// graphs. Per-package programs duplicated path-mapped and Cordis closures, reaching ~5 GB.
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// The bug class this repo cares most about: lost promises in the loop.
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/switch-exhaustiveness-check': ['error', {
considerDefaultExhaustiveForUnions: true,
}],
'@typescript-eslint/no-unnecessary-condition': ['error', {
allowConstantLoopConditions: true,
}],
// `any` requires a justification comment — enforced as: no bare casts.
'@typescript-eslint/no-explicit-any': 'error',
// Style points where the codebase intentionally diverges from preset:
'@typescript-eslint/no-namespace': 'off', // Cordis Config-namespace idiom
'@typescript-eslint/no-empty-object-type': 'off', // merge-extensible maps
'@typescript-eslint/no-invalid-void-type': 'off', // event signatures
'@typescript-eslint/restrict-template-expressions': ['error', {
allowNumber: true,
allowBoolean: true,
}],
// `void foo()` in arrow listeners is our idiom for intentional fire-and-forget
'no-void': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
},
// --- examples: demo code conforms to async interfaces without awaiting ---
{
files: ['examples/**/*.ts'],
rules: {
'@typescript-eslint/require-await': 'off',
},
},
// --- tests: same rules, minus the friction that fights test ergonomics --
{
files: [
'packages/*/*/tests/**/*.{ts,tsx}',
'apps/*/tests/**/*.{ts,tsx}',
'examples/*/tests/**/*.{ts,tsx}',
'scripts/**/*.spec.{ts,tsx}',
],
extends: [
...tseslint.configs.strictTypeChecked,
],
languageOptions: {
parserOptions: {
// Same shared project service as the src block: test files resolve
// through the root solution to tsconfig.host.json (its include covers
// every host tests/ tree).
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'off', // assertions follow expect()s
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/require-await': 'off', // mock execute() signatures
'@typescript-eslint/no-empty-function': 'off', // stub agents
'@typescript-eslint/only-throw-error': 'off', // testing non-Error throws
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
},
// --- client tests: the root program excludes packages/client (host/client
// Context merges collide), so the shared project service cannot resolve
// them — parse these through the client aggregate explicitly.
{
files: [
'packages/client/*/tests/**/*.{ts,tsx}',
'scripts/client-bundle-purity.spec.ts',
],
languageOptions: {
parserOptions: {
projectService: false,
project: ['./tsconfig.client.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
// --- file-local duplication (all owned TypeScript) ---------------------
{
files: ['packages/**/*.{ts,tsx}', 'apps/**/*.{ts,tsx}', 'examples/**/*.{ts,tsx}', 'scripts/**/*.{ts,tsx}', 'website/**/*.{ts,tsx}'],
plugins: { sonarjs },
rules: {
// Cross-file clones are covered separately by jscpd.
'sonarjs/duplicates-in-character-class': 'error',
'sonarjs/no-all-duplicated-branches': 'error',
'sonarjs/no-duplicate-in-composite': 'error',
'sonarjs/no-duplicate-test-title': 'error',
'sonarjs/no-identical-conditions': 'error',
'sonarjs/no-identical-expressions': 'error',
'sonarjs/no-identical-functions': 'error',
'sonarjs/no-duplicated-branches': 'error',
},
},
// --- formatting (everything we own) -------------------------------------
{
files: [
'packages/**/*.{ts,tsx}',
'apps/**/*.{ts,tsx}',
'examples/**/*.{ts,tsx}',
'scripts/**/*.{ts,tsx}',
'website/**/*.{ts,tsx}',
'eslint.config.mjs',
],
plugins: { '@stylistic': stylistic },
rules: {
'@stylistic/indent': ['error', 2],
'@stylistic/semi': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'none' },
singleline: { delimiter: 'semi', requireLast: false },
}],
'@stylistic/max-len': ['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }],
},
},
// TypeGraph coverage must retain source-authored syntax that production lint rules forbid.
{
files: ['packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts'],
rules: {
'@stylistic/quotes': 'off',
'@typescript-eslint/no-deprecated': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-mixed-enums': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
},
},
)

59
eslint.format.config.mjs Normal file
View File

@@ -0,0 +1,59 @@
import stylistic from '@stylistic/eslint-plugin'
import parser from '@typescript-eslint/parser'
// Oxlint's JavaScript-plugin compatibility layer reports these rules but does
// not execute their fixers. Keep this config formatting-only: Oxlint remains
// the authoritative repository linter after this pass applies safe fixes.
export default [
{
ignores: [
'**/lib/**',
'**/node_modules/**',
'**/.sessions/**',
'.claude/**',
'**/.doc-typecheck-*/**',
'**/.node-next-types-*/**',
// Do not mirror Oxlint's contract-fixture ignore: those files must reach this formatter.
'website/.generated/**',
'vendor/**',
'native/**',
'**/*.js',
'**/*.mjs',
'**/*.config.ts',
'packages/client/tsdown.client.ts',
],
},
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parser,
parserOptions: {
sourceType: 'module',
},
},
plugins: {
'@stylistic': stylistic,
},
rules: {
'@stylistic/indent': ['error', 2],
'@stylistic/semi': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'none' },
singleline: { delimiter: 'semi', requireLast: false },
}],
},
},
{
// TypeGraph coverage must retain source-authored syntax that the normal quote rule forbids.
files: ['packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts'],
rules: {
'@stylistic/quotes': 'off',
},
},
]

View File

@@ -4,11 +4,18 @@
pre-commit: pre-commit:
jobs: jobs:
- name: format (staged)
glob: '*.{ts,tsx,mts,cts,mjs}'
exclude:
- 'vendor/*/src/**'
run: node_modules/.bin/eslint --config eslint.format.config.mjs --fix --no-warn-ignored {staged_files}
stage_fixed: true
- name: lint (staged) - name: lint (staged)
glob: '*.{ts,tsx,mts,cts,mjs}' glob: '*.{ts,tsx,mts,cts,mjs}'
exclude: exclude:
- 'vendor/*/src/**' - 'vendor/*/src/**'
run: node_modules/.bin/eslint --fix {staged_files} run: node_modules/.bin/tsx scripts/run-oxlint.ts --fix --no-error-on-unmatched-pattern {staged_files}
stage_fixed: true stage_fixed: true
- name: whitespace (staged) - name: whitespace (staged)

View File

@@ -20,8 +20,8 @@
"clean": "tsx scripts/clean.ts", "clean": "tsx scripts/clean.ts",
"change-scope": "tsx scripts/change-scope.ts", "change-scope": "tsx scripts/change-scope.ts",
"typecheck": "tsc -b", "typecheck": "tsc -b",
"lint": "node --max-old-space-size=8192 node_modules/eslint/bin/eslint.js .", "lint": "tsx scripts/run-oxlint.ts .",
"lint:fix": "node --max-old-space-size=8192 node_modules/eslint/bin/eslint.js . --fix", "lint:fix": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix",
"duplication": "jscpd --config .jscpd.json packages scripts", "duplication": "jscpd --config .jscpd.json packages scripts",
"test": "vitest run", "test": "vitest run",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
@@ -117,9 +117,10 @@
"@types/jsdom": "^28.0.3", "@types/jsdom": "^28.0.3",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/node": "^22.20.0", "@types/node": "^22.20.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/coverage-v8": "^4.1.8", "@vitest/coverage-v8": "^4.1.8",
"@yarnpkg/cli-dist": "4.17.1", "@yarnpkg/cli-dist": "4.17.1",
"eslint": "^10.4.1", "eslint": "10.5.0",
"eslint-plugin-sonarjs": "^4.1.0", "eslint-plugin-sonarjs": "^4.1.0",
"execa": "^10.0.0", "execa": "^10.0.0",
"fast-check": "^4.8.0", "fast-check": "^4.8.0",
@@ -133,11 +134,12 @@
"mdast-util-gfm": "^3.1.0", "mdast-util-gfm": "^3.1.0",
"mermaid": "11.16.0", "mermaid": "11.16.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"oxlint": "1.76.0",
"oxlint-tsgolint": "7.0.2001",
"publint": "^0.3.21", "publint": "^0.3.21",
"tsdown": "^0.22.2", "tsdown": "^0.22.2",
"tsx": "^4.22.4", "tsx": "^4.22.4",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.61.0",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.8" "vitest": "^4.1.8"
} }

View File

@@ -601,7 +601,7 @@ function backscanGoal(log: readonly SessionEvent[]): FxGoalProjection | null {
const source = event.data?.source const source = event.data?.source
if (source?.kind !== 'goal' || source.round !== 0) continue if (source?.kind !== 'goal' || source.round !== 0) continue
const change = source.change const change = source.change
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition
if (change === undefined || change.kind !== 'goal/change') continue if (change === undefined || change.kind !== 'goal/change') continue
if (change.operation === 'clear') return null if (change.operation === 'clear') return null
return { goal: change.goal, roundsStarted: change.roundsStarted, createdAt: change.createdAt, updatedAt: change.updatedAt } return { goal: change.goal, roundsStarted: change.roundsStarted, createdAt: change.createdAt, updatedAt: change.updatedAt }

View File

@@ -32,7 +32,7 @@ const install: InvariantInstaller = (ctx, fail) => {
const baselines = new WeakMap<Fiber, number>() const baselines = new WeakMap<Fiber, number>()
// Async listener by design: emitPluginDisposed awaits-and-logs returned // Async listener by design: emitPluginDisposed awaits-and-logs returned
// promises, so a violation surfaces loudly instead of unhandled. // promises, so a violation surfaces loudly instead of unhandled.
// eslint-disable-next-line @typescript-eslint/no-misused-promises // oxlint-disable-next-line typescript/no-misused-promises
ctx.on('internal/plugin', async (fiber) => { ctx.on('internal/plugin', async (fiber) => {
if (fiber.name !== 'client-hmr') return if (fiber.name !== 'client-hmr') return
if (fiber.uid !== null) { if (fiber.uid !== null) {

View File

@@ -9,7 +9,7 @@
* with the last holding entry, session instances cleared (with persisted * with the last holding entry, session instances cleared (with persisted
* state) on scope death. * state) on scope death.
*/ */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- /* oxlint-disable typescript/no-redundant-type-constituents --
* `keyof SlotMap & string` is the declare-merge key pattern: SlotMap only * `keyof SlotMap & string` is the declare-merge key pattern: SlotMap only
* holds this package's 'root' row in this compilation unit, but consumers * holds this package's 'root' row in this compilation unit, but consumers
* merge keys in; the rule fires on the narrow-map view, not on real * merge keys in; the rule fires on the narrow-map view, not on real
@@ -310,6 +310,6 @@ export class SlotsService extends Service {
// The core's overloads proved the shares; the implementation works on // The core's overloads proved the shares; the implementation works on
// the erased view (same pattern as the core's own implementation arm). // the erased view (same pattern as the core's own implementation arm).
const options = rawOptions as ErasedRegisterOptions const options = rawOptions as ErasedRegisterOptions
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return this.ctx.effect(() => this['_register'](options, component), 'slots.register()') return this.ctx.effect(() => this['_register'](options, component), 'slots.register()')
} }

View File

@@ -4,7 +4,7 @@
*/ */
/* jscpd:ignore-start */ /* jscpd:ignore-start */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- /* oxlint-disable typescript/no-redundant-type-constituents --
* `keyof SlotMap & string` is the declare-merge key pattern: SlotMap is empty * `keyof SlotMap & string` is the declare-merge key pattern: SlotMap is empty
* in this compilation unit (intersection reads `never`) but consumers merge * in this compilation unit (intersection reads `never`) but consumers merge
* keys in; the rule fires on the empty-map view, not on real redundancy. */ * keys in; the rule fires on the empty-map view, not on real redundancy. */

View File

@@ -10,7 +10,7 @@
* machinery — everything mounts the production implementations. * machinery — everything mounts the production implementations.
* @module @deepseek-ai/dsh-client-test-runtime * @module @deepseek-ai/dsh-client-test-runtime
*/ */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- /* oxlint-disable typescript/no-redundant-type-constituents --
* `keyof SlotMap & string` is the declare-merge key pattern (see ui-slots): * `keyof SlotMap & string` is the declare-merge key pattern (see ui-slots):
* this compilation unit sees only the runtime's 'root' row, but consumer * this compilation unit sees only the runtime's 'root' row, but consumer
* programs merge their own keys in; the rule fires on the narrow-map view. */ * programs merge their own keys in; the rule fires on the narrow-map view. */

View File

@@ -8,7 +8,7 @@
export async function writeClipboard(text: string): Promise<void> { export async function writeClipboard(text: string): Promise<void> {
// lib.dom types clipboard non-optional, but insecure contexts omit it — // lib.dom types clipboard non-optional, but insecure contexts omit it —
// that runtime gap is exactly what this guard detects. // that runtime gap is exactly what this guard detects.
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */ /* oxlint-disable-next-line typescript/no-unnecessary-condition */
if (navigator.clipboard?.writeText) { if (navigator.clipboard?.writeText) {
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
@@ -19,7 +19,7 @@ export async function writeClipboard(text: string): Promise<void> {
} }
// execCommand('copy') is the only clipboard fallback where the async API // execCommand('copy') is the only clipboard fallback where the async API
// is missing (insecure contexts); deprecated but deliberately retained. // is missing (insecure contexts); deprecated but deliberately retained.
/* eslint-disable @typescript-eslint/no-deprecated */ /* oxlint-disable typescript/no-deprecated */
const exec = typeof document.execCommand === 'function' const exec = typeof document.execCommand === 'function'
? document.execCommand.bind(document) ? document.execCommand.bind(document)
: undefined : undefined
@@ -36,7 +36,7 @@ export async function writeClipboard(text: string): Promise<void> {
} catch { } catch {
// Clipboard unavailable; the button stays idle. // Clipboard unavailable; the button stays idle.
} }
/* eslint-enable @typescript-eslint/no-deprecated */ /* oxlint-enable typescript/no-deprecated */
el.remove() el.remove()
} }

View File

@@ -105,7 +105,7 @@ export function InputBar({
// IME guard so a composition-closing Shift+Enter still breaks the line. // IME guard so a composition-closing Shift+Enter still breaks the line.
if (e.key === 'Enter' && e.shiftKey) return if (e.key === 'Enter' && e.shiftKey) return
// keyCode 229 is the legacy IME-composition signal engines emit without isComposing. // keyCode 229 is the legacy IME-composition signal engines emit without isComposing.
// eslint-disable-next-line @typescript-eslint/no-deprecated // oxlint-disable-next-line typescript/no-deprecated
const composing = composingRef.current || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229 const composing = composingRef.current || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
if (keyboard.arbitrate(e.key === 'ArrowUp' ? 'up' : 'down', composing) === 'consumed') e.preventDefault() if (keyboard.arbitrate(e.key === 'ArrowUp' ? 'up' : 'down', composing) === 'consumed') e.preventDefault()
@@ -165,8 +165,8 @@ export function InputBar({
if (machineBusy) return // submitting is the read-only span; adjudicating holds the pending lock if (machineBusy) return // submitting is the read-only span; adjudicating holds the pending lock
const next = e.target.value const next = e.target.value
keyboard.setDraft(next) keyboard.setDraft(next)
// selectionStart is number|null in lib.dom; the eslint program narrows it. // selectionStart is number|null in lib.dom; the type-aware lint program narrows it.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition
keyboard.track(next, e.target.selectionStart ?? next.length) keyboard.track(next, e.target.selectionStart ?? next.length)
} }
@@ -178,13 +178,13 @@ export function InputBar({
// too (one char = one step). Mouse selection of a chip is handled in the // too (one char = one step). Mouse selection of a chip is handled in the
// backdrop click handler below. Undo/redo must NOT reach the browser: the // backdrop click handler below. Undo/redo must NOT reach the browser: the
// machine owns the transaction log. // machine owns the transaction log.
// selectionStart/End are number|null in lib.dom; the eslint program narrows them. // selectionStart/End are number|null in lib.dom; the type-aware lint program narrows them.
/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* oxlint-disable typescript/no-unnecessary-condition */
const selectionOf = (el: HTMLTextAreaElement) => ({ const selectionOf = (el: HTMLTextAreaElement) => ({
start: el.selectionStart ?? 0, start: el.selectionStart ?? 0,
end: el.selectionEnd ?? el.selectionStart ?? 0, end: el.selectionEnd ?? el.selectionStart ?? 0,
}) })
/* eslint-enable @typescript-eslint/no-unnecessary-condition */ /* oxlint-enable typescript/no-unnecessary-condition */
const onCopyOrCut = (e: React.ClipboardEvent<HTMLTextAreaElement>, cut: boolean): void => { const onCopyOrCut = (e: React.ClipboardEvent<HTMLTextAreaElement>, cut: boolean): void => {
const el = e.currentTarget const el = e.currentTarget

View File

@@ -12,7 +12,7 @@
export async function writeClipboard(text: string): Promise<boolean> { export async function writeClipboard(text: string): Promise<boolean> {
// lib.dom types clipboard non-optional, but insecure contexts omit it — // lib.dom types clipboard non-optional, but insecure contexts omit it —
// that runtime gap is exactly what this guard detects. // that runtime gap is exactly what this guard detects.
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */ /* oxlint-disable-next-line typescript/no-unnecessary-condition */
if (navigator.clipboard?.writeText) { if (navigator.clipboard?.writeText) {
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
@@ -25,7 +25,7 @@ export async function writeClipboard(text: string): Promise<boolean> {
// jsdom and older hosts: best-effort execCommand path when present. // jsdom and older hosts: best-effort execCommand path when present.
// execCommand('copy') is the only clipboard fallback where the async API // execCommand('copy') is the only clipboard fallback where the async API
// is missing; deprecated but deliberately retained. // is missing; deprecated but deliberately retained.
/* eslint-disable @typescript-eslint/no-deprecated */ /* oxlint-disable typescript/no-deprecated */
const exec = typeof document.execCommand === 'function' const exec = typeof document.execCommand === 'function'
? document.execCommand.bind(document) ? document.execCommand.bind(document)
: undefined : undefined
@@ -44,5 +44,5 @@ export async function writeClipboard(text: string): Promise<boolean> {
} finally { } finally {
el.remove() el.remove()
} }
/* eslint-enable @typescript-eslint/no-deprecated */ /* oxlint-enable typescript/no-deprecated */
} }

View File

@@ -16,7 +16,7 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
let s: string let s: string
try { try {
// lib typing hides stringify's undefined arm (undefined/function/symbol payloads). // lib typing hides stringify's undefined arm (undefined/function/symbol payloads).
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition
s = JSON.stringify(payload, null, 2) ?? String(payload) s = JSON.stringify(payload, null, 2) ?? String(payload)
} catch { } catch {
s = String(payload) s = String(payload)

View File

@@ -38,7 +38,7 @@ export function parseQuestionTitle(title: string): string {
/** Return whether a textarea key event belongs to an active IME composition. */ /** Return whether a textarea key event belongs to an active IME composition. */
function isComposing(event: KeyboardEvent<HTMLTextAreaElement>): boolean { function isComposing(event: KeyboardEvent<HTMLTextAreaElement>): boolean {
// keyCode 229 is the legacy IME-composition signal engines emit without isComposing. // keyCode 229 is the legacy IME-composition signal engines emit without isComposing.
// eslint-disable-next-line @typescript-eslint/no-deprecated // oxlint-disable-next-line typescript/no-deprecated
return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229 return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229
} }
@@ -64,9 +64,9 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) {
const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null) const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// index stays in bounds (every setIndex site clamps) and drafts mirrors questions 1:1. // index stays in bounds (every setIndex site clamps) and drafts mirrors questions 1:1.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const question = questions[index]! const question = questions[index]!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const draft = drafts[index]! const draft = drafts[index]!
const hasOptions = (question.options?.length ?? 0) > 0 const hasOptions = (question.options?.length ?? 0) > 0

View File

@@ -8,7 +8,7 @@
* consumer `declare module` augmentation merges with declarations lexically in * consumer `declare module` augmentation merges with declarations lexically in
* the augmented module, not with re-exports. * the augmented module, not with re-exports.
*/ */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- /* oxlint-disable typescript/no-redundant-type-constituents --
* `keyof SlotMap & string` is the declare-merge key pattern: SlotMap is empty * `keyof SlotMap & string` is the declare-merge key pattern: SlotMap is empty
* in THIS compilation unit (so the intersection reads as `never`), but every * in THIS compilation unit (so the intersection reads as `never`), but every
* consumer merges keys in and the intersection is what keeps them string-typed. * consumer merges keys in and the intersection is what keeps them string-typed.
@@ -350,7 +350,7 @@ interface ErasedOptions {
priority?: number | undefined priority?: number | undefined
children?: Record<string, SlotSpec<SlotEntryDef>> | undefined children?: Record<string, SlotSpec<SlotEntryDef>> | undefined
store?: StoreDecl | undefined store?: StoreDecl | undefined
/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- /* oxlint-disable-next-line typescript/no-explicit-any --
* implementation-signature position only (both public overloads type inject * implementation-signature position only (both public overloads type inject
* exactly); `never[]` would fail overload-to-implementation compatibility * exactly); `never[]` would fail overload-to-implementation compatibility
* against the per-declaration InjectParams tuples. */ * against the per-declaration InjectParams tuples. */

View File

@@ -21,7 +21,7 @@ export type MaybeSnapshotSelectorHook<T> =
* declared as the store's complete write set (the audit face — components can * declared as the store's complete write set (the audit face — components can
* only write through these). * only write through these).
*/ */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- /* oxlint-disable-next-line typescript/no-explicit-any --
* any[] (not unknown[]): each action carries its own parameter list, and * any[] (not unknown[]): each action carries its own parameter list, and
* unknown[] would reject every concrete signature under strict parameter * unknown[] would reject every concrete signature under strict parameter
* contravariance. Params are re-inferred per action by BakedActions. */ * contravariance. Params are re-inferred per action by BakedActions. */
@@ -95,14 +95,14 @@ export interface StoreHandle<T, A extends ActionsDecl<T>> {
* Exclusive-store registration form: the registrant passes the factory itself * Exclusive-store registration form: the registrant passes the factory itself
* and the framework calls it per entry x scope (no shared identity exists). * and the framework calls it per entry x scope (no shared identity exists).
*/ */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- /* oxlint-disable-next-line typescript/no-explicit-any --
* erased position accepting every StoreHandle instantiation; T/A are * erased position accepting every StoreHandle instantiation; T/A are
* recovered per use site by conditional inference (HandleOf/BoundActions/ * recovered per use site by conditional inference (HandleOf/BoundActions/
* PropsStore). */ * PropsStore). */
export type StoreFactory = () => StoreHandle<any, any> export type StoreFactory = () => StoreHandle<any, any>
/** The register `store` option position: a shared handle or an exclusive factory. */ /** The register `store` option position: a shared handle or an exclusive factory. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- same erased-constraint position as StoreFactory (see above). // oxlint-disable-next-line typescript/no-explicit-any -- same erased-constraint position as StoreFactory (see above).
export type StoreDecl = StoreHandle<any, any> | StoreFactory export type StoreDecl = StoreHandle<any, any> | StoreFactory
/** Normalize a store declaration to its handle type (factories yield their return). */ /** Normalize a store declaration to its handle type (factories yield their return). */

View File

@@ -41,7 +41,7 @@ describe('tsdown client artifact', () => {
// Same execution form the loader uses (inline script eval, window scope) — // Same execution form the loader uses (inline script eval, window scope) —
// the implied-eval ban targets accidental string execution, not this // the implied-eval ban targets accidental string execution, not this
// deliberate bundle-execution fixture. // deliberate bundle-execution fixture.
// eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call // oxlint-disable-next-line typescript/no-implied-eval, typescript/no-unsafe-call
new Function(code!)() new Function(code!)()
expect(handoff).toBeDefined() expect(handoff).toBeDefined()
const modules = new Map<string, unknown>([ const modules = new Map<string, unknown>([

View File

@@ -134,7 +134,7 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS)
export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream): () => void { export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream): () => void {
// The slot's VALUE is stored for restore and reassigned — never invoked // The slot's VALUE is stored for restore and reassigned — never invoked
// detached, so the unbound-method concern does not apply. // detached, so the unbound-method concern does not apply.
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const original = stream.write const original = stream.write
stream.write = (chunk: unknown, ...rest: unknown[]): boolean => { stream.write = (chunk: unknown, ...rest: unknown[]): boolean => {
logs.push(typeof chunk === 'string' ? chunk : String(chunk)) logs.push(typeof chunk === 'string' ? chunk : String(chunk))

View File

@@ -195,7 +195,7 @@ export class BasicCompactService extends CompactService {
// A model-free prune can land before later summary work fails. That // A model-free prune can land before later summary work fails. That
// durable reduction is sufficient retry proof; do not discard it just // durable reduction is sufficient retry proof; do not discard it just
// because the optional second phase threw. Cancellation still wins. // because the optional second phase threw. Cancellation still wins.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the signal can abort while recovery is awaited. // oxlint-disable-next-line typescript/no-unnecessary-condition -- the signal can abort while recovery is awaited.
if (!signal.aborted && agent.session.surface.replaceGeneration > generation) { if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
ctx.logger.warn( ctx.logger.warn(
`context-overflow compaction failed after durable surface progress: ${message}; ` `context-overflow compaction failed after durable surface progress: ${message}; `
@@ -205,14 +205,14 @@ export class BasicCompactService extends CompactService {
return { kind: 'retry' } return { kind: 'retry' }
} }
ctx.logger.warn( ctx.logger.warn(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the signal can abort while recovery is awaited. // oxlint-disable-next-line typescript/no-unnecessary-condition -- the signal can abort while recovery is awaited.
`context-overflow compaction failed: ${message}; ${signal.aborted `context-overflow compaction failed: ${message}; ${signal.aborted
? 'cancellation prevents retry' ? 'cancellation prevents retry'
: 'preserving the original request error'}`, : 'preserving the original request error'}`,
) )
return next() return next()
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the signal can abort while compaction is awaited. // oxlint-disable-next-line typescript/no-unnecessary-condition -- the signal can abort while compaction is awaited.
if (signal.aborted if (signal.aborted
|| agent.session.surface.replaceGeneration <= generation) return next() || agent.session.surface.replaceGeneration <= generation) return next()
if (result !== null) logResult(result, 'context overflow recovery') if (result !== null) logResult(result, 'context overflow recovery')

View File

@@ -49,7 +49,7 @@ export function selectCompactableRange(
let accumulated = 0 let accumulated = 0
let keepFromIdx = pricedNodes.length let keepFromIdx = pricedNodes.length
for (let index = pricedNodes.length - 1; index >= 0; index -= 1) { for (let index = pricedNodes.length - 1; index >= 0; index -= 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
accumulated += pricedNodes[index]!.tokens accumulated += pricedNodes[index]!.tokens
keepFromIdx = index keepFromIdx = index
if (accumulated >= retainTokens) break if (accumulated >= retainTokens) break
@@ -57,15 +57,15 @@ export function selectCompactableRange(
if (keepFromIdx === 0) return null if (keepFromIdx === 0) return null
while (keepFromIdx > 0) { while (keepFromIdx > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx]!)) break if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx]!)) break
keepFromIdx -= 1 keepFromIdx -= 1
} }
if (keepFromIdx === 0) return null if (keepFromIdx === 0) return null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const first = surfaceNodes[0]! const first = surfaceNodes[0]!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const cutoff = surfaceNodes[keepFromIdx - 1]! const cutoff = surfaceNodes[keepFromIdx - 1]!
return { start: first, end: cutoff } return { start: first, end: cutoff }
} }
@@ -98,11 +98,11 @@ export async function compactSurfaceRegion(
`compactRegion: start seq ${start} (position ${startIdx}) is after end seq ${end} (position ${endIdx}) on the surface`, `compactRegion: start seq ${start} (position ${startIdx}) is after end seq ${end} (position ${endIdx}) on the surface`,
) )
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
if (!toolPairingBalancedBefore(session, nodes[startIdx]!)) { if (!toolPairingBalancedBefore(session, nodes[startIdx]!)) {
throw new Error(`compactRegion: start seq ${start} is not a balanced boundary (would split a step's tool-call/result pair)`) throw new Error(`compactRegion: start seq ${start} is not a balanced boundary (would split a step's tool-call/result pair)`)
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
if (!toolPairingBalancedAfter(session, nodes[endIdx]!)) { if (!toolPairingBalancedAfter(session, nodes[endIdx]!)) {
throw new Error(`compactRegion: end seq ${end} is not a balanced boundary (would split a step, or the step is still open)`) throw new Error(`compactRegion: end seq ${end} is not a balanced boundary (would split a step, or the step is still open)`)
} }
@@ -196,7 +196,7 @@ function buildSummarizationInput(
const events = session.events const events = session.events
const regionMessages = shadowedSeqs const regionMessages = shadowedSeqs
// shadowedSeqs are current surface seqs, so each is a valid log index. // shadowedSeqs are current surface seqs, so each is a valid log index.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
.map(seq => session.deriveEventMessage(events[seq]!)) .map(seq => session.deriveEventMessage(events[seq]!))
.filter((message): message is Message => message !== null) .filter((message): message is Message => message !== null)
return { return {
@@ -213,7 +213,7 @@ function inspectTurnTail(
let compactionInProgress = false let compactionInProgress = false
let compactionStateKnown = false let compactionStateKnown = false
for (let index = events.length - 1; index >= 0; index -= 1) { for (let index = events.length - 1; index >= 0; index -= 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const event = events[index]! const event = events[index]!
if (!compactionStateKnown) { if (!compactionStateKnown) {
if (event.type === 'compact/start') { if (event.type === 'compact/start') {

View File

@@ -221,7 +221,7 @@ export class ReactLoopAgent implements Agent {
if (this.abort !== undefined || !this.queued.some(item => item.wakeup)) return if (this.abort !== undefined || !this.queued.some(item => item.wakeup)) return
// The some() guard above proves the queue is non-empty; the non-null // The some() guard above proves the queue is non-empty; the non-null
// assertion expresses that invariant. // assertion expresses that invariant.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const { message } = this.queued.shift()! const { message } = this.queued.shift()!
const inheritedOutboxLength = this.outbox.length const inheritedOutboxLength = this.outbox.length
@@ -368,7 +368,7 @@ export class ReactLoopAgent implements Agent {
outcome.failure, requestFailureHistory, outcome.retryPolicy, signal, outcome.failure, requestFailureHistory, outcome.retryPolicy, signal,
() => Promise.resolve<RequestErrorAction>(undefined), () => Promise.resolve<RequestErrorAction>(undefined),
) )
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited. // oxlint-disable-next-line typescript/no-unnecessary-condition -- signal can abort while recovery is awaited.
if (action?.kind === 'retry' && !signal.aborted) { if (action?.kind === 'retry' && !signal.aborted) {
retryFailures = Object.freeze([...requestFailureHistory, outcome.failure]) retryFailures = Object.freeze([...requestFailureHistory, outcome.failure])
} }
@@ -584,7 +584,7 @@ export class ReactLoopAgent implements Agent {
const maxTokens = this.options.maxTokens const maxTokens = this.options.maxTokens
const seedConfig = deepFreeze(structuredClone( const seedConfig = deepFreeze(structuredClone(
this.requestHeaderLogged this.requestHeaderLogged
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the header it now folds // oxlint-disable-next-line typescript/no-non-null-assertion -- the instance logged the header it now folds
? persistedConfig! ? persistedConfig!
: { : {
...route, ...route,

View File

@@ -83,7 +83,7 @@ export async function executeToolCalls(
let concluded = false let concluded = false
while (next < planned.length) { while (next < planned.length) {
// Commit before classifying again so registry changes affect unstarted calls. // Commit before classifying again so registry changes affect unstarted calls.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
const first = planned[next]! const first = planned[next]!
const mode = ctx.tools.executionMode(first.exec).kind const mode = ctx.tools.executionMode(first.exec).kind
const group = mode === 'parallel' ? planned.slice(next) : [first] const group = mode === 'parallel' ? planned.slice(next) : [first]
@@ -151,7 +151,7 @@ async function runGroup(
const result = slot.needsPost const result = slot.needsPost
? await ctx.tools[TOOL_REGISTRY_SCHEDULER].finalize(slot.exec, slot.result) ? await ctx.tools[TOOL_REGISTRY_SCHEDULER].finalize(slot.exec, slot.result)
: ctx.tools[TOOL_REGISTRY_SCHEDULER].finish(slot.exec, slot.result) : ctx.tools[TOOL_REGISTRY_SCHEDULER].finish(slot.exec, slot.result)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded index // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded index
appendToolResult(session, turn, step, call!.block, result, callSeqs[committed]!) appendToolResult(session, turn, step, call!.block, result, callSeqs[committed]!)
for (const context of result.additionalContexts ?? []) acceptContext(context) for (const context of result.additionalContexts ?? []) acceptContext(context)
concluded ||= result.concludesTurn === true concluded ||= result.concludesTurn === true
@@ -162,7 +162,7 @@ async function runGroup(
const inFlight = new Map<number, Promise<number>>() const inFlight = new Map<number, Promise<number>>()
const startCall = async (index: number): Promise<void> => { const startCall = async (index: number): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded index // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded index
const call = group[index]! const call = group[index]!
callSeqs[index] = appendToolCall(session, turn, step, call.block) callSeqs[index] = appendToolCall(session, turn, step, call.block)
started++ started++
@@ -198,7 +198,7 @@ async function runGroup(
const fillPool = async (): Promise<void> => { const fillPool = async (): Promise<void> => {
while (!aborted && nextToStart < group.length && inFlight.size < maxParallelToolCalls) { while (!aborted && nextToStart < group.length && inFlight.size < maxParallelToolCalls) {
// Re-read later modes after ordered commits so registry changes can create a barrier. // Re-read later modes after ordered commits so registry changes can create a barrier.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
const nextCall = group[nextToStart]! const nextCall = group[nextToStart]!
if (nextToStart > 0 && mode === 'parallel' if (nextToStart > 0 && mode === 'parallel'
&& ctx.tools.executionMode(nextCall.exec).kind !== 'parallel') break && ctx.tools.executionMode(nextCall.exec).kind !== 'parallel') break

View File

@@ -249,7 +249,7 @@ describe('config-driven session id', () => {
const failures: unknown[] = [] const failures: unknown[] = []
ctx.on('agent-loop/config-start-failed', () => { throw unrenderable }) ctx.on('agent-loop/config-start-failed', () => { throw unrenderable })
// Deliberately violate the normal Error-only rejection rule to exercise the unknown boundary. // Deliberately violate the normal Error-only rejection rule to exercise the unknown boundary.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // oxlint-disable-next-line typescript/prefer-promise-reject-errors
ctx.on('agent-loop/config-start-failed', () => Promise.reject(unrenderable) as never) ctx.on('agent-loop/config-start-failed', () => Promise.reject(unrenderable) as never)
ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) }) ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) })
vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(unrenderable) vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(unrenderable)

View File

@@ -108,12 +108,12 @@ export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
} }
}, },
async serial(name, ...rest) { async serial(name, ...rest) {
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function // oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
const serial = ctx.serial as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => Promise<never> const serial = ctx.serial as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => Promise<never>
return await serial(carrier, name, agent, ...rest) return await serial(carrier, name, agent, ...rest)
}, },
waterfall(name, ...rest) { waterfall(name, ...rest) {
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function // oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
const waterfall = ctx.waterfall as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => never const waterfall = ctx.waterfall as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => never
return waterfall(carrier, name, agent, ...rest) return waterfall(carrier, name, agent, ...rest)
}, },

View File

@@ -328,7 +328,7 @@ export class AgentRegistry extends Service {
// caller's composite effect can yield it for in-order teardown; the // caller's composite effect can yield it for in-order teardown; the
// loop's constructor effect returns it directly, identity-nesting the // loop's constructor effect returns it directly, identity-nesting the
// registration under that effect. // registration under that effect.
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return dispose return dispose
} }
@@ -355,7 +355,7 @@ export class AgentRegistry extends Service {
// capability and need no Cordis tracker magic. // capability and need no Cordis tracker magic.
const { target } = this.requireFactory() const { target } = this.requireFactory()
const receiver = getTraceable(ownerCtx, target) const receiver = getTraceable(ownerCtx, target)
// eslint-disable-next-line @typescript-eslint/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver // oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
return Reflect.apply(target.createAgent, receiver, [ownerCtx, options]) return Reflect.apply(target.createAgent, receiver, [ownerCtx, options])
} }
@@ -370,7 +370,7 @@ export class AgentRegistry extends Service {
const ownerCtx = this.ctx const ownerCtx = this.ctx
const { target } = this.requireFactory() const { target } = this.requireFactory()
const receiver = getTraceable(ownerCtx, target) const receiver = getTraceable(ownerCtx, target)
// eslint-disable-next-line @typescript-eslint/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver // oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
return Reflect.apply(target.resume, receiver, [ownerCtx, options]) return Reflect.apply(target.resume, receiver, [ownerCtx, options])
} }
@@ -397,7 +397,7 @@ export class AgentRegistry extends Service {
yield this.enter(agent, this.ctx.agent) yield this.enter(agent, this.ctx.agent)
this.announce(agent) this.announce(agent)
}.bind(this), 'agents.register()') }.bind(this), 'agents.register()')
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return dispose return dispose
} }

View File

@@ -241,7 +241,7 @@ export class ScopedLayers<L extends ScopeLayer> {
} }
if (notify) this.onChange() if (notify) this.onChange()
}.bind(this), options.label) }.bind(this), options.label)
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- exact synchronous disposer preserves Cordis effect identity // oxlint-disable-next-line typescript/no-misused-promises -- exact synchronous disposer preserves Cordis effect identity
return dispose return dispose
} }
} }

View File

@@ -597,7 +597,7 @@ export class Session {
for (const seq of nodes.slice(this.derivedNodes)) { for (const seq of nodes.slice(this.derivedNodes)) {
// Surface sequences are built from this.log — seq is always a valid // Surface sequences are built from this.log — seq is always a valid
// index by construction. The non-null assertion expresses that invariant. // index by construction. The non-null assertion expresses that invariant.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const msg = this.deriveEventMessage(this.log[seq]!) const msg = this.deriveEventMessage(this.log[seq]!)
// A surface node is one of the five message-producing types, but an // A surface node is one of the five message-producing types, but an
// empty-content assistant/message (a max-tokens step that hosts only // empty-content assistant/message (a max-tokens step that hosts only
@@ -912,7 +912,7 @@ export class SessionStore extends Service {
} catch (error: unknown) { } catch (error: unknown) {
// Preserve the listener's exact rejection value; flush is a caller-owned // Preserve the listener's exact rejection value; flush is a caller-owned
// failure boundary, and Cordis listeners may throw arbitrary values. // failure boundary, and Cordis listeners may throw arbitrary values.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // oxlint-disable-next-line typescript/prefer-promise-reject-errors
return Promise.reject(error) return Promise.reject(error)
} }
})) }))

View File

@@ -340,7 +340,7 @@ export class SurfaceManager implements SessionSurface {
/** Fold events appended since the previous access. */ /** Fold events appended since the previous access. */
private _processDelta(): void { private _processDelta(): void {
for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) { for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
applySurfaceEvent(this._state, this.log[i]!, i, this.log) applySurfaceEvent(this._state, this.log[i]!, i, this.log)
this._lastProcessedSeq = i this._lastProcessedSeq = i
} }

View File

@@ -86,7 +86,7 @@ describe('packChunkRuns', () => {
['a block-index switch', [...deltaRun('text-delta', 2), ...deltaRun('text-delta', 1, 2, 7)]], ['a block-index switch', [...deltaRun('text-delta', 2), ...deltaRun('text-delta', 1, 2, 7)]],
['a step switch', deltaRun('text-delta', 3).map((e, k) => k === 2 ? chunkEvent(e.seq, e.time, (e.data as { chunk: StreamChunk }).chunk, 1, 2) : e)], ['a step switch', deltaRun('text-delta', 3).map((e, k) => k === 2 ? chunkEvent(e.seq, e.time, (e.data as { chunk: StreamChunk }).chunk, 1, 2) : e)],
])('breaks a run on %s (both halves too short to pack)', (_label, events) => { ])('breaks a run on %s (both halves too short to pack)', (_label, events) => {
expect(packChunkRuns(events as SessionEvent[])).toStrictEqual(events) expect(packChunkRuns(events)).toStrictEqual(events)
}) })
it('breaks a tool-call run on call-id or name change', () => { it('breaks a tool-call run on call-id or name change', () => {

View File

@@ -546,19 +546,19 @@ export function defineTool<const S extends ParameterSchemaSpec, const O extends
options: DefineToolOptions<S, O>, options: DefineToolOptions<S, O>,
): ToolDefinition { ): ToolDefinition {
// Object-literal methods do not use `this`; retaining references is safe. // Object-literal methods do not use `this`; retaining references is safe.
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userExecute = options.execute const userExecute = options.execute
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userFinalizeContent = options.finalizeContent const userFinalizeContent = options.finalizeContent
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userRender = options.output.render const userRender = options.output.render
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userPresentationMeta = options.output.presentationMeta const userPresentationMeta = options.output.presentationMeta
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userPresentCall = options.presentCall const userPresentCall = options.presentCall
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userPresentResult = options.presentResult const userPresentResult = options.presentResult
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const userIsConcurrencySafe = options.isConcurrencySafe const userIsConcurrencySafe = options.isConcurrencySafe
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) { if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`) throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`)

View File

@@ -27,7 +27,7 @@ export type ContentToolFixtureOptions<S extends ParameterSchemaSpec> = Omit<
export function defineContentToolFixture<const S extends ParameterSchemaSpec>( export function defineContentToolFixture<const S extends ParameterSchemaSpec>(
options: ContentToolFixtureOptions<S>, options: ContentToolFixtureOptions<S>,
): ToolDefinition { ): ToolDefinition {
// eslint-disable-next-line @typescript-eslint/unbound-method // oxlint-disable-next-line typescript/unbound-method
const execute = options.execute const execute = options.execute
return defineTool({ return defineTool({
...options, ...options,

View File

@@ -148,7 +148,7 @@ export function parseCliArgs(args: readonly string[]): CliCommand {
throw new CliArgumentError(`expected exactly one positional task or -p, received ${parsed.positionals.length} positional(s)`) throw new CliArgumentError(`expected exactly one positional task or -p, received ${parsed.positionals.length} positional(s)`)
} }
// Cardinality was checked above, so the fallback index zero exists. // Cardinality was checked above, so the fallback index zero exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const task = prompt ?? parsed.positionals[0]! const task = prompt ?? parsed.positionals[0]!
if (task.trim().length === 0) throw new CliArgumentError('task must not be blank') if (task.trim().length === 0) throw new CliArgumentError('task must not be blank')
@@ -301,7 +301,7 @@ export async function runOneShot(ctx: Context, options: OneShotOptions): Promise
try { try {
/* v8 ignore next -- skips send only when cancellation wins the listener-registration race above */ /* v8 ignore next -- skips send only when cancellation wins the listener-registration race above */
if (!firstTurnEnded) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition if (!firstTurnEnded) { // oxlint-disable-line typescript/no-unnecessary-condition
agent.followup(createUserMessage({ content: [{ type: 'text', text: options.task }], source: { kind: 'user' } })) agent.followup(createUserMessage({ content: [{ type: 'text', text: options.task }], source: { kind: 'user' } }))
} }
await turnEnded await turnEnded
@@ -361,7 +361,7 @@ async function bootInterruptibly(
return await Promise.race([booting, interruptedBoot]) return await Promise.race([booting, interruptedBoot])
} catch (error: unknown) { } catch (error: unknown) {
// The awaited race permits the signal to change after the preflight check. // The awaited race permits the signal to change after the preflight check.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition
if (signal.aborted) { if (signal.aborted) {
void booting.then( void booting.then(
async (lateContext) => { async (lateContext) => {

View File

@@ -32,6 +32,9 @@ class ObservedStateGate {
* the write/edit prior-observation policy. * the write/edit prior-observation policy.
*/ */
private owner(actor: object | undefined): object | undefined { private owner(actor: object | undefined): object | undefined {
// tsgolint treats object as assignable to weak FsPolicyExec, while tsc still requires the structural cast for property access.
// See the analyzer-divergence consequence in .agents/notes/implemented/process/2026-07-29-oxlint-linter.md.
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- The analyzers disagree on this weak type.
return (actor as FsPolicyExec | undefined)?.agent?.session return (actor as FsPolicyExec | undefined)?.agent?.session
} }

View File

@@ -103,7 +103,7 @@ export function applyGoalProjection(state: GoalProjection | null, event: Session
// Session-log data is a durable boundary: the static type promises the kind, // Session-log data is a durable boundary: the static type promises the kind,
// but a foreign or corrupted change record must degrade to same-reference, // but a foreign or corrupted change record must degrade to same-reference,
// never feed the zod parse in the registry drive. // never feed the zod parse in the registry drive.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- durable-boundary guard // oxlint-disable-next-line typescript/no-unnecessary-condition -- durable-boundary guard
if (change === undefined || change.kind !== 'goal/change') return state if (change === undefined || change.kind !== 'goal/change') return state
if (change.operation === 'clear') return null if (change.operation === 'clear') return null
return { return {

View File

@@ -23,7 +23,7 @@ export class GoalError extends HarnessError {
* @param code - stable machine-routable classification. * @param code - stable machine-routable classification.
*/ */
// Keep the constructor to narrow HarnessError's string code at this boundary. // Keep the constructor to narrow HarnessError's string code at this boundary.
// eslint-disable-next-line @typescript-eslint/no-useless-constructor -- type-only narrowing // oxlint-disable-next-line typescript/no-useless-constructor -- type-only narrowing
constructor(message: string, code: GoalErrorCode) { constructor(message: string, code: GoalErrorCode) {
super(message, code) super(message, code)
} }

View File

@@ -125,7 +125,7 @@ function fullResponse(narrow: RpcResponse<unknown>): Response {
*/ */
// K appears once in the signature but ties the UNARY_ROUTES[K] row lookup to its own // K appears once in the signature but ties the UNARY_ROUTES[K] row lookup to its own
// schema/invoke pairing; a union parameter degrades the row to an uninvokable intersection. // schema/invoke pairing; a union parameter degrades the row to an uninvokable intersection.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters // oxlint-disable-next-line typescript/no-unnecessary-type-parameters
async function handleUnary<K extends keyof RpcMethodMap>( async function handleUnary<K extends keyof RpcMethodMap>(
api: ApiProxy, method: K, message: ClientRequest, signal: AbortSignal, api: ApiProxy, method: K, message: ClientRequest, signal: AbortSignal,
): Promise<Response> { ): Promise<Response> {

View File

@@ -78,14 +78,14 @@ export function boundedInsert(window: ListingCandidate[], candidate: ListingCand
// oversized level costs O(1) per candidate past the head instead of a // oversized level costs O(1) per candidate past the head instead of a
// window scan (100k children against a 1,001 window must not approach // window scan (100k children against a 1,001 window must not approach
// 10^8 comparisons). // 10^8 comparisons).
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a full window (length === keep >= 1) has a tail // oxlint-disable-next-line typescript/no-non-null-assertion -- a full window (length === keep >= 1) has a tail
if (window.length === keep && candidate.name.localeCompare(window[window.length - 1]!.name) >= 0) return true if (window.length === keep && candidate.name.localeCompare(window[window.length - 1]!.name) >= 0) return true
// Binary insertion keeps a retained candidate at O(log keep) comparisons. // Binary insertion keeps a retained candidate at O(log keep) comparisons.
let lo = 0 let lo = 0
let hi = window.length let hi = window.length
while (lo < hi) { while (lo < hi) {
const mid = (lo + hi) >>> 1 const mid = (lo + hi) >>> 1
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition // oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
if (candidate.name.localeCompare(window[mid]!.name) < 0) hi = mid if (candidate.name.localeCompare(window[mid]!.name) < 0) hi = mid
else lo = mid + 1 else lo = mid + 1
} }

View File

@@ -24,7 +24,7 @@ export function providerForClosedStep(
if (stepEndIndex < 0) return undefined if (stepEndIndex < 0) return undefined
for (let index = stepEndIndex; index >= 0; index -= 1) { for (let index = stepEndIndex; index >= 0; index -= 1) {
// The loop bounds prove this indexed read exists. // The loop bounds prove this indexed read exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const event = events[index]! const event = events[index]!
if (event.type === 'request/header') return event.data.header.config.provider if (event.type === 'request/header') return event.data.header.config.provider
} }

View File

@@ -531,7 +531,7 @@ export class LlmService extends Service {
yield value yield value
} }
} finally { } finally {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the iteration catch sets its latch before entering finally. // oxlint-disable-next-line typescript/no-unnecessary-condition -- the iteration catch sets its latch before entering finally.
if (!completed && !iterationFailed) { if (!completed && !iterationFailed) {
const close = iterator.return?.bind(iterator) const close = iterator.return?.bind(iterator)
if (close) await close() if (close) await close()

View File

@@ -72,10 +72,10 @@ describe('BlockAssembler', () => {
it('mustGet throws when an index is missing from the partials map (invariant violation)', () => { it('mustGet throws when an index is missing from the partials map (invariant violation)', () => {
const assembler = new BlockAssembler() const assembler = new BlockAssembler()
// Force the invariant violation: manually corrupt the data structures. // Force the invariant violation: manually corrupt the data structures.
/* eslint-disable */ /* oxlint-disable */
const hack = assembler as any const hack = assembler as any
hack.order.push(99) hack.order.push(99)
/* eslint-enable */ /* oxlint-enable */
expect(() => assembler.blocks()).toThrow('BlockAssembler invariant violated') expect(() => assembler.blocks()).toThrow('BlockAssembler invariant violated')
}) })

View File

@@ -756,7 +756,7 @@ describe('LlmService', () => {
return { return {
[Symbol.asyncIterator](): AsyncIterator<StreamChunk> { [Symbol.asyncIterator](): AsyncIterator<StreamChunk> {
// Third-party adapters can reject with arbitrary values. // Third-party adapters can reject with arbitrary values.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // oxlint-disable-next-line typescript/prefer-promise-reject-errors
return { next: () => Promise.reject('plain provider failure') } return { next: () => Promise.reject('plain provider failure') }
}, },
} }

View File

@@ -171,7 +171,7 @@ export class TokenMeterService extends Service {
} }
while (state.consumedEvents < session.events.length) { while (state.consumedEvents < session.events.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- contiguous session seqs index the durable log // oxlint-disable-next-line typescript/no-non-null-assertion -- contiguous session seqs index the durable log
const event = session.events[state.consumedEvents]! const event = session.events[state.consumedEvents]!
this._foldEvent(session, state, event) this._foldEvent(session, state, event)
state.consumedEvents += 1 state.consumedEvents += 1
@@ -226,7 +226,7 @@ export class TokenMeterService extends Service {
} }
// assistant/message is surface-mandatory at every append/seed boundary. // assistant/message is surface-mandatory at every append/seed boundary.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const eventTokens = surface!.tokens const eventTokens = surface!.tokens
if (event.data.usage !== undefined && nextHeader !== undefined) { if (event.data.usage !== undefined && nextHeader !== undefined) {
const providerAssistantTokens = this._estimateProviderAssistant( const providerAssistantTokens = this._estimateProviderAssistant(
@@ -334,7 +334,7 @@ export class TokenMeterService extends Service {
// Session construction validates contiguous seqs, and the explicit // Session construction validates contiguous seqs, and the explicit
// earlier-than-assistant check above therefore guarantees existence. // earlier-than-assistant check above therefore guarantees existence.
const source = session.events[seq] const source = session.events[seq]
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const sourceEvent = source! const sourceEvent = source!
if (sourceEvent.type !== 'assistant/chunk') { if (sourceEvent.type !== 'assistant/chunk') {
throw new Error(`token meter: assistant/message at seq ${event.seq} source seq ${seq} is not assistant/chunk`) throw new Error(`token meter: assistant/message at seq ${event.seq} source seq ${seq} is not assistant/chunk`)

View File

@@ -39,7 +39,7 @@ export class SessionQueryError extends HarnessError {
declare readonly code: SessionQueryErrorCode declare readonly code: SessionQueryErrorCode
// The base stores the value; this signature narrows its open string code. // The base stores the value; this signature narrows its open string code.
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // oxlint-disable-next-line typescript/no-useless-constructor
constructor(message: string, code: SessionQueryErrorCode, options?: ErrorOptions) { constructor(message: string, code: SessionQueryErrorCode, options?: ErrorOptions) {
super(message, code, options) super(message, code, options)
} }

View File

@@ -91,7 +91,7 @@ export function traceEvent(
} }
// The target check above proves the parallel record exists at this index. // The target check above proves the parallel record exists at this index.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const targetRecord = analysis.records[seq]! const targetRecord = analysis.records[seq]!
const replacedBy = analysis.replacedBy.get(seq) const replacedBy = analysis.replacedBy.get(seq)
return { return {
@@ -225,7 +225,7 @@ function buildDescendants(
const stack = [{ sessionId, descendants }] const stack = [{ sessionId, descendants }]
while (stack.length > 0) { while (stack.length > 0) {
// The length guard proves a frame exists. // The length guard proves a frame exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const frame = stack.pop()! const frame = stack.pop()!
const nodes: SessionLineageNode[] = [] const nodes: SessionLineageNode[] = []
for (const child of childrenByParent.get(frame.sessionId) ?? []) { for (const child of childrenByParent.get(frame.sessionId) ?? []) {
@@ -235,7 +235,7 @@ function buildDescendants(
} }
for (let index = nodes.length - 1; index >= 0; index -= 1) { for (let index = nodes.length - 1; index >= 0; index -= 1) {
// The loop bounds prove this indexed node exists. // The loop bounds prove this indexed node exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const node = nodes[index]! const node = nodes[index]!
stack.push({ sessionId: node.session.header.id, descendants: node.descendants }) stack.push({ sessionId: node.session.header.id, descendants: node.descendants })
} }

View File

@@ -134,7 +134,7 @@ function expectCode(code: SessionQueryErrorCode): Error {
function rejectUnknown<T>(reason: unknown): Promise<T> { function rejectUnknown<T>(reason: unknown): Promise<T> {
return new Promise<T>((_resolve, reject) => { return new Promise<T>((_resolve, reject) => {
// Exercise containment for an implementation that violates the Error rejection convention. // Exercise containment for an implementation that violates the Error rejection convention.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // oxlint-disable-next-line typescript/prefer-promise-reject-errors
reject(reason) reject(reason)
}) })
} }

View File

@@ -1444,7 +1444,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
}, },
])('fails generic when inspecting $name is unsafe', async ({ secrets, diagnostic, failure }) => { ])('fails generic when inspecting $name is unsafe', async ({ secrets, diagnostic, failure }) => {
const mounted = await mount() const mounted = await mount()
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- hostile unknown rejection is the scenario // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- hostile unknown rejection is the scenario
FakeQuery.sessionSearch = () => Promise.reject(failure()) FakeQuery.sessionSearch = () => Promise.reject(failure())
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined) const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)

View File

@@ -36,7 +36,7 @@ class CooperativeAdapter extends LlmAdapter {
if (signal === undefined) throw new Error('expected title request signal') if (signal === undefined) throw new Error('expected title request signal')
await new Promise<never>((_resolve, reject) => { await new Promise<never>((_resolve, reject) => {
const rejectAbort = (): void => { const rejectAbort = (): void => {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- exercise exact AbortSignal.reason propagation // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise exact AbortSignal.reason propagation
reject(signal.reason) reject(signal.reason)
} }
if (signal.aborted) { if (signal.aborted) {

View File

@@ -381,7 +381,7 @@ class SkillWatchManager {
const current = await resolveRootWatchMode(state.root.path) const current = await resolveRootWatchMode(state.root.path)
// A child unlink can publish an empty catalog before root unlinkDir arrives. // A child unlink can publish an empty catalog before root unlinkDir arrives.
// Discovery therefore revalidates the retained handle independently. // Discovery therefore revalidates the retained handle independently.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- watcher callbacks can mark unhealthy while the probe awaits // oxlint-disable-next-line typescript/no-unnecessary-condition -- watcher callbacks can mark unhealthy while the probe awaits
if (!state.unhealthy && sameWatchMode(watcher.mode, current)) return if (!state.unhealthy && sameWatchMode(watcher.mode, current)) return
} }
await this.replaceWatcher(state) await this.replaceWatcher(state)
@@ -398,7 +398,7 @@ class SkillWatchManager {
/* v8 ignore next -- The loop returns no handle only when teardown wins between awaited probes. */ /* v8 ignore next -- The loop returns no handle only when teardown wins between awaited probes. */
if (watcher === undefined) return if (watcher === undefined) return
/* v8 ignore start -- Post-open teardown is timing-dependent; the disposal race has an explicit integration test. */ /* v8 ignore start -- Post-open teardown is timing-dependent; the disposal race has an explicit integration test. */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- teardown can race awaited watcher startup // oxlint-disable-next-line typescript/no-unnecessary-condition -- teardown can race awaited watcher startup
if (this.closing || state.owners.size === 0) { if (this.closing || state.owners.size === 0) {
await this.closeWatcher(watcher) await this.closeWatcher(watcher)
return return
@@ -407,7 +407,7 @@ class SkillWatchManager {
state.watcher = watcher state.watcher = watcher
state.unhealthy = false state.unhealthy = false
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- teardown can race awaited watcher startup // oxlint-disable-next-line typescript/no-unnecessary-condition -- teardown can race awaited watcher startup
if (!this.closing) { if (!this.closing) {
state.unhealthy = true state.unhealthy = true
this.ctx.logger.warn(`skill-local: failed to watch ${state.root.path}: ${errorMessage(error)}`) this.ctx.logger.warn(`skill-local: failed to watch ${state.root.path}: ${errorMessage(error)}`)

View File

@@ -267,7 +267,7 @@ export class SkillService extends Service {
invalidateCache() invalidateCache()
} }
}, 'skills.registerProvider()') }, 'skills.registerProvider()')
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; preserve exact disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; preserve exact disposer identity
return dispose return dispose
} catch (error) { } catch (error) {
lifecycle.abort(error) lifecycle.abort(error)
@@ -307,7 +307,7 @@ export class SkillService extends Service {
invalidateCache() invalidateCache()
} }
}, 'skills.register()') }, 'skills.register()')
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return dispose return dispose
} }

View File

@@ -761,7 +761,7 @@ describe('SkillService registry', () => {
const warnings: string[] = [] const warnings: string[] = []
ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn
const disposeThrowing = ctx.on('skills/change', () => { throw new Error('observer threw') }) const disposeThrowing = ctx.on('skills/change', () => { throw new Error('observer threw') })
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- deliberate rejection proves notification containment // oxlint-disable-next-line typescript/no-misused-promises -- deliberate rejection proves notification containment
const disposeRejecting = ctx.on('skills/change', () => Promise.reject(new Error('observer rejected'))) const disposeRejecting = ctx.on('skills/change', () => Promise.reject(new Error('observer rejected')))
let observed = 0 let observed = 0
const disposeObserver = ctx.on('skills/change', () => { observed += 1 }) const disposeObserver = ctx.on('skills/change', () => { observed += 1 })
@@ -907,7 +907,7 @@ describe('SkillService registry', () => {
name: 'hostile-failure', name: 'hostile-failure',
list() { list() {
// Deliberately violate the provider contract to prove containment is total. // Deliberately violate the provider contract to prove containment is total.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // oxlint-disable-next-line typescript/prefer-promise-reject-errors
return Promise.reject(hostileFailure) return Promise.reject(hostileFailure)
}, },
async get() { async get() {

View File

@@ -268,7 +268,7 @@ function catalogHistory(agent: Agent): { visibleDigest?: string; published: bool
let published = false let published = false
for (let index = events.length - 1; index >= 0; index -= 1) { for (let index = events.length - 1; index >= 0; index -= 1) {
// The loop bounds prove the read-only event view contains this index. // The loop bounds prove the read-only event view contains this index.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line typescript/no-non-null-assertion
const event = events[index]! const event = events[index]!
if (event.type !== 'user/message' if (event.type !== 'user/message'
|| event.data.source.kind !== 'plugin' || event.data.source.kind !== 'plugin'

View File

@@ -56,7 +56,7 @@ class JsonKvUnit implements KvUnit {
private readonly onClose: () => void, private readonly onClose: () => void,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/require-await -- async keeps the closed guard a rejection, not a synchronous throw // oxlint-disable-next-line typescript/require-await -- async keeps the closed guard a rejection, not a synchronous throw
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> { async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
this.assertOpen() this.assertOpen()
const tables: Record<string, Record<string, unknown>> = {} const tables: Record<string, Record<string, unknown>> = {}

View File

@@ -145,7 +145,7 @@ export async function startInProcessRun(
// Close the narrow handoff race before installing the live-run listener. // Close the narrow handoff race before installing the live-run listener.
// Static analysis does not model the abort that may land between the // Static analysis does not model the abort that may land between the
// factory's listener detachment and this continuation. // factory's listener detachment and this continuation.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // oxlint-disable-next-line typescript/no-unnecessary-condition
if (request.signal.aborted) { if (request.signal.aborted) {
flags.cancelled = true flags.cancelled = true
await handle.dispose() await handle.dispose()

View File

@@ -194,7 +194,7 @@ export class SubagentService extends Service {
*/ */
registerProvider(provider: SubagentProvider): () => void { registerProvider(provider: SubagentProvider): () => void {
const name = provider.name const name = provider.name
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return this.ctx.effect(function* (this: SubagentService) { return this.ctx.effect(function* (this: SubagentService) {
if (this.providers.has(name)) { if (this.providers.has(name)) {
throw new SubagentError(`a subagent provider named "${name}" is already registered`, 'DUPLICATE_PROVIDER') throw new SubagentError(`a subagent provider named "${name}" is already registered`, 'DUPLICATE_PROVIDER')

View File

@@ -226,7 +226,7 @@ describe('SubagentService', () => {
const heard: string[] = [] const heard: string[] = []
ctx.on('subagent/provider-removed', () => { throw new Error('sync boom') }) ctx.on('subagent/provider-removed', () => { throw new Error('sync boom') })
// Runtime listeners may return thenables even though the declaration's observable result is void. // Runtime listeners may return thenables even though the declaration's observable result is void.
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- exercises rejected-listener containment // oxlint-disable-next-line typescript/no-misused-promises -- exercises rejected-listener containment
ctx.on('subagent/provider-removed', async () => { throw new Error('async boom') }) ctx.on('subagent/provider-removed', async () => { throw new Error('async boom') })
ctx.on('subagent/provider-removed', () => { throw { toString: () => { throw new Error('coercion') } } }) ctx.on('subagent/provider-removed', () => { throw { toString: () => { throw new Error('coercion') } } })
ctx.on('subagent/provider-removed', name => void heard.push(name)) ctx.on('subagent/provider-removed', name => void heard.push(name))

View File

@@ -192,7 +192,7 @@ export class InvariantService extends Service {
} }
// Cordis attaches setup thenability and async teardown to this callable; // Cordis attaches setup thenability and async teardown to this callable;
// the service seam intentionally exposes only the conventional disposer. // the service seam intentionally exposes only the conventional disposer.
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- the extra runtime shape stays private. // oxlint-disable-next-line typescript/no-misused-promises -- the extra runtime shape stays private.
return registration return registration
} }
} }

View File

@@ -90,7 +90,7 @@ export class TypertRegistry extends Service {
for (const record of schemaRecords) schemas.delete(record.key) for (const record of schemaRecords) schemas.delete(record.key)
} }
}, 'typert.register()') }, 'typert.register()')
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; preserve Cordis disposer identity // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; preserve Cordis disposer identity
return dispose return dispose
} }

View File

@@ -136,7 +136,7 @@ describe('CommandService', () => {
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
ctx.on('commands/change', () => { throw new Error('observer threw') }) ctx.on('commands/change', () => { throw new Error('observer threw') })
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- exercises rejected-listener containment // oxlint-disable-next-line typescript/no-misused-promises -- exercises rejected-listener containment
ctx.on('commands/change', () => Promise.reject(new Error('observer rejected'))) ctx.on('commands/change', () => Promise.reject(new Error('observer rejected')))
const afterFailures = vi.fn() const afterFailures = vi.fn()
ctx.on('commands/change', afterFailures) ctx.on('commands/change', afterFailures)
@@ -229,7 +229,7 @@ describe('CommandService', () => {
ctx.commands.register({ ctx.commands.register({
name: 'reject-value', name: 'reject-value',
description: 'Reject a non-Error value', description: 'Reject a non-Error value',
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- exercise untyped plugin normalization // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise untyped plugin normalization
handler: () => Promise.reject('not an Error'), handler: () => Promise.reject('not an Error'),
}) })
await expect(ctx.commands.execute(agent, '/reject-value', new AbortController().signal)) await expect(ctx.commands.execute(agent, '/reject-value', new AbortController().signal))
@@ -239,7 +239,7 @@ describe('CommandService', () => {
ctx.commands.register({ ctx.commands.register({
name: 'reject-hostile', name: 'reject-hostile',
description: 'Reject an unrenderable value', description: 'Reject an unrenderable value',
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- exercise hostile plugin normalization // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise hostile plugin normalization
handler: () => Promise.reject(hostile), handler: () => Promise.reject(hostile),
}) })
await expect(ctx.commands.execute(agent, '/reject-hostile', new AbortController().signal)) await expect(ctx.commands.execute(agent, '/reject-hostile', new AbortController().signal))

View File

@@ -366,7 +366,7 @@ export function createTuiChat(
// the controller needs `appendNotice`/`overlayManager`, defined after that // the controller needs `appendNotice`/`overlayManager`, defined after that
// closure. Declare here, assign once after those exist, and defer the first // closure. Declare here, assign once after those exist, and defer the first
// `updatePromptValues()` call until after the assignment so no read precedes it. // `updatePromptValues()` call until after the assignment so no read precedes it.
// eslint-disable-next-line prefer-const -- single assignment is a forward-reference, not a const. // oxlint-disable-next-line prefer-const -- single assignment is a forward-reference, not a const.
let modelController!: ModelController let modelController!: ModelController
const now = (): number => runtime.now?.() ?? Date.now() const now = (): number => runtime.now?.() ?? Date.now()
const agentStatus = (): AgentStatus => agent.status const agentStatus = (): AgentStatus => agent.status

View File

@@ -548,7 +548,7 @@ describe('dsh-workflow-workerthread', () => {
// The rejection VALUE's own coercion throws: a warn built with bare // The rejection VALUE's own coercion throws: a warn built with bare
// String(error) would itself throw, skipping the ChildDisposed ack // String(error) would itself throw, skipping the ChildDisposed ack
// and wedging the script's finally until the grace/terminate path. // and wedging the script's finally until the grace/terminate path.
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the non-Error rejection IS the scenario under test // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error rejection IS the scenario under test
dispose: () => Promise.reject({ toString: () => { throw new Error('coercion trap') } }), dispose: () => Promise.reject({ toString: () => { throw new Error('coercion trap') } }),
}), }),
} }

View File

@@ -75,7 +75,7 @@ describe('dsh-workflow (interface)', () => {
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => ctx.logger) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => ctx.logger)
const seen: string[] = [] const seen: string[] = []
// Runtime listeners may return thenables even though the declaration's observable result is void. // Runtime listeners may return thenables even though the declaration's observable result is void.
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- exercises rejected-listener containment // oxlint-disable-next-line typescript/no-misused-promises -- exercises rejected-listener containment
ctx.on('workflow/agent-start', async () => { throw new Error('async observer failed') }) ctx.on('workflow/agent-start', async () => { throw new Error('async observer failed') })
ctx.on('workflow/agent-start', (_info, agent) => { seen.push(agent.label) }) ctx.on('workflow/agent-start', (_info, agent) => { seen.push(agent.label) })
const engine = ctx.workflows as StubEngine const engine = ctx.workflows as StubEngine

445
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.20.0 specifier: ^22.20.0
version: 22.20.0 version: 22.20.0
'@typescript-eslint/parser':
specifier: 8.61.0
version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^4.1.8 specifier: ^4.1.8
version: 4.1.8(vitest@4.1.8) version: 4.1.8(vitest@4.1.8)
@@ -45,7 +48,7 @@ importers:
specifier: 4.17.1 specifier: 4.17.1
version: 4.17.1 version: 4.17.1
eslint: eslint:
specifier: ^10.4.1 specifier: 10.5.0
version: 10.5.0(jiti@2.7.0) version: 10.5.0(jiti@2.7.0)
eslint-plugin-sonarjs: eslint-plugin-sonarjs:
specifier: ^4.1.0 specifier: ^4.1.0
@@ -86,6 +89,12 @@ importers:
micromark-extension-gfm: micromark-extension-gfm:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
oxlint:
specifier: 1.76.0
version: 1.76.0(oxlint-tsgolint@7.0.2001)
oxlint-tsgolint:
specifier: 7.0.2001
version: 7.0.2001
publint: publint:
specifier: ^0.3.21 specifier: ^0.3.21
version: 0.3.21 version: 0.3.21
@@ -98,15 +107,12 @@ importers:
typescript: typescript:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
typescript-eslint:
specifier: ^8.61.0
version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) version: 6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
vitest: vitest:
specifier: ^4.1.8 specifier: ^4.1.8
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
apps/cli: apps/cli:
dependencies: dependencies:
@@ -7452,6 +7458,158 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@oxlint-tsgolint/darwin-arm64@7.0.2001':
resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@7.0.2001':
resolution: {integrity: sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@7.0.2001':
resolution: {integrity: sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@7.0.2001':
resolution: {integrity: sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@7.0.2001':
resolution: {integrity: sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@7.0.2001':
resolution: {integrity: sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==}
cpu: [x64]
os: [win32]
'@oxlint/binding-android-arm-eabi@1.76.0':
resolution: {integrity: sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxlint/binding-android-arm64@1.76.0':
resolution: {integrity: sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxlint/binding-darwin-arm64@1.76.0':
resolution: {integrity: sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxlint/binding-darwin-x64@1.76.0':
resolution: {integrity: sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxlint/binding-freebsd-x64@1.76.0':
resolution: {integrity: sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxlint/binding-linux-arm-gnueabihf@1.76.0':
resolution: {integrity: sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm-musleabihf@1.76.0':
resolution: {integrity: sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm64-gnu@1.76.0':
resolution: {integrity: sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.76.0':
resolution: {integrity: sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.76.0':
resolution: {integrity: sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.76.0':
resolution: {integrity: sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.76.0':
resolution: {integrity: sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.76.0':
resolution: {integrity: sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.76.0':
resolution: {integrity: sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.76.0':
resolution: {integrity: sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.76.0':
resolution: {integrity: sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxlint/binding-win32-arm64-msvc@1.76.0':
resolution: {integrity: sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxlint/binding-win32-ia32-msvc@1.76.0':
resolution: {integrity: sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxlint/binding-win32-x64-msvc@1.76.0':
resolution: {integrity: sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -8157,14 +8315,6 @@ packages:
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.61.0':
resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.61.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.61.0': '@typescript-eslint/parser@8.61.0':
resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -8188,30 +8338,26 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.61.0': '@typescript-eslint/tsconfig-utils@8.65.0':
resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.61.0': '@typescript-eslint/types@8.61.0':
resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.65.0':
resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.61.0': '@typescript-eslint/typescript-estree@8.61.0':
resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.61.0':
resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.61.0': '@typescript-eslint/visitor-keys@8.61.0':
resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -9315,10 +9461,6 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
immediate@3.0.6: immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
@@ -10098,6 +10240,23 @@ packages:
oxc-resolver@11.20.0: oxc-resolver@11.20.0:
resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==}
oxlint-tsgolint@7.0.2001:
resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==}
hasBin: true
oxlint@1.76.0:
resolution: {integrity: sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=7.0.2001'
vite-plus: '*'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
vite-plus:
optional: true
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -10691,13 +10850,6 @@ packages:
typebox@1.1.38: typebox@1.1.38:
resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==}
typescript-eslint@8.61.0:
resolution: {integrity: sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
typescript-language-server@5.3.0: typescript-language-server@5.3.0:
resolution: {integrity: sha512-5puofxZHgFdAYtfNpmwCAvgtaYgg8wrUnH30m7Ze3QuguId5RNRadKASpOpyDxTyUdAF51FjhTdjntLw/EuWcQ==} resolution: {integrity: sha512-5puofxZHgFdAYtfNpmwCAvgtaYgg8wrUnH30m7Ze3QuguId5RNRadKASpOpyDxTyUdAF51FjhTdjntLw/EuWcQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -12452,6 +12604,81 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.20.0': '@oxc-resolver/binding-win32-x64-msvc@11.20.0':
optional: true optional: true
'@oxlint-tsgolint/darwin-arm64@7.0.2001':
optional: true
'@oxlint-tsgolint/darwin-x64@7.0.2001':
optional: true
'@oxlint-tsgolint/linux-arm64@7.0.2001':
optional: true
'@oxlint-tsgolint/linux-x64@7.0.2001':
optional: true
'@oxlint-tsgolint/win32-arm64@7.0.2001':
optional: true
'@oxlint-tsgolint/win32-x64@7.0.2001':
optional: true
'@oxlint/binding-android-arm-eabi@1.76.0':
optional: true
'@oxlint/binding-android-arm64@1.76.0':
optional: true
'@oxlint/binding-darwin-arm64@1.76.0':
optional: true
'@oxlint/binding-darwin-x64@1.76.0':
optional: true
'@oxlint/binding-freebsd-x64@1.76.0':
optional: true
'@oxlint/binding-linux-arm-gnueabihf@1.76.0':
optional: true
'@oxlint/binding-linux-arm-musleabihf@1.76.0':
optional: true
'@oxlint/binding-linux-arm64-gnu@1.76.0':
optional: true
'@oxlint/binding-linux-arm64-musl@1.76.0':
optional: true
'@oxlint/binding-linux-ppc64-gnu@1.76.0':
optional: true
'@oxlint/binding-linux-riscv64-gnu@1.76.0':
optional: true
'@oxlint/binding-linux-riscv64-musl@1.76.0':
optional: true
'@oxlint/binding-linux-s390x-gnu@1.76.0':
optional: true
'@oxlint/binding-linux-x64-gnu@1.76.0':
optional: true
'@oxlint/binding-linux-x64-musl@1.76.0':
optional: true
'@oxlint/binding-openharmony-arm64@1.76.0':
optional: true
'@oxlint/binding-win32-arm64-msvc@1.76.0':
optional: true
'@oxlint/binding-win32-ia32-msvc@1.76.0':
optional: true
'@oxlint/binding-win32-x64-msvc@1.76.0':
optional: true
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@@ -13063,22 +13290,6 @@ snapshots:
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.61.0
'@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.61.0
eslint: 10.5.0(jiti@2.7.0)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/scope-manager': 8.61.0
@@ -13093,8 +13304,8 @@ snapshots:
'@typescript-eslint/project-service@8.61.0(typescript@6.0.3)': '@typescript-eslint/project-service@8.61.0(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3)
'@typescript-eslint/types': 8.61.0 '@typescript-eslint/types': 8.65.0
debug: 4.4.3 debug: 4.4.3
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -13109,20 +13320,14 @@ snapshots:
dependencies: dependencies:
typescript: 6.0.3 typescript: 6.0.3
'@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
debug: 4.4.3
eslint: 10.5.0(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.61.0': {} '@typescript-eslint/types@8.61.0': {}
'@typescript-eslint/types@8.65.0': {}
'@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3)': '@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.61.0(typescript@6.0.3) '@typescript-eslint/project-service': 8.61.0(typescript@6.0.3)
@@ -13138,17 +13343,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.61.0
'@typescript-eslint/types': 8.61.0
'@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3)
eslint: 10.5.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.61.0': '@typescript-eslint/visitor-keys@8.61.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.61.0 '@typescript-eslint/types': 8.61.0
@@ -13190,7 +13384,7 @@ snapshots:
obug: 2.1.3 obug: 2.1.3
std-env: 4.1.0 std-env: 4.1.0
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/expect@4.1.8': '@vitest/expect@4.1.8':
dependencies: dependencies:
@@ -14453,8 +14647,6 @@ snapshots:
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@7.0.5: {}
immediate@3.0.6: {} immediate@3.0.6: {}
immer@10.2.0: {} immer@10.2.0: {}
@@ -15421,6 +15613,38 @@ snapshots:
'@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0
'@oxc-resolver/binding-win32-x64-msvc': 11.20.0 '@oxc-resolver/binding-win32-x64-msvc': 11.20.0
oxlint-tsgolint@7.0.2001:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 7.0.2001
'@oxlint-tsgolint/darwin-x64': 7.0.2001
'@oxlint-tsgolint/linux-arm64': 7.0.2001
'@oxlint-tsgolint/linux-x64': 7.0.2001
'@oxlint-tsgolint/win32-arm64': 7.0.2001
'@oxlint-tsgolint/win32-x64': 7.0.2001
oxlint@1.76.0(oxlint-tsgolint@7.0.2001):
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.76.0
'@oxlint/binding-android-arm64': 1.76.0
'@oxlint/binding-darwin-arm64': 1.76.0
'@oxlint/binding-darwin-x64': 1.76.0
'@oxlint/binding-freebsd-x64': 1.76.0
'@oxlint/binding-linux-arm-gnueabihf': 1.76.0
'@oxlint/binding-linux-arm-musleabihf': 1.76.0
'@oxlint/binding-linux-arm64-gnu': 1.76.0
'@oxlint/binding-linux-arm64-musl': 1.76.0
'@oxlint/binding-linux-ppc64-gnu': 1.76.0
'@oxlint/binding-linux-riscv64-gnu': 1.76.0
'@oxlint/binding-linux-riscv64-musl': 1.76.0
'@oxlint/binding-linux-s390x-gnu': 1.76.0
'@oxlint/binding-linux-x64-gnu': 1.76.0
'@oxlint/binding-linux-x64-musl': 1.76.0
'@oxlint/binding-openharmony-arm64': 1.76.0
'@oxlint/binding-win32-arm64-msvc': 1.76.0
'@oxlint/binding-win32-ia32-msvc': 1.76.0
'@oxlint/binding-win32-x64-msvc': 1.76.0
oxlint-tsgolint: 7.0.2001
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
@@ -16088,17 +16312,6 @@ snapshots:
typebox@1.1.38: {} typebox@1.1.38: {}
typescript-eslint@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
eslint: 10.5.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
typescript-language-server@5.3.0: typescript-language-server@5.3.0:
dependencies: dependencies:
vscode-jsonrpc: 5.0.1 vscode-jsonrpc: 5.0.1
@@ -16342,36 +16555,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
es-module-lexer: 2.1.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 22.20.0
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
jsdom: 29.1.1
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies: dependencies:
'@vitest/expect': 4.1.8 '@vitest/expect': 4.1.8
@@ -16402,6 +16585,36 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
es-module-lexer: 2.1.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
'@types/node': 22.20.0
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
jsdom: 29.1.1
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies: dependencies:
'@vitest/expect': 4.1.8 '@vitest/expect': 4.1.8

View File

@@ -0,0 +1,98 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
import { describe, expect, it } from 'vitest'
type Rules = Record<string, unknown>
interface Profile {
readonly count: number
readonly indexes: readonly number[]
readonly sha256: string
}
// A one-time audit against eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
// mapped @typescript-eslint/* to typescript/* and four extension rules to their
// Oxlint core equivalents. These fingerprints pin the resulting repository
// contract; they do not re-evaluate that deleted baseline or track its preset.
const profiles = {
source: {
count: 88,
indexes: [0, 1, 4, 5],
sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
},
example: {
count: 87,
indexes: [0, 1, 2, 4, 5],
sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
},
test: {
count: 83,
indexes: [0, 3, 4, 5],
sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
},
} as const satisfies Record<string, Profile>
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isUnknownArray(value: unknown): value is unknown[] {
return Array.isArray(value)
}
function severity(value: unknown): 0 | 1 | 2 {
const level = isUnknownArray(value) ? value[0] : value
if (level === 'off' || level === 0) return 0
if (level === 'warn' || level === 'warning' || level === 1) return 1
if (level === 'error' || level === 2) return 2
throw new Error(`unsupported lint severity: ${JSON.stringify(level)}`)
}
function normalizedRules(rules: Rules): Rules {
return Object.fromEntries(Object.entries(rules)
.filter(([, value]) => severity(value) > 0)
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, value]) => {
const options = isUnknownArray(value) ? value.slice(1) : []
return [name, [severity(value), ...options]]
}))
}
function mergedRules(overrides: readonly unknown[], indexes: readonly number[]): Rules {
const merged: Rules = {}
for (const index of indexes) {
const override = overrides[index]
if (!isRecord(override) || !isRecord(override.rules)) {
throw new Error(`.oxlintrc.json override ${index} must contain a rules object`)
}
Object.assign(merged, override.rules)
}
return normalizedRules(merged)
}
describe('Oxlint repository rule fingerprint', () => {
const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url))
const result = parseConfigFileTextToJson(path, readFileSync(path, 'utf8'))
if (result.error !== undefined) {
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
}
const parsed: unknown = result.config
if (!isRecord(parsed) || !Array.isArray(parsed.overrides)) {
throw new Error('.oxlintrc.json must contain an overrides array')
}
const overrides: readonly unknown[] = parsed.overrides
it('pins the complete override shape', () => {
expect(overrides).toHaveLength(6)
})
it.each(Object.entries(profiles))('pins the %s rule profile', (_name, profile) => {
const rules = mergedRules(overrides, profile.indexes)
const fingerprint = createHash('sha256').update(JSON.stringify(rules)).digest('hex')
expect(Object.keys(rules)).toHaveLength(profile.count)
expect(fingerprint).toBe(profile.sha256)
})
})

View File

@@ -0,0 +1,250 @@
import { spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
import { describe, expect, it } from 'vitest'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const eslintCli = fileURLToPath(new URL('../node_modules/eslint/bin/eslint.js', import.meta.url))
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isUnknownArray(value: unknown): value is unknown[] {
return Array.isArray(value)
}
function runStagedFormatter(paths: readonly string[]) {
return spawnSync(process.execPath, [eslintCli, '--config', 'eslint.format.config.mjs', '--fix', '--no-warn-ignored', ...paths], {
cwd: repositoryRoot,
encoding: 'utf8',
env: { ...process.env, NO_COLOR: '1' },
})
}
function runOxlint(args: readonly string[], env: NodeJS.ProcessEnv = {}) {
return spawnSync(process.execPath, [oxlintCli, ...args], {
cwd: repositoryRoot,
encoding: 'utf8',
env: { ...process.env, NO_COLOR: '1', ...env },
})
}
function normalizedOutput(result: ReturnType<typeof runOxlint>): string {
return `${result.stdout}${result.stderr}`.replaceAll('\\', '/')
}
async function writeContractConfig(suffix: string): Promise<string> {
const path = join(repositoryRoot, `.oxlintrc.contract-${suffix}.json`)
await writeFile(path, JSON.stringify({ extends: ['./.oxlintrc.json'], ignorePatterns: [] }))
return path
}
describe('Oxlint executable contract', () => {
it('discovers the owning TypeScript project for every file class', async () => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)
const probes = [
['host package source', 'packages/fs/fs-policy/src', 'packages/fs/fs-policy/tsconfig.json'],
['host package test', 'packages/fs/fs-policy/tests', 'tsconfig.host.json'],
['client package source', 'packages/client/ui-primitives/src', 'packages/client/ui-primitives/tsconfig.json'],
['client package test', 'packages/client/ui-trajectory/tests', 'tsconfig.client.json'],
['example', 'examples/headless-agent/tests', 'tsconfig.host.json'],
['website', 'website', 'tsconfig.host.json'],
] as const
const source = `export function probePromise(): Promise<void> {
return Promise.resolve()
}
probePromise()
`
try {
const paths: Array<readonly [label: string, path: string, tsconfig: string]> = []
for (const [label, parent, tsconfig] of probes) {
const path = join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`)
await writeFile(path, source)
paths.push([label, relative(repositoryRoot, path), tsconfig])
}
const clientScript = 'scripts/client-bundle-purity.spec.ts'
const result = runOxlint([
'--config',
relative(repositoryRoot, configPath),
'--format',
'unix',
...paths.map(([, path]) => path),
clientScript,
], { OXC_LOG: 'debug' })
const output = normalizedOutput(result)
expect(result.error).toBeUndefined()
expect(result.status, output).toBe(1)
for (const [label, path, tsconfig] of paths) {
expect(output, label).toContain(`${path.replaceAll('\\', '/')}:5:1: Promises must be awaited`)
expect(output, `${label} project`).toContain(
`Got tsconfig for file ${join(repositoryRoot, path).replaceAll('\\', '/')}: ${join(repositoryRoot, tsconfig).replaceAll('\\', '/')}`,
)
}
expect(output.match(/typescript\(no-floating-promises\)/g)).toHaveLength(probes.length)
expect(output, 'client aggregate script project').toContain(
`Got tsconfig for file ${join(repositoryRoot, clientScript).replaceAll('\\', '/')}: ${join(repositoryRoot, 'tsconfig.client.json').replaceAll('\\', '/')}`,
)
expect(output).not.toContain('Unmatched file:')
} finally {
await Promise.all([
...probes.map(([, parent]) => rm(join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`), { force: true })),
rm(configPath, { force: true }),
])
}
}, 20_000)
it('runs JavaScript compatibility and nursery rules', async () => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)
const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
const source = `export function firstProbe(): number {
const first = 1
const second = 2
return first + second
}
export function secondProbe(): number {
const first = 1
const second = 2
return first + second
}
export function hasValue(value: string): boolean {
return value !== undefined
}
export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
`
try {
await writeFile(path, source)
const result = runOxlint([
'--config',
relative(repositoryRoot, configPath),
'--format',
'unix',
relative(repositoryRoot, path),
])
const output = normalizedOutput(result)
expect(result.error).toBeUndefined()
expect(result.status, output).toBe(1)
expect(output).toContain('@stylistic(max-len)')
expect(output).toContain('sonarjs(no-identical-functions)')
expect(output).toContain('typescript(no-unnecessary-condition)')
} finally {
await Promise.all([
rm(path, { force: true }),
rm(configPath, { force: true }),
])
}
}, 20_000)
it('keeps formatter rules aligned with Oxlint validation', async () => {
const oxlintPath = join(repositoryRoot, '.oxlintrc.json')
const result = parseConfigFileTextToJson(oxlintPath, await readFile(oxlintPath, 'utf8'))
if (result.error !== undefined) {
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
}
const parsed = result.config as unknown
if (!isRecord(parsed) || !isUnknownArray(parsed.overrides)) {
throw new Error('.oxlintrc.json must contain an overrides array')
}
const stylisticOverride = parsed.overrides.find((value: unknown) =>
isRecord(value) && isRecord(value.rules) && '@stylistic/max-len' in value.rules)
if (!isRecord(stylisticOverride) || !isRecord(stylisticOverride.rules)) {
throw new Error('.oxlintrc.json must contain the @stylistic validator override')
}
const validatorRules = { ...stylisticOverride.rules }
const maxLen = validatorRules['@stylistic/max-len']
delete validatorRules['@stylistic/max-len']
const formatterUrl = pathToFileURL(join(repositoryRoot, 'eslint.format.config.mjs')).href
const formatterModule = await import(formatterUrl) as unknown
if (!isRecord(formatterModule) || !isUnknownArray(formatterModule.default)) {
throw new Error('eslint.format.config.mjs must default-export a config array')
}
const formatterOverride = formatterModule.default.find((value: unknown) => isRecord(value) && isRecord(value.rules))
if (!isRecord(formatterOverride) || !isRecord(formatterOverride.rules)) {
throw new Error('eslint.format.config.mjs must contain a rules object')
}
expect(validatorRules).toStrictEqual(formatterOverride.rules)
expect(maxLen).toStrictEqual(['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }])
})
it('reports an unused suppression', async () => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)
const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
try {
await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
const result = runOxlint([
'--config',
relative(repositoryRoot, configPath),
'--format',
'unix',
relative(repositoryRoot, path),
])
const output = normalizedOutput(result)
expect(result.error).toBeUndefined()
expect(result.status, output).toBe(0)
expect(output).toContain('Unused oxlint-disable directive')
} finally {
await Promise.all([
rm(path, { force: true }),
rm(configPath, { force: true }),
])
}
})
it('accepts an ignored-only staged selection', () => {
const result = runOxlint([
'--fix',
'--no-error-on-unmatched-pattern',
'scripts/install-lefthook.mjs',
])
expect(result.error).toBeUndefined()
expect(result.status, normalizedOutput(result)).toBe(0)
})
it('applies staged stylistic fixes before Oxlint validation', async () => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
const path = join(directory, 'fix.ts')
try {
await mkdir(directory, { recursive: true })
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
const relativePath = relative(repositoryRoot, path)
const formatResult = runStagedFormatter([relativePath])
const lintResult = runOxlint(['--config', relative(repositoryRoot, configPath), '--fix', relativePath])
expect(formatResult.error).toBeUndefined()
expect(formatResult.status, normalizedOutput(formatResult)).toBe(0)
expect(lintResult.error).toBeUndefined()
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
} finally {
await Promise.all([
rm(directory, { recursive: true, force: true }),
rm(configPath, { force: true }),
])
}
}, 20_000)
})

View File

@@ -42,6 +42,18 @@ function withPnpmEntrypoint<T>(action: () => T): T {
} }
} }
function withEnv<T>(name: string, value: string | undefined, action: () => T): T {
const previous = process.env[name]
if (value === undefined) Reflect.deleteProperty(process.env, name)
else process.env[name] = value
try {
return action()
} finally {
if (previous === undefined) Reflect.deleteProperty(process.env, name)
else process.env[name] = previous
}
}
describe('gate graph validation', () => { describe('gate graph validation', () => {
it.each([ it.each([
'ci-primary', 'ci-primary',
@@ -96,6 +108,32 @@ describe('gate graph validation', () => {
}) })
}) })
describe('Oxlint gate', () => {
it('uses the package script when no worker bound is configured', () => {
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
expect(subject).toMatchObject({
id: 'lint',
displayCommand: 'pnpm run lint',
command: process.execPath,
args: ['/private/pnpm.cjs', 'run', 'lint'],
})
})
it('surfaces the configured worker bound on the shared package script', () => {
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
expect(subject).toMatchObject({
id: 'lint',
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint',
command: process.execPath,
args: ['/private/pnpm.cjs', 'run', 'lint'],
})
})
})
describe('Node 24 consumer graph', () => { describe('Node 24 consumer graph', () => {
it('owns the seven-command pool and orders restored-artifact consumers', () => { it('owns the seven-command pool and orders restored-artifact consumers', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers')) const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))

View File

@@ -181,10 +181,6 @@ function pnpmInvocation(args: string[]): Pick<Gate, 'command' | 'args'> {
return { command: process.execPath, args: [entrypoint, ...args] } return { command: process.execPath, args: [entrypoint, ...args] }
} }
function nodeOptions(...options: string[]): string {
return [process.env.NODE_OPTIONS, ...options].filter(option => option !== undefined && option !== '').join(' ')
}
/** /**
* Construct the complete gate list for a named aggregate. * Construct the complete gate list for a named aggregate.
* @param selected - aggregate mode to construct. * @param selected - aggregate mode to construct.
@@ -380,43 +376,11 @@ function ciWindowsObservationalGates(): Gate[] {
] ]
} }
function lintGate(eslintTargets: readonly string[] = ['.']): Gate { function lintGate(): Gate {
const concurrencyArgs = eslintConcurrencyArgs() const raw = process.env.DSH_OXLINT_THREADS
if (process.env.DSH_ESLINT_CACHE === '1') { return pnpmScript('lint', 'lint', raw === undefined || raw === ''
return pnpmExec('lint', [ ? {}
'eslint', : { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
...eslintTargets,
...concurrencyArgs,
'--cache',
'--cache-location',
'.cache/eslint/',
'--cache-strategy',
'content',
], {
label: 'lint',
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
if (concurrencyArgs.length > 0) {
return pnpmExec('lint', ['eslint', ...eslintTargets, ...concurrencyArgs], {
label: 'lint',
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
return pnpmScript('lint', 'lint', {
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
function eslintConcurrencyArgs(): string[] {
const raw = process.env.DSH_ESLINT_CONCURRENCY
if (raw === undefined || raw === '') return []
if (raw === 'auto') return ['--concurrency=auto']
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`run-gates: DSH_ESLINT_CONCURRENCY must be a positive integer or auto, got ${JSON.stringify(raw)}.`)
}
return [`--concurrency=${raw}`]
} }
function coverageGate(): Gate { function coverageGate(): Gate {

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { resolveOxlintInvocation } from './run-oxlint.ts'
describe('Oxlint invocation', () => {
it('preserves the ordinary default invocation', () => {
expect(resolveOxlintInvocation(['.'], { PATH: '/bin' })).toEqual({
args: ['.'],
env: { PATH: '/bin' },
})
})
it('bounds both worker pools from one setting', () => {
expect(resolveOxlintInvocation(['.', '--fix'], { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '12' })).toEqual({
args: ['.', '--fix', '--threads=4'],
env: { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '4' },
})
})
it.each(['0', '-1', '1.5', 'auto'])('rejects invalid worker bound %s', (value) => {
expect(() => resolveOxlintInvocation(['.'], { DSH_OXLINT_THREADS: value }))
.toThrow('DSH_OXLINT_THREADS must be a positive integer')
})
it('rejects a competing direct worker bound', () => {
expect(() => resolveOxlintInvocation(['.', '--threads=2'], { DSH_OXLINT_THREADS: '4' }))
.toThrow('use DSH_OXLINT_THREADS instead')
})
})

46
scripts/run-oxlint.ts Normal file
View File

@@ -0,0 +1,46 @@
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
/** Complete Oxlint child-process arguments and environment. */
export interface OxlintInvocation {
readonly args: readonly string[]
readonly env: NodeJS.ProcessEnv
}
/**
* Apply the repository worker bound to both Oxlint backends.
* @param args - Oxlint CLI arguments requested by the caller.
* @param env - Environment inherited by the Oxlint process.
* @returns the complete CLI arguments and child environment.
*/
export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation {
const raw = env.DSH_OXLINT_THREADS
if (raw === undefined || raw === '') return { args: [...args], env: { ...env } }
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
}
if (args.some(arg => arg === '--threads' || arg.startsWith('--threads='))) {
throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.')
}
return {
args: [...args, `--threads=${raw}`],
env: { ...env, GOMAXPROCS: raw },
}
}
function main(): void {
const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env)
const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
env: invocation.env,
stdio: 'inherit',
})
if (result.error !== undefined) throw result.error
process.exitCode = result.status ?? 1
}
const entrypoint = process.argv[1]
if (entrypoint !== undefined && resolve(entrypoint) === fileURLToPath(import.meta.url)) main()

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,7 @@ interface InvariantHost {
type PluginFiber = ReturnType<RegistryService['plugin']> type PluginFiber = ReturnType<RegistryService['plugin']>
const hosts = new WeakMap<Context, InvariantHost>() const hosts = new WeakMap<Context, InvariantHost>()
// eslint-disable-next-line @typescript-eslint/unbound-method -- every call below supplies its RegistryService receiver explicitly. // oxlint-disable-next-line typescript/unbound-method -- every call below supplies its RegistryService receiver explicitly.
const originalPlugin = RegistryService.prototype.plugin const originalPlugin = RegistryService.prototype.plugin
RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, getOuterStack?: () => string[]) { RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, getOuterStack?: () => string[]) {

View File

@@ -126,7 +126,7 @@ function heritageExemption(
returnType = d.type.type returnType = d.type.type
} else continue } else continue
baseParams ??= new Set() baseParams ??= new Set()
// Leading underscores are the deliberately-unused marker (eslint // Leading underscores are the deliberately-unused marker (lint
// argsIgnorePattern), not a rename: `_cwd` overriding `cwd` is the // argsIgnorePattern), not a rename: `_cwd` overriding `cwd` is the
// same parameter, so compare underscore-stripped on both sides. // same parameter, so compare underscore-stripped on both sides.
for (const p of params) if (ts.isIdentifier(p.name)) baseParams.add(p.name.text.replace(/^_+/, '')) for (const p of params) if (ts.isIdentifier(p.name)) baseParams.add(p.name.text.replace(/^_+/, ''))

View File

@@ -98,6 +98,8 @@ export default defineConfig({
'packages/*/*/src/types.ts', 'packages/*/*/src/types.ts',
'packages/*/*/src/bin.ts', 'packages/*/*/src/bin.ts',
'packages/*/*/src/worker.ts', 'packages/*/*/src/worker.ts',
// A killed executable lint-contract test can leave a non-product source probe behind.
'packages/*/*/src/oxlint-contract-*.ts',
// GUI step-1 skeleton (PR #500): client/web UI files whose remaining // GUI step-1 skeleton (PR #500): client/web UI files whose remaining
// branches need a browser-grade harness the jsdom lane doesn't cover // branches need a browser-grade harness the jsdom lane doesn't cover
// yet. TODO(gui): cover and remove as the client test lane matures. // yet. TODO(gui): cover and remove as the client test lane matures.