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
9 changes: 9 additions & 0 deletions .cursor/rules/learnings-index.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@ 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-only] The deps bootstrap chain
(dependencies.rb → dev/deps closure, ensure_bundler) loads before the
bundle exists: stdlib-only, so no sorbet-runtime sigs — it caps below
`typed: strict`. → .cursor/skills/learnings/pre-bundle-stdlib-only/

## toolchain

- [toolchain/sig-retrofit-runtime-semantics] Adding sigs to existing
methods changes runtime behavior (void sentinel, param validation,
private-receiver T.unsafe(self) calls raising), not just static checks.
→ .cursor/skills/learnings/sig-retrofit-runtime-semantics/


## org tier

Expand Down
38 changes: 38 additions & 0 deletions .cursor/skills/learnings/pre-bundle-stdlib-only/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: pre-bundle-stdlib-only
description: >-
MUST be used when adding requires, Sorbet sigs, or new files to the deps
bootstrap chain (dependencies.rb, lib/dev/deps.rb and its require_relative
closure, lib/ensure_bundler.rb), or when raising a lib/ file's typed sigil.
---

# The pre-bundle bootstrap chain is stdlib-only — no sigs, no T.*

bin/setup.rb and bin/test.rb load `dependencies.rb` (→ `dev/deps` → config,
dsl, dependency_declaration, tap, cli_ui, lockfile, dependency, fetcher,
dependency_installer) and `ensure_bundler` BEFORE the bundle exists, so on a
fresh machine no gem — including sorbet-runtime — is loadable there. These
files cap at `typed: true`/`typed: false` (see the Sorbet/StrictSigil
exclusion list in .rubocop.yml): a `sig`, `extend T::Sig`, or `T.unsafe`
anywhere in the chain crashes first-time setup with a NameError on `T`.

Wrong:

```ruby
# lib/dev/deps/config.rb — pre-bundle file
require "sorbet-runtime" # not installed yet on a fresh machine
extend T::Sig
```

Right:

```ruby
# lib/dev/deps/config.rb stays sig-free at `# typed: true`;
# files loaded only through the dev runtime (src/dev.rb requires
# sorbet-runtime first) may go `# typed: strict` with sigs.
```

learned-from: the typed-sigil enforcement pass (dev#139) — bringing lib/ to
`typed: strict` stopped at the bootstrap chain, whose stdlib-only comment in
dependencies.rb had not spelled out the Sorbet consequence.
date: 2026-08-28
38 changes: 38 additions & 0 deletions .cursor/skills/learnings/sig-retrofit-runtime-semantics/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: sig-retrofit-runtime-semantics
description: >-
MUST be used when adding Sorbet sigs to existing Ruby methods: sig-wrapped
methods behave differently at runtime (void sentinel, validated params,
private-call receivers), so retrofitting is not behavior-neutral.
---

# Retrofitting sigs changes runtime behavior, not just static checking

sorbet-runtime wraps every sig'd method, so adding sigs to working code can
break it three ways: `.void` replaces the method's return value with a VOID
sentinel (callers or tests asserting `.nil?` fail); params are validated on
every call (duck-typed test fakes and StringIO-for-IO injections raise
TypeError — type such seams `T.untyped` / `T.any(IO, StringIO)`); and
`T.unsafe(self).some_method(...)` is an explicit-receiver call, which raises
NoMethodError when the target (e.g. `Kernel#system`) is private. For
runtime-sized splats keep the call receiverless and unsafe-cast the argv.

Wrong:

```ruby
success = T.unsafe(self).system(env, *build_command(spec), chdir: dir)
# => NoMethodError: private method 'system' called for an instance of …
```

Right:

```ruby
argv = [env, *build_command(spec)]
success = system(*T.unsafe(argv), chdir: dir)
```

learned-from: the typed-sigil enforcement pass (dev#139) — four test
failures after lib/ went strict: two private-receiver T.unsafe(self) calls,
a `.nil?` assertion on a newly-void install_all, and a String passed where
the sig'd base contract validates a Hash.
date: 2026-08-28
Loading