From 1b8dc772ac77569f2fe010c3992dfada8978d5bb Mon Sep 17 00:00:00 2001 From: "d3mlabs-ai-flow[bot]" <305891656+d3mlabs-ai-flow[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:22:42 -0400 Subject: [PATCH] ai-flow /build: capture learnings from the build pass Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com> --- .cursor/rules/learnings-index.mdc | 10 +++++ .../pre-bundle-stdlib-chain/SKILL.md | 40 +++++++++++++++++++ .../learnings/sorbet-splat-unsafe/SKILL.md | 39 ++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 .cursor/skills/learnings/pre-bundle-stdlib-chain/SKILL.md create mode 100644 .cursor/skills/learnings/sorbet-splat-unsafe/SKILL.md diff --git a/.cursor/rules/learnings-index.mdc b/.cursor/rules/learnings-index.mdc index 6ff67a0..0ff77dc 100644 --- a/.cursor/rules/learnings-index.mdc +++ b/.cursor/rules/learnings-index.mdc @@ -54,9 +54,19 @@ propose a retirement, a consolidation, or a glob-scoped sub-index split. normalization gate: it must recognize every layout Cursor writes, since render re-serializes its output and any mis-split compounds on the next pull. → .cursor/skills/learnings/plan-parse-normalizes/ +- [architecture/pre-bundle-stdlib-chain] The dev/deps require chain loads + before the bundle exists: stdlib-only, sigils capped at typed: true; + sorbet-runtime is required once in src/dev.rb, self-required only by the + consumer-loaded deps hooks and bin/test.rb's rake_test_argv helper. + → .cursor/skills/learnings/pre-bundle-stdlib-chain/ ## toolchain +- [toolchain/sorbet-splat-unsafe] Sorbet rejects runtime-sized splats + (error 7019) — T.let/T.cast never help; take argv as an Array param and + keep the one T.unsafe at the stdlib boundary. + → .cursor/skills/learnings/sorbet-splat-unsafe/ + ## org tier diff --git a/.cursor/skills/learnings/pre-bundle-stdlib-chain/SKILL.md b/.cursor/skills/learnings/pre-bundle-stdlib-chain/SKILL.md new file mode 100644 index 0000000..d609d19 --- /dev/null +++ b/.cursor/skills/learnings/pre-bundle-stdlib-chain/SKILL.md @@ -0,0 +1,40 @@ +--- +name: pre-bundle-stdlib-chain +description: >- + MUST be used when editing lib/dev/deps core files (deps.rb, config, dsl, + tap, lockfile, fetcher, dependency_installer, dependency*), raising a + typed: sigil, or adding a require "sorbet-runtime" anywhere under Dev. +--- + +# The pre-bundle chain is stdlib-only; sorbet-runtime loads once at the root + +`require "dev/deps"` is loaded by bin/setup.rb, bin/test.rb, and consumer +bootstrap BEFORE the bundle exists, so that chain (deps.rb → config → dsl, +tap, cli_ui, lockfile, fetcher, dependency_installer, dependency*) must +stay stdlib-only: no sorbet-runtime, no `sig`, sigils capped at +`typed: true` (or `false` where Data.define blocks Sorbet). The rubocop +Sorbet/StrictSigil excludes document each holdout — a non-strict sigil +there is deliberate, not a CI gap. Everything else lives under the `Dev` +namespace and gets sorbet-runtime from the single early require in +src/dev.rb (every entry point requires "dev" first); the only +self-requiring exceptions are the deps hooks (loaded standalone by +consumer dependencies.rb via install-build-deps) and lib/rake_test_argv.rb +(loaded standalone by bin/test.rb). + +Wrong: + +```ruby +# lib/dev/deps/config.rb — breaks bin/setup.rb on a fresh machine: +require "sorbet-runtime" +``` + +Right: + +```ruby +# typed: true (ceiling for the pre-bundle chain; no sigs, no new require) +``` + +learned-from: dev#140 review — eight "why not typed: strict / why did CI +not catch this" threads, all answered by this constraint; the same review +centralized sorbet-runtime into src/dev.rb. +date: 2026-09-04 diff --git a/.cursor/skills/learnings/sorbet-splat-unsafe/SKILL.md b/.cursor/skills/learnings/sorbet-splat-unsafe/SKILL.md new file mode 100644 index 0000000..8d2689e --- /dev/null +++ b/.cursor/skills/learnings/sorbet-splat-unsafe/SKILL.md @@ -0,0 +1,39 @@ +--- +name: sorbet-splat-unsafe +description: >- + MUST be used when a sig'd method forwards a runtime-built argv to a + splatting call (system, Open3.capture3, another rest-args method) or when + reviewing a T.unsafe/T.let/T.cast around a splat. +--- + +# Runtime-sized splats: design them away, don't annotate them + +Sorbet rejects `f(*array)` whenever the array's size isn't statically known +(error 7019) — regardless of declared element type, so `T.let`/`T.cast` +never help, and it bites any target with fixed positional params +(`Kernel#system` and `Open3.capture3` both take an env-or-command first +param). Restructure so no call site splats: give your own helpers an +`argv: T::Array[String]` parameter instead of rest args, and keep the one +unavoidable `T.unsafe` at the stdlib boundary, commented with error 7019. + +Wrong: + +```ruby +argv = ["docker", "inspect", "--format", fmt, *ids] +sources = capture(*T.unsafe(argv)) # every caller escapes +``` + +Right: + +```ruby +sources = capture(["docker", "inspect", "--format", fmt, *ids]) + +sig { params(argv: T::Array[String]).returns(String) } +def capture(argv) + # T.unsafe: fixed first param can't match a runtime-sized splat (7019). + out, _err, status = Open3.capture3(*T.unsafe(argv)) +``` + +learned-from: dev#140 review — three threads asked "why T.unsafe / why not +T.let?" about splat escapes; capture(argv) removed all but the boundary one. +date: 2026-09-04