Merge master through adaptive directory picker

This commit is contained in:
Hypatia May
2026-07-30 22:21:40 +08:00
104 changed files with 4684 additions and 111 deletions

View File

@@ -2,5 +2,5 @@
# 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-22-evidence-based-larger-hosted-runners.md
2026-07-22-evidence-based-larger-hosted-runners.md: 983d5520bd73fc3cf82c37bf0d4a9ff1c6e6f51c
2026-07-22-evidence-based-larger-hosted-runners.zh.md: a86dcf2c60d7b950e7557e84ef6993e712a2ce09
2026-07-22-evidence-based-larger-hosted-runners.md: d46b8291ec05e997728da76354354f9e36bd2fb4
2026-07-22-evidence-based-larger-hosted-runners.zh.md: e05ad30a713258ed7bc3d8099f8d6fab3d7c0c5d

View File

@@ -18,9 +18,9 @@ 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.
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.
Linux primary work uses three independent 32-core jobs. Coverage runs alone with its own worker bound, and the static scheduler owns source and documentation gates that do not consume emitted output. The third job owns the single Linux build, then starts lint, Node 24 runtime compatibility, build-backed snapshots, documentation typechecking, and all artifact consumers against that tree. This [independent consumer build](2026-07-30-independent-ci-consumer-build.md) lets all three jobs request runners immediately without duplicating compilation or transferring a run-scoped artifact. 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 consumes the consumer lane's complete project-reference output. 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 artifact boundary remains explicit. `scripts/publint-all.ts` calls publint's supported API against an in-memory publication view formed from each manifest's declared files plus npm's mandatory metadata, avoiding one package-manager pack process per package. `scripts/verify-built-package-invariants.mjs` stages the declared `lib/` files below the real package and imports its compiled self-reference through plain Node and Cordis Loader normalization; a runtime chunk omitted from the publication contract still fails.
@@ -68,7 +68,7 @@ An additional serial Linux reference runs on the in-house self-hosted pool (`vm-
**Keep build behind typecheck.** This orders independent compiler invocations and turns snapshot replay into a three-stage critical chain. Build output has its own success dependency, so only snapshot and publication consumers wait for it.
**Keep static gates and post-build consumers on one runner.** Reusing one workspace avoids a setup wave and artifact transfer, but build-duration variance delays every consumer and leaves their lint and snapshot tails after the static result. A run-scoped built tree preserves one exact build while independent jobs keep both complete paths within the observed target.
**Publish the static job's build to post-build consumers.** A run-scoped artifact preserves one exact build, but the workflow can only consume it by waiting for the entire static job and then requesting another runner. The [independent consumer build](2026-07-30-independent-ci-consumer-build.md) assigns the single Linux build to its actual consumers instead.
**Keep the complete required path on standard GitHub-hosted capacity.** This avoids repository-external runner configuration, but exact-head standard-runner runs remain materially slower and can spend longer queued behind shared capacity. Standard-hosted compatibility and serial references preserve portable evidence without making that slower topology the ordinary primary path.
@@ -80,7 +80,7 @@ An additional serial Linux reference runs on the in-house self-hosted pool (`vm-
The required topology pays one setup wave per 32-core lane and retains no shard selectors. Every ordinary pull request consumes paid enterprise Linux and Windows minutes; manual benchmarks add other sizes only when remeasurement is useful.
GitHub rounds each larger-runner execution up to a whole minute, so complete-job measurement exposes both billed time and workflow complexity. Splitting Linux repeats setup twice and transfers one built tree, but isolates coverage, static gates, and post-build consumers from each other's critical paths without repeating the build; consolidating Windows avoids repeating its slower setup.
GitHub rounds each larger-runner execution up to a whole minute, so complete-job measurement exposes both billed time and workflow complexity. Splitting Linux repeats setup twice, but the consumer lane owns the only built tree and coverage, static gates, and post-build consumers enter runner allocation independently; consolidating Windows avoids repeating its slower setup.
Performance targets are observations, not cancellation deadlines or correctness requirements. Manual all-size and serial suites remain available when image, dependency, scheduler, or pricing changes need remeasurement.

View File

@@ -18,11 +18,11 @@ Status: implemented
原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除因此未使用的诊断路径无法继续维系第二套 CI 架构。
Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib``packages/*/*/lib``vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 Oxlint 的文件发现范围因为这些进程重叠执行时产物检查会删除这些目录。pnpm store 会得到恢复但缓存上传不会进入拉取请求关键路径Oxlint 没有由仓库管理的结果缓存。性能报告采用每个作业从 `startedAt``completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器负责不消费生成输出的源码和文档门禁。第三个作业负责唯一一次 Linux 构建,随后让 lint、Node 24 运行时兼容性、依赖构建产物的快照、文档类型检查和所有产物消费方基于该目录树启动。这种[消费方独立构建](2026-07-30-independent-ci-consumer-build.md)使 3 个作业都能立即请求运行器,而无需重复编译或传输仅供本次运行使用的产物。生成的 NodeNext 消费方目录不会纳入 Oxlint 的文件发现范围因为这些进程重叠执行时产物检查会删除这些目录。pnpm store 会得到恢复但缓存上传不会进入拉取请求关键路径Oxlint 没有由仓库管理的结果缓存。性能报告采用每个作业从 `startedAt``completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查只构建一次完整 project-reference 。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。
门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查以消费方通道的完整 project-reference 输出为输入。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。
产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API该视图由每个 manifest元数据清单声明的文件和 npm 强制要求的元数据组成,从而避免为每个包启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布契约只要遗漏一个运行时分片,检查仍会失败。
产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API该视图由每个 manifest元数据清单声明的文件和 npm 强制要求的元数据组成,从而避免为每个包package启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布契约只要遗漏一个运行时分片,检查仍会失败。
Windows 以一次 32 核环境设置同时承载阻塞性构建、生产网站和观测性的构建产物契约。重复的 lint、覆盖率和快照清单由 Linux 承担,因为在 Windows 上运行这些观测性副本会延长付费关键路径,却不会新增任何阻塞性平台契约。
@@ -68,7 +68,7 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
**让构建继续等待类型检查。** 此方案会给相互独立的编译器调用排定先后顺序,并把快照回放变成 3 阶段关键链。构建输出本身有独立的成功依赖关系,因此只有快照和发布消费方需要等待它。
**将静态门禁和构建后消费方保留在同一台运行器上。** 复用同一个工作区可以省去一轮设置和一次产物传输,但构建耗时的波动会延迟每个消费方,并使消费方的 lint 和快照尾段延续到静态结果之后。仅供本次运行使用的已构建目录树可以保留同一份构建结果,而相互独立的作业能让两条完整路径都保持在实测目标内
**将静态作业的构建发布给构建后消费方。** 仅供本次运行使用的产物能保留同一份构建结果,但工作流要消费它,只能先等待整个静态作业完成,再请求另一台运行器。[消费方独立构建](2026-07-30-independent-ci-consumer-build.md)则转而让实际消费方负责唯一一次 Linux 构建
**将完整必需路径保留在 GitHub 标准托管容量上。** 此方案可以避免依赖仓库外部的运行器配置,但标准运行器上的分支头精确运行仍明显更慢,也可能因共享容量而排队更久。标准托管兼容性作业和串行参考流程保留可移植证据,无需让这套较慢的拓扑成为普通主路径。
@@ -80,7 +80,7 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
必需拓扑中的每个 32 核通道只承担 1 轮设置开销,且不保留分片选择器。每个普通拉取请求都会消耗付费的企业级 Linux 和 Windows 运行器分钟数;只有在重新测量有价值时,手动基准测试才会加入其他规格。
GitHub 会把每次大型运行器执行向上取整到整分钟计费,因此完整作业测量能同时呈现计费时长与工作流复杂度。拆分 Linux 会重复两轮设置并传输一份已构建目录树,但能使覆盖率、静态门禁与构建后消费方不再相互进入关键路径,且无需重复构建;合并 Windows 则避免重复其耗时更长的设置。
GitHub 会把每次大型运行器执行向上取整到整分钟计费,因此完整作业测量能同时呈现计费时长与工作流复杂度。拆分 Linux 会重复两轮设置,但消费方通道拥有唯一一份已构建目录树,覆盖率、静态门禁与构建后消费方分别进入运行器分配;合并 Windows 则避免重复其耗时更长的设置。
性能目标是观测结果,而非取消截止时间或正确性要求。当映像、依赖、调度器或定价发生变化而需要重新测量时,仍可使用手动全规格和串行套件。

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-30-independent-ci-consumer-build.md
2026-07-30-independent-ci-consumer-build.md: ea87d8051a30c282bdf57ddc3226072be8cb7f24
2026-07-30-independent-ci-consumer-build.zh.md: 1b5faf73711bd522ddf0ae04f870e0401a481c0e

View File

@@ -0,0 +1,35 @@
# Agent Note: Independent CI consumer build
Status: implemented
English | [中文](2026-07-30-independent-ci-consumer-build.zh.md)
## Problem
The [larger-runner topology](2026-07-22-evidence-based-larger-hosted-runners.md) gave the static and built-consumer inventories separate jobs, but the static job owned their shared build. It uploaded the emitted tree only after every static gate completed, and the consumer job declared a job-level dependency before restoring that tree. Compiled-output snapshots and publication checks genuinely require a complete build; they do not require runtime-closure checks, documentation generation, module-graph verification, or Knip.
That wider dependency made runner availability part of the required critical chain. In one failover run, static waited 8 minutes 1 second for a runner and ran for 1 minute 41 seconds; only then could consumers enter the same shared pool, where they waited another 10 minutes 34 seconds before running for 1 minute 58 seconds. Reusing the static build saved repository work but serialized two independent runner allocations.
## Decision
The three required Linux jobs enter runner allocation independently. Coverage remains source-only. Static owns source and documentation checks that do not consume emitted output. The consumer job owns the single Linux build together with documentation typechecking, compiled-output snapshots, publication checks, NodeNext checks, and built-bin smokes.
The consumer's internal gate graph preserves the real dependency. Build and source-only Node compatibility start first; publint waits for build, built-package invariants validate that publication view, and every compiled-output consumer waits for that validation. Example and Web snapshots therefore continue to exercise current `lib/` output under plain Node, while no GitHub job waits for an unrelated job or transfers a built-tree artifact.
Windows and serial reference aggregates retain their own build ownership. The change is confined to the required pull-request Linux topology; `all checks passed` still aggregates the same named jobs and fails for any unsuccessful dependency.
## Alternatives considered
**Keep publishing the static job's build.** This preserves one build but cannot express the actual step-level dependency: GitHub makes the consumer wait for the whole static job before it can request a runner. The saved build time is smaller than the repeated queue delay during failover saturation.
**Build independently in both jobs.** Removing the job dependency while leaving build in static would restore parallel allocation, but every pull request would compile the same tree twice. Moving documentation typechecking and build ownership to the consumer preserves one build.
**Add a dedicated build job.** A narrow producer would make the dependency name accurate, but it would add a fourth setup and runner-allocation stage before consumers. The consumer already owns every long-lived use of emitted output, so a separate producer has no second independent consumer.
**Combine static and consumers only during failover.** One long job would avoid the second allocation, but conditional job inventories and result aggregation would create a second CI topology. Independent jobs preserve the same graph on hosted and failover pools.
## Consequences
Static and consumer queue delays overlap instead of accumulating. The consumer's active time includes the build, while the static job becomes shorter and artifact upload, download, compression, and extraction disappear. Total Linux build count remains one.
A static failure no longer prevents the consumer inventory from producing its own evidence; the final verdict still fails. Build and documentation-typecheck failures appear under `node 24 / snapshots and artifacts` rather than `node 24 / static`, matching the job that owns their output dependency.

View File

@@ -0,0 +1,35 @@
# Agent Note: 消费方独立构建
Status: implemented
[English](2026-07-30-independent-ci-consumer-build.md) | 中文
## 问题
[大型运行器拓扑](2026-07-22-evidence-based-larger-hosted-runners.md)将静态门禁清单和构建后消费方清单分配给不同作业,但二者共用的构建由静态作业负责。静态作业要等所有静态门禁完成后才上传生成的目录树,消费方作业则在恢复该目录树前声明了作业级依赖。基于编译输出的快照与发布校验确实需要完整构建,但不依赖运行时依赖闭包检查、文档生成、模块图验证或 Knip。
这项过宽的依赖使运行器可用性成为必需关键链的一环。一次故障切换运行中,静态作业等待运行器 8 分 1 秒,随后运行 1 分 41 秒;直到此时,消费方作业才能进入同一个共享池,它又等待 10 分 34 秒,随后运行 1 分 58 秒。复用静态作业的构建省去了部分仓库工作,却让两次原本相互独立的运行器分配串行发生。
## 决策
3 个必需 Linux 作业分别进入运行器分配。覆盖率仍只消费源码。静态作业负责无需消费生成输出的源码检查与文档检查。消费方作业负责唯一一次 Linux 构建以及文档类型检查、基于编译输出的快照、发布校验、NodeNext 检查和 built-bin 冒烟测试。
消费方内部的门禁图保留实际依赖关系。构建和只消费源码的 Node 兼容性检查率先启动publint 等待构建完成,已构建包不变式检查会验证该发布视图,所有编译输出消费方都等待这项验证完成。因此,示例和 Web 快照仍会在普通 Node 下验证当前 `lib/` 输出;同时,没有任何 GitHub 作业需要等待无关作业或传输已构建目录树产物。
Windows 与串行参考聚合流程仍各自负责自身构建。本变更仅涉及拉取请求的必需 Linux 拓扑;`all checks passed` 仍聚合同一批具名作业,任一依赖未成功时都会失败。
## 曾考虑的替代方案
**继续发布静态作业的构建。** 此方案只需构建一次却无法表达实际的步骤级依赖GitHub 会让消费方等到整个静态作业结束后才可请求运行器。故障切换池饱和时,再次排队的延迟超过了省下的构建时间。
**在两个作业中分别独立构建。** 在静态作业中保留构建、同时移除作业依赖,可以恢复并行分配,但每个拉取请求都会对同一目录树编译两次。将文档类型检查和构建职责移给消费方,则仍只需构建一次。
**新增专用构建作业。** 职责单一的生产方能让依赖名称与实际关系相符,但会在消费方之前新增第 4 个需要设置和分配运行器的阶段。所有需要持续使用生成输出的任务都已由消费方作业负责,因此单独增设生产方也没有第二个相互独立的消费方。
**仅在故障切换期间合并静态作业与消费方作业。** 单个长作业可以避免第二次分配,但带条件分支的作业清单与结果聚合会形成第二套 CI 拓扑。独立作业能让托管池与故障切换池使用同一作业图。
## 后果
静态作业与消费方作业的排队延迟会相互重叠不再累加。消费方的活动耗时包含构建静态作业则变短产物上传、下载、压缩和解压步骤全部消失。Linux 构建总次数仍为 1 次。
静态作业失败不再阻止消费方清单生成自身证据;最终判定仍会失败。构建与文档类型检查失败会归入 `node 24 / snapshots and artifacts` 而非 `node 24 / static`,这一归类与输出依赖的实际归属一致。