Skip to content

Add the 0002-static-first-invocation package codemod - #1056

Merged
kody-bot merged 3 commits into
mainfrom
cursor/static-first-package-model-8ec0
Jul 30, 2026
Merged

kody-bot merged 3 commits into
mainfrom
cursor/static-first-package-model-8ec0

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Registers the static-first migration in the new package-codemod framework (#1049) as 0002-static-first-invocation — the narrow-phase enforcement vehicle for #1048.

Safety-first design (per Kent's direction: avoid changing semantics)

The only automated transform is the API rename:

  • packages.invokeChecked(...) member calls (including packages?.invokeChecked) → packages.invoke(...) via AST range replacement. Semantics-preserving by construction: since feat(invoke): key-less packages.invoke lean path + widen-phase deprecation shims #1046, the runtime aliases invokeChecked to invoke (runtime-tool-factories.ts returns invokeChecked: invoke) — same call shape, same mandatory contract check, same key-less behavior. The rewrite changes which name appears in source and nothing else; it future-proofs the package for the narrow phase when the alias is removed.

Everything that would change semantics is a conservative needsManual finding (per the codemod doctrine), naming the replacement:

  • packages.check(...) — its contract return value has no mechanical equivalent (invoke checks internally); the call site needs human restructuring.
  • Literal dynamic import("kody:@...") — namespace semantics and kody.dependencies manifest changes need a human; static-import conversion also changes bundling/pinning, realm, and storage binding, so it is never automated.
  • Parse failures on files referencing the deprecated surface.

A post-rewrite verification confirms no detectable invokeChecked member expressions remain. Detection reuses package-runtime/deprecated-invocation-usage.ts (the publish-check collector from #1046), so codemod detect findings and the non-fatal publish lint warnings stay in lockstep by construction. Documented in docs/contributing/package-codemods.md alongside 0001.

Role in the program

Kent's own 10 affected packages were already migrated manually (pre-framework); this codemod's fleet-wide scan is the proof mechanism for the #1048 gate ("legacy surface absent from all production packages") and the migration path for community forks and post-launch users before the shims are removed.

Testing

  • 4 new node tests: mechanical rewrite incl. optional chaining + idempotent rerun; unrelated invokeChecked identifiers and non-code files untouched; packages.check / dynamic-import needsManual findings; mixed mechanical+manual package.
  • Full package-codemods suite green (14 tests), typecheck green, format/lint clean.

Program report

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added a migration codemod that updates deprecated packages.invokeChecked(...) calls to packages.invoke(...), including optional chaining.
    • Detects related patterns that require manual review and provides targeted guidance.
    • Reports changed files and supports safe repeat execution without duplicating changes.
  • Documentation

    • Added authoring guidance describing the codemod’s supported transformations, detection behavior, and manual-review scenarios.

cursoragent and others added 2 commits July 30, 2026 14:29
Registers the narrow-phase migration for the static-first two-rule
model in the new codemod framework: mechanical AST rewrite of
packages.invokeChecked member calls to packages.invoke (identical call
shape; invoke is always contract-checked and key-less calls run the
lean path), with conservative needsManual findings for packages.check
and literal dynamic import("kody:@...") usage. Detection reuses the
deprecated-invocation-usage collector so codemod findings and the
publish-check deprecation warnings stay in lockstep. Its fleet-wide
scan is the phase-3 gate for #1048.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e211ebab-9870-4f7c-b6d5-1a7a9cc26022

📥 Commits

Reviewing files that changed from the base of the PR and between 08db34b and 9b88706.

📒 Files selected for processing (2)
  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.node.test.ts
  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts
📝 Walkthrough

Walkthrough

Adds codemod 0002-static-first-invocation, registers it, documents its behavior, and tests rewrites of packages.invokeChecked alongside manual handling for unsupported invocation patterns.

Changes

Static-first invocation migration

Layer / File(s) Summary
Codemod detection and transformation
packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts
Detects deprecated invocation patterns, rewrites supported invokeChecked member expressions to invoke, and reports needsManual cases.
Registry and authoring guide integration
packages/worker/src/package-codemods/registry.ts, docs/contributing/package-codemods.md
Registers the codemod and documents its rewrite, detection, verification, and manual-migration behavior.
Behavioral test coverage
packages/worker/src/package-codemods/codemods/0002-static-first-invocation.node.test.ts
Covers direct and optional-chained rewrites, idempotency, unrelated identifiers, non-code files, manual findings, and mixed-file transforms.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PackageFiles
  participant CodemodRegistry
  participant StaticFirstInvocationCodemod
  participant ASTParser
  PackageFiles->>CodemodRegistry: select registered codemod
  CodemodRegistry->>StaticFirstInvocationCodemod: detect deprecated usage
  StaticFirstInvocationCodemod->>ASTParser: parse code files
  ASTParser-->>StaticFirstInvocationCodemod: member-expression ranges
  StaticFirstInvocationCodemod->>PackageFiles: rewrite invokeChecked to invoke
  StaticFirstInvocationCodemod-->>PackageFiles: return changed paths and needsManual findings
Loading

Possibly related issues

Possibly related PRs

  • kentcdodds/kody#1046 — Introduces runtime invocation changes whose deprecated surfaces are detected or rewritten by this codemod.
  • kentcdodds/kody#1049 — Introduces the package-codemod registry extended by this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding the 0002-static-first-invocation package codemod.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/static-first-package-model-8ec0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 08db34b. Configure here.

@github-actions

github-actions Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1056.kody-a99.workers.dev

Worker: kody-pr-1056
D1: kody-pr-1056-db
KV: kody-pr-1056-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts (2)

149-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loop is a filter+map.

-	for (const usage of usages) {
-		// `packages.check` return values and dynamic-import namespaces have no
-		// mechanical one-to-one rewrite; report them instead of guessing.
-		if (usage.kind !== 'packages.invokeChecked') {
-			needsManual.push({
-				path: usage.filePath,
-				message: findingMessageForUsage(usage),
-			})
-		}
-	}
+	// `packages.check` return values and dynamic-import namespaces have no
+	// mechanical one-to-one rewrite; report them instead of guessing.
+	for (const usage of usages.filter(
+		(candidate) => candidate.kind !== 'packages.invokeChecked',
+	)) {
+		needsManual.push({
+			path: usage.filePath,
+			message: findingMessageForUsage(usage),
+		})
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts`
around lines 149 - 158, Update the loop over usages to use a filter-and-map
style transformation: retain only usages whose kind is not
packages.invokeChecked, then produce needsManual entries with each usage’s
filePath and findingMessageForUsage(usage). Preserve the existing exclusion and
output fields.

212-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider satisfies PackageCodemod for consistency with codemod 0001.

ambientStorageToPackageStorageCodemod uses satisfies, which retains the literal id type; the annotation here widens it to string.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts`
around lines 212 - 218, Update the staticFirstInvocationCodemod declaration to
use the satisfies operator with PackageCodemod instead of a direct type
annotation, preserving the literal type of staticFirstInvocationCodemodId while
retaining the existing detect and transform implementations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.node.test.ts`:
- Around line 83-124: The test must cover malformed source containing
packages.invokeChecked and assert that detection and transformation report a
parse-failure needsManual finding without changing the file. Update
collectDeprecatedInvocationUsage so parse errors are propagated or represented
as findings instead of being swallowed as no usage, allowing the rewrite flow to
emit the required parse-failure message.

In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts`:
- Around line 160-192: Update the post-rewrite verification loop in
0002-static-first-invocation.ts to iterate over invokeCheckedPaths, while
avoiding duplicate needsManual entries for paths already recorded with a
parse-failure finding; preserve the existing scannable-file, declaration-file,
and remaining-usage checks. In docs/contributing/package-codemods.md lines
140-142, retain the broad verification guarantee now that all candidates are
covered.

---

Nitpick comments:
In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts`:
- Around line 149-158: Update the loop over usages to use a filter-and-map style
transformation: retain only usages whose kind is not packages.invokeChecked,
then produce needsManual entries with each usage’s filePath and
findingMessageForUsage(usage). Preserve the existing exclusion and output
fields.
- Around line 212-218: Update the staticFirstInvocationCodemod declaration to
use the satisfies operator with PackageCodemod instead of a direct type
annotation, preserving the literal type of staticFirstInvocationCodemodId while
retaining the existing detect and transform implementations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47873b17-acee-43c1-85c3-c5d4d74a3891

📥 Commits

Reviewing files that changed from the base of the PR and between d93cc2e and 08db34b.

📒 Files selected for processing (4)
  • docs/contributing/package-codemods.md
  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.node.test.ts
  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts
  • packages/worker/src/package-codemods/registry.ts

Comment on lines +83 to +124
test('0002 reports packages.check and literal dynamic kody imports as needsManual', () => {
const files = {
'package.json': manifest(),
'index.ts': [
"import { packages } from 'kody:runtime'",
'',
'export default async function run() {',
"\tconst contract = await packages.check({ kodyId: 'github', exportName: './request' })",
"\tconst dynamicModule = await import('kody:@kentcdodds/github/request')",
'\treturn { contract, dynamicType: typeof dynamicModule.default }',
'}',
'',
].join('\n'),
}

const findings = staticFirstInvocationCodemod.detect(files)
expect(findings).toEqual([
{
path: 'index.ts',
message: expect.stringContaining('literal dynamic `import("kody:@...")`'),
},
{
path: 'index.ts',
message: expect.stringContaining('`packages.check`'),
},
])

const result = staticFirstInvocationCodemod.transform(files)
expect(result.changed).toBe(false)
expect(result.changedPaths).toEqual([])
expect(result.files).toEqual(files)
expect(result.needsManual).toEqual([
{
path: 'index.ts',
message: expect.stringContaining('literal dynamic `import("kody:@...")`'),
},
{
path: 'index.ts',
message: expect.stringContaining('`packages.check`'),
},
])
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cover—and fix—the required parse-failure manual finding.

The PR requires parse failures to yield needsManual, but this suite omits that case. More importantly, the supplied implementation catches parse errors in collectDeprecatedInvocationUsage() and returns no usage, so malformed files never enter the rewrite loop or receive the parse-failure finding. Add a malformed packages.invokeChecked fixture expecting needsManual; it will expose the required implementation fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.node.test.ts`
around lines 83 - 124, The test must cover malformed source containing
packages.invokeChecked and assert that detection and transformation report a
parse-failure needsManual finding without changing the file. Update
collectDeprecatedInvocationUsage so parse errors are propagated or represented
as findings instead of being swallowed as no usage, allowing the rewrite flow to
emit the required parse-failure message.

Comment on lines +160 to +192
for (const path of [...invokeCheckedPaths].sort((left, right) =>
left.localeCompare(right),
)) {
const source = nextFiles[path]
if (typeof source !== 'string') continue
const rewritten = rewriteInvokeChecked(source)
if (rewritten == null) {
needsManual.push({ path, message: manualParseFailureMessage })
continue
}
if (rewritten !== source) {
nextFiles[path] = rewritten
changedPaths.push(path)
}
}

// Defensive verification: the rewrite must fully clear detectable
// `packages.invokeChecked` member expressions, or the file needs a human.
for (const path of changedPaths) {
const source = nextFiles[path]
if (!scannableModuleFilePattern.test(path)) continue
if (isTypeDeclarationFilePath(path)) continue
const remaining = collectDeprecatedInvocationUsage({
[path]: source ?? '',
}).filter((usage) => usage.kind === 'packages.invokeChecked')
if (remaining.length > 0) {
needsManual.push({
path,
message:
'`packages.invokeChecked` member expressions remain after the rewrite; migrate manually.',
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Post-rewrite verification does not cover every invokeChecked candidate, so the documented guarantee overstates behavior. Verification iterates changedPaths only; a candidate file whose AST scan yields no ranges is neither rewritten nor flagged, leaving deprecated usage unreported.

  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts#L160-L192: run the verification loop over invokeCheckedPaths rather than changedPaths, de-duplicating against the parse-failure finding already pushed for that path.
  • docs/contributing/package-codemods.md#L140-L142: once the loop covers all candidates the wording holds; otherwise narrow it to say verification applies only to files the codemod actually rewrote.
📍 Affects 2 files
  • packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts#L160-L192 (this comment)
  • docs/contributing/package-codemods.md#L140-L142
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/worker/src/package-codemods/codemods/0002-static-first-invocation.ts`
around lines 160 - 192, Update the post-rewrite verification loop in
0002-static-first-invocation.ts to iterate over invokeCheckedPaths, while
avoiding duplicate needsManual entries for paths already recorded with a
parse-failure finding; preserve the existing scannable-file, declaration-file,
and remaining-usage checks. In docs/contributing/package-codemods.md lines
140-142, retain the broad verification guarantee now that all candidates are
covered.

…ite candidates

collectDeprecatedInvocationUsage returns nothing for files it cannot
parse, so unparseable files referencing the deprecated surface silently
scanned clean. A textual pre-scan now surfaces them as needsManual in
both detect and transform, and the post-rewrite verification covers
every invokeChecked candidate rather than only changed files.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants