Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cursor/rules/learnings-index.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions .cursor/skills/learnings/pre-bundle-stdlib-chain/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions .cursor/skills/learnings/sorbet-splat-unsafe/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Loading