jsx_rosetta 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -11
  3. data/CLAUDE.md +70 -0
  4. data/README.md +50 -0
  5. data/agents/jsx-rosetta-resolve-todo-file.md +90 -0
  6. data/lib/jsx_rosetta/ast/inflector.rb +17 -0
  7. data/lib/jsx_rosetta/backend/phlex.rb +1078 -77
  8. data/lib/jsx_rosetta/backend/rails_view.rb +1 -1
  9. data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +73 -20
  10. data/lib/jsx_rosetta/backend/view_component.rb +48 -2
  11. data/lib/jsx_rosetta/cli.rb +175 -37
  12. data/lib/jsx_rosetta/icons/lucide.json +37 -0
  13. data/lib/jsx_rosetta/icons.rb +44 -0
  14. data/lib/jsx_rosetta/ir/lowering.rb +720 -31
  15. data/lib/jsx_rosetta/ir/radix_registry.rb +84 -0
  16. data/lib/jsx_rosetta/ir/types.rb +187 -3
  17. data/lib/jsx_rosetta/ir.rb +5 -4
  18. data/lib/jsx_rosetta/pages_routing.rb +640 -0
  19. data/lib/jsx_rosetta/version.rb +1 -1
  20. data/lib/jsx_rosetta.rb +8 -6
  21. data/plans/nextjs_pages_to_rails.md +200 -0
  22. data/plans/nextjs_pages_to_rails_slice_2.md +118 -0
  23. data/plans/nextjs_pages_to_rails_slice_3.md +121 -0
  24. data/plans/nextjs_pages_to_rails_slice_4.md +301 -0
  25. data/plans/translator_widening_and_pages_followups.md +120 -0
  26. data/plans/translator_widening_slice_a.md +208 -0
  27. data/skills/jsx-rosetta-resolve-todos/SKILL.md +206 -0
  28. data/skills/jsx-rosetta-resolve-todos/data/design_tokens.template.yml +71 -0
  29. data/skills/jsx-rosetta-resolve-todos/data/target_app_conventions.template.yml +107 -0
  30. data/skills/jsx-rosetta-resolve-todos/examples/design_tokens.ant_design_v5.yml +190 -0
  31. data/skills/jsx-rosetta-resolve-todos/recipes/01_design_tokens.md +74 -0
  32. data/skills/jsx-rosetta-resolve-todos/recipes/02_promoted_ivar.md +49 -0
  33. data/skills/jsx-rosetta-resolve-todos/recipes/03_react_hooks.md +34 -0
  34. data/skills/jsx-rosetta-resolve-todos/recipes/04_apollo_hooks.md +34 -0
  35. data/skills/jsx-rosetta-resolve-todos/recipes/05_event_handlers.md +45 -0
  36. data/skills/jsx-rosetta-resolve-todos/recipes/06_module_constants.md +29 -0
  37. data/skills/jsx-rosetta-resolve-todos/recipes/07_nextjs_navigation.md +44 -0
  38. data/skills/jsx-rosetta-resolve-todos/recipes/08_generic_js_bailouts.md +55 -0
  39. data/skills/jsx-rosetta-resolve-todos/tools/apply_promoted_ivar.rb +189 -0
  40. data/skills/jsx-rosetta-resolve-todos/tools/apply_substitutions.rb +292 -0
  41. data/skills/jsx-rosetta-resolve-todos/tools/diff_corpus.rb +161 -0
  42. data/skills/jsx-rosetta-resolve-todos/tools/discover_bailouts.rb +211 -0
  43. metadata +29 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 229956b183e3760f9555874fef214d7ce66d549a603e9d431f5c28251db25bb4
4
- data.tar.gz: 226c173f5e1010d154801740a2924bbc776ff8eede26587ec1d9d796da51c5f1
3
+ metadata.gz: 161aa59c1e226755b166b5c7366d0c2b5bc1b3d1f55762b1c8c37f551392179a
4
+ data.tar.gz: 826dd0aff4042b710b354b79c0aa912587477c0f7b07892541fa0e1c7bed090b
5
5
  SHA512:
6
- metadata.gz: 4dbf8ac15094f2462a38c65dfbf539163cfb471077e4bd8874893eda3262da031df72da161793f4c65d75597d55c1d7ea09b395f991fb7fcf80b074d6a74e7ca
7
- data.tar.gz: 94627c0471abeab22e92727047ec9e870439297bc6df6447da1bfb79914415d14abc665d3b2325d9496615e62c3f0099898db11a6c05dfd37acf739aa0af518c
6
+ metadata.gz: ea022328a7f0cc56e87b08064a2aa4cb602254caf66c5f0f511faa295f8823867ba1fcfece9952d8d1fb064993736af21df8d060ef3b20ddd0ff8788d8f65d8c
7
+ data.tar.gz: fa4085f3972ec65ba2062bbed47f936fb0519d37220d6631fd8c427d03edfb2e20ecf3961fc9dd650f6b27d82db43bd2196a3d9e7b3ab44284d06a07b608efac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,129 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Added — translate cva() variant builders into Ruby constants
6
+
7
+ - **`const fooVariants = cva(base, { variants, defaultVariants })`** —
8
+ the dominant variant-builder pattern in shadcn/ui-shaped components —
9
+ used to land as a 40-line TODO comment block above the class, and
10
+ its use-site `cn(fooVariants({ variant }), className)` emitted the
11
+ verbatim JS as a literal string into the `class:` attribute. Both
12
+ paths are now translated end-to-end.
13
+ - Lowering recognizes the `cva(<base>, { variants, defaultVariants,
14
+ compoundVariants })` call shape and records it as a new
15
+ `IR::CvaBinding` node (parallel to `IR::LocalBinding`).
16
+ - Phlex backend renders each `CvaBinding` as three module-level
17
+ Ruby constants alongside the class — `FOO_BASE_CLASS`,
18
+ `FOO_VARIANT_CLASSES` (a frozen hash of axis → option → class
19
+ string), and `FOO_DEFAULT_VARIANTS`.
20
+ - The use-site call `cn(fooVariants({ variant, size }), className)`
21
+ in a `className={...}` attribute now emits a Ruby string
22
+ interpolation against those constants:
23
+ `"#{FOO_BASE_CLASS} #{FOO_VARIANT_CLASSES["variant"][@variant]}
24
+ #{FOO_VARIANT_CLASSES["size"][@size]} #{@class_name}"`.
25
+ - `render_initializer` now uses `defaultVariants` as the Ruby kwarg
26
+ default for any prop name matching a cva axis (so `variant:
27
+ "default"` flows from the cva binding even though the React
28
+ function signature took the prop undefaulted).
29
+ - `compoundVariants` (the rule-of-rules cva feature) is not yet
30
+ translated — the verbatim JS source surfaces as a `# TODO:
31
+ compoundVariants from FOO aren't translated` comment alongside the
32
+ emitted constants so the reviewer can hand-port the rules.
33
+ - Other module-level constants (non-cva) still take the existing
34
+ "# TODO: module-level constants" pre-class comment path.
35
+
36
+ ### Added — drop Slot.Root branch from polymorphic asChild tags
37
+
38
+ - **shadcn's `<Comp asChild>` no longer NameErrors at render.** Components
39
+ using `const Comp = asChild ? Slot : "div"` (or `Slot.Root`) routed
40
+ the truthy branch through Radix's `Slot`, which has no Ruby class on
41
+ the Phlex side. Lowering now detects this Slot-vs-tag conditional
42
+ (Slot rooted at a `radix-ui`/`@radix-ui/react-*` import) and emits
43
+ only the non-Slot branch — no conditional, no `as_child` kwarg,
44
+ just the underlying tag.
45
+ - New CLI flag `--keep-slot` and `JsxRosetta.translate(..., keep_slot:
46
+ true)` preserves the full polymorphic conditional for consumers
47
+ that shim `Components::Slot::Root` themselves.
48
+ - Detection respects the import source: a project-local `import
49
+ { Slot } from "./my-slot"` is untouched.
50
+
51
+ ### Added — map known Radix primitive tags to underlying HTML elements
52
+
53
+ - **`<SeparatorPrimitive.Root />`, `<LabelPrimitive.Root />`, etc.**
54
+ used to lower as `ComponentInvocation`s, producing
55
+ `render SeparatorPrimitive::Root.new(...)` — undefined constants,
56
+ NameError on render. Now: when the import source matches a Radix
57
+ package (`radix-ui`, `@radix-ui/react-*`) and the
58
+ `(LocalName, Member)` pair is in a small registry, lower as an
59
+ HTML Element with the underlying tag and any always-applied
60
+ attributes (`role`, `type`, etc.). Consumer attrs win on collision
61
+ with the registry defaults.
62
+ - Registry lives at `lib/jsx_rosetta/ir/radix_registry.rb`. Covers
63
+ Separator / Label / Avatar (Root/Image/Fallback) / Switch
64
+ (Root/Thumb) / Progress (Root/Indicator) / AspectRatio (Root) /
65
+ ScrollArea (Root/Viewport). Unknown primitives fall through to
66
+ the existing `ComponentInvocation` / TODO behavior.
67
+ - Works with both named-aliased imports
68
+ (`import { Separator as SeparatorPrimitive } from "radix-ui"`)
69
+ and namespace imports
70
+ (`import * as AvatarPrimitive from "@radix-ui/react-avatar"`).
71
+
72
+ ### Added — auto-emit Lucide icon shims as a translation sidecar
73
+
74
+ - **`import { ChevronRight } from "lucide-react"` now lands a working
75
+ `ChevronRight` Phlex class.** Previously the translator carried the
76
+ React import through verbatim, so the generated component contained
77
+ `render ChevronRight.new(...)` referencing a non-existent Ruby class
78
+ — NameError at render time. The Phlex backend now detects Lucide
79
+ imports (`lucide-react`, `lucide`) that are actually used as JSX
80
+ component tags and emits two kinds of sidecar files:
81
+ - `lucide_icon.rb` — a shared `LucideIcon < Phlex::HTML` base.
82
+ - `<icon>.rb` — one file per referenced icon, defining a subclass
83
+ that renders the vendored SVG path data inline. One file per
84
+ icon means Zeitwerk autoloads each cleanly under the consumer's
85
+ `app/components/` (or wherever the output directory points).
86
+ - Vendored path data lives at `lib/jsx_rosetta/icons/lucide.json`
87
+ (the ~35 icons most commonly imported by shadcn/ui sources). Icons
88
+ not in the vendored set emit a class whose `inner_svg` is empty
89
+ with a TODO comment pointing at the lucide.json refresh path.
90
+ - Tolerates both canonical (`ChevronRight`) and legacy `*Icon`
91
+ (`ChevronRightIcon`) names — same path data either way.
92
+ - `--phlex-namespace=Components` wraps every sidecar in the same
93
+ module as the main translation, so the consumer's autoloader
94
+ config doesn't need a special case.
95
+
96
+ ### Added — translate Stimulus controller bodies when safe
97
+
98
+ - **Paste JSX handler bodies into the generated `_controller.js`.**
99
+ Auto-generated Stimulus controllers used to leave the handler body
100
+ as a TODO comment with the verbatim source above an empty
101
+ `clickHandler(event) { // ... }` stub. For DOM-driven handlers (the
102
+ common shape in shadcn-style UI), the JSX body is *already valid JS*
103
+ — we now paste it directly into the method, using the original
104
+ arrow's parameter name so identifier references in the body still
105
+ resolve.
106
+ - `IR::StimulusMethod` gains a `params:` field — the original arrow
107
+ parameter names — used as the Stimulus method's parameter signature.
108
+ - New `safe_to_paste_handler?` heuristic on the Phlex backend bails
109
+ out (falls back to the previous TODO behavior) when the body
110
+ references React state setters (`setX(`), React hooks (`useX(`),
111
+ or is just an identifier-bound `// originally bound to: …` comment.
112
+ - Collision markers are preserved across the new path.
113
+
114
+ ### Fixed — silent children loss on self-closing-with-spread tags
115
+
116
+ - **Auto-yield on blockless spread-children tags.** The shadcn idiom
117
+ `<tag {...props} />` (self-closing JSX whose rest-spread carries React
118
+ `children`) translated to a Phlex `tag(..., **(@props || {}))` call
119
+ with no block — so callers' `Component.new { ... }` blocks were
120
+ silently dropped at render. Now: when an Element or
121
+ ComponentInvocation has no explicit IR children, a `SpreadAttribute`,
122
+ and a non-void tag, emit `tag(...) do; yield if block_given?; end`.
123
+ The `block_given?` guard preserves the no-block use case. Void HTML
124
+ elements (input, img, br, …) still emit blockless. Explicit-children
125
+ paths and `RenderProp` flows are untouched.
126
+
3
127
  ## [0.5.1] - 2026-05-11
4
128
 
5
129
  A correctness pass on the v0.5.0 Phlex output. A random-sample review
@@ -308,9 +432,6 @@ generated Ruby parsed but didn't behave like the source JSX.
308
432
 
309
433
  ### Stress test outcome
310
434
 
311
- - 929-file Phlex stress run on `reserv-web`: 887/929 clean translations
312
- (unchanged — rejection logic untouched), **0/1224 syntax failures**
313
- (down from 25 on v0.3.0). All emitted `.rb` files now pass `ruby -c`.
314
435
  - Five residual bugs were caught during the v0.4.0 stress rerun and
315
436
  fixed inline:
316
437
  - Prop default expressions that translated to `nil # TODO: ...` inside
@@ -336,10 +457,8 @@ generated Ruby parsed but didn't behave like the source JSX.
336
457
 
337
458
  ## [0.3.0] - 2026-05-10
338
459
 
339
- Driven by a 929-file stress run against the entire `reserv-web` codebase
340
- (`reserv-web/src/` + `reserv-web/pages/` + `packages/`). Baseline outcome
341
- on v0.2.0: 838/929 (90.2%) clean exit, 91 hard failures across 5 distinct
342
- error categories. This release ships fixes for all five plus a follow-up
460
+ Baseline outcome on v0.2.0: 838/929 (90.2%) clean exit, 91 hard failures across
461
+ 5 distinct error categories. This release ships fixes for all five plus a follow-up
343
462
  that opens up lowercase JSX-returning helpers as components, lifting the
344
463
  corpus to **887/929 (95.5%) clean exit**. The 42 remaining failures are
345
464
  non-component modules (utility/hook libraries, AG-Grid column
@@ -421,10 +540,8 @@ initializers); each now reports a classifier-tagged error that explains
421
540
 
422
541
  ## [0.2.0] - 2026-05-10
423
542
 
424
- Driven by an empirical probe of v0.1.0 against a 39-file Next.js production
425
- slice (`reserv-web/src/components/rolloverbook`). The slice exposed three
426
- return-shape gaps and a crash on nested destructure; this release fixes all
427
- four. Probe outcome: 33/39 → **39/39 emit**.
543
+ The slice exposed three return-shape gaps and a crash on nested destructure;
544
+ this release fixes all four. Probe outcome: 33/39 → **39/39 emit**.
428
545
 
429
546
  ### Fixed
430
547
 
data/CLAUDE.md ADDED
@@ -0,0 +1,70 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ The README has the user-facing pipeline overview, backend trade-offs, and CLI surface. This file covers the things that aren't obvious from the README or a cold read of the source.
6
+
7
+ ## Common commands
8
+
9
+ ```bash
10
+ bin/setup # bundle install + npm install in node/ (required after fresh clone)
11
+ bundle exec rspec # full test suite
12
+ bundle exec rspec spec/backend/phlex_spec.rb # single file
13
+ bundle exec rspec spec/backend/phlex_spec.rb:123 # single example by line
14
+ bundle exec rspec -e "guard-ladder" # examples whose describe/it matches
15
+ bundle exec rubocop
16
+ bundle exec rake # default: rspec + rubocop
17
+ bundle exec exe/jsx_rosetta translate path/X.tsx --as=phlex --phlex-suffix=Component -o /tmp
18
+ ```
19
+
20
+ `bin/setup` is the first-run requirement — the Node sidecar's `node_modules` isn't bundled, and `JsxRosetta.parse` shells out to `node/parse.js`. If `rspec` fails with parser errors, re-run `bin/setup`. `JSX_ROSETTA_NODE` overrides the `node` binary path.
21
+
22
+ `tmp/run_phlex_stress.sh` translates a large external JSX/TSX corpus through the Phlex backend; results land in `tmp/stress/` (gitignored). Use after non-trivial changes to confirm clean-translation count and `ruby -c` pass count don't regress. The TSV at `tmp/stress/phlex_results.tsv` is the headline.
23
+
24
+ ## Pipeline
25
+
26
+ JSX → Babel AST → IR → backend. See README for the user-facing diagram. Hot files:
27
+
28
+ - `lib/jsx_rosetta/ir/lowering.rb` (1700+ lines, single class) — the source of truth for AST→IR. New translation behaviors land here.
29
+ - `lib/jsx_rosetta/ir/types.rb` — `Data.define`'d value classes. `IR::Component` is the root.
30
+ - `lib/jsx_rosetta/backend/view_component/expression_translator.rb` — shared by every backend for JS-expression fragments inside JSX bodies/attributes. Despite the directory name, the Phlex backend uses it too.
31
+ - `lib/jsx_rosetta/backend/phlex.rb` — the most actively maintained backend; most recent improvements target it first.
32
+
33
+ When in doubt, lowering preserves verbatim JS as `IR::Interpolation` so the backend's `ExpressionTranslator` can take a second pass at it.
34
+
35
+ ## ExpressionTranslator identifier resolution (load-bearing)
36
+
37
+ An identifier reference inside JSX is classified into one of five buckets in order. This is the central machinery for closing render-time NameErrors — most recent fixes work by tightening which names land in bucket 4.
38
+
39
+ 1. **Local scope** (pushed via `with_locals` — loop bindings, render-prop params, render-method params) → bare snake_case.
40
+ 2. **Prop alias** (`"data-testid": dataTestId` in destructure) → `@ivar` of the underlying prop name.
41
+ 3. **Prop name** → `@snake_case_ivar`.
42
+ 4. **`local_binding_names` ∪ `imported_names`** (hook tuple destructures, top-level `const`/`function`/`import` declarations) → `nil` at leaf position; **bail** (return nil from the translate call) at member-chain root / unary operand / binary operand. The bail routes the caller into a TODO emission instead of a bare snake_case ref that NameErrors at render.
43
+ 5. **Fallthrough** → bare snake_case identifier, recorded as `unresolved`. Backends drop with TODO on uppercase (PascalCase / SCREAMING) since those are almost always imports; lowercase passes through as a Rails-helper-shaped reference (`current_user` etc).
44
+
45
+ When adding a "we know X exists but can't translate it" class, plumb the names into the translator via `imported_names:` or `local_binding_names:` so bucket 4 fires.
46
+
47
+ ## Project conventions
48
+
49
+ - **Preserve-as-TODO over speculative translation.** Flag `# TODO:` with verbatim source rather than guess. Translator bailout + backend drop-with-TODO helpers exist to keep this safe.
50
+ - **No JS-to-Ruby translation paths that best-guess arbitrary call expressions.** Extending the `ExpressionTranslator` for unambiguous mappings (`===`→`==`, `??`→`||`, etc.) is fine; guessing call semantics is not.
51
+ - **No external-corpus references in committed artifacts.** Specs, fixtures, source comments, commit messages, CHANGELOG, README must stay generic ("the stress corpus" / "a real-world JSX corpus"). Anything under `tmp/` is gitignored and can reference whatever.
52
+
53
+ ## Emission rules that are subtle
54
+
55
+ - **Phlex attribute casing.** HTML-element attrs (lowercase tag) preserve camelCase verbatim — Phlex 2 only converts `_` to `-`, so SVG attrs like `preserveAspectRatio` and `viewBox` must stay camelCase. Component invocations (PascalCase tag) full-snake_case all keys per Ruby kwarg convention. See `plain_attribute_part` in `backend/phlex.rb`.
56
+ - **Dropped attributes are omitted, not nilled.** When a value bails to `nil` with a recorded TODO, the entire kwarg drops from the emission (the TODO above the element is the record). Explicit `attr={null}` in source still emits `attr: nil` (translation succeeded, no TODO → not a drop). Mechanism: `plain_attribute_part` watches whether a TODO was appended during value computation.
57
+ - **Sibling components share `module_bindings` by reference.** `lower_all` attaches the *same* array to every sibling, so backends emit the constants TODO prefix only on the first sibling via `first_emit_for_module_bindings?` (object-identity check on the array).
58
+ - **JSX in non-child positions** lowers through `lower_value_expression` → `lower_jsx_value`. Single-child Fragments unwrap (common React workaround for "single ReactNode required"). Hit by attribute values (`icon={<Foo/>}`) and render lambdas in column-config arrays.
59
+ - **Inline arrow handlers split by tag case.** On PascalCase tags, lower to `IR::EventHandler` (verbatim JS body) → backend extracts a stub instance method (`handle_click`, `handle_change`) and emits `on_click: method(:handle_click)`. On HTML elements, lower to Stimulus method extraction instead (see `stimulus_methods` on `IR::Component`).
60
+
61
+ ## Spec gotchas
62
+
63
+ - `spec/backend/phlex_spec.rb` is the canonical large suite (~1000 lines now) — most behavior changes get their regression cover here.
64
+ - The Phlex spec helper's `files_for` uses **no suffix by default** — paths are `x.rb` (not `x_component.rb`) unless the spec passes `suffix: "Component"`.
65
+ - `spec/fixtures/jsx/*.{jsx,tsx}` + `spec/fixtures/expected/*` feed the Button full-IR-equality test; touch carefully.
66
+ - `spec/.rspec_status` is gitignored and huge — don't read it.
67
+
68
+ ## Commit message style
69
+
70
+ Recent commits use a focused subject + structured body (subject explains *what changed*, body has sections like "Bug fixes:", "Implementation:", "Stress corpus impact:"). When behavior changes shift the stress numbers (clean translations / syntax fails / top dropped-attribute categories), include before/after counts in the body — they're the most useful signal for whether the change earns its complexity.
data/README.md CHANGED
@@ -168,6 +168,56 @@ auto-perform. Common cases:
168
168
  - **Reserved Rails controller names** (in the routes script) → `# WARNING:`
169
169
  line listing collisions.
170
170
 
171
+ ## Resolving TODOs (optional Claude Code skill)
172
+
173
+ The repository ships an optional [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) skill and worker agent that automate the last-mile work of clearing `# TODO:` comments from the generated output. The skill is fully generic — it ships no app-specific assumptions, no design-system data, and no host-stack conventions. You bring those.
174
+
175
+ **What it does:**
176
+
177
+ - A pure-Ruby corpus scanner (`tools/discover_bailouts.rb`) tallies the dropped-expression patterns in your generated output and surfaces likely design-system / config-namespace clusters.
178
+ - A pure-Ruby substitution pass (`tools/apply_substitutions.rb`) reads a YAML you supply (`match:` regex + `tokens:` value table) and splices values into the generated `render Foo.new(...)` calls. Always validates with `ruby -c` before writing; reverts on failure.
179
+ - LLM-driven recipes for React hooks, Apollo data fetching, event handlers, Next.js navigation, and module-level constants. These default to **sharpening** verbose TODOs into single-decision items rather than guessing translations.
180
+ - A `jsx-rosetta-resolve-todo-file` worker agent for parallel fan-out across a corpus.
181
+
182
+ A reference Ant Design v5 token mapping ships under `examples/` — drop-in usable if your source app uses Ant Design with default theming.
183
+
184
+ **Layout in this repo:**
185
+
186
+ ```
187
+ skills/jsx-rosetta-resolve-todos/
188
+ ├── SKILL.md # workflow + routing table
189
+ ├── recipes/ # per-TODO-class recipes
190
+ ├── tools/ # discover_bailouts.rb, apply_substitutions.rb
191
+ ├── data/design_tokens.template.yml # blank schema for your design system
192
+ └── examples/design_tokens.ant_design_v5.yml # reference for Ant-using apps
193
+
194
+ agents/
195
+ └── jsx-rosetta-resolve-todo-file.md # fan-out worker definition
196
+ ```
197
+
198
+ **Installing into your environment:**
199
+
200
+ User-level (available in every Claude Code session on your machine):
201
+
202
+ ```bash
203
+ mkdir -p ~/.claude/skills ~/.claude/agents
204
+ ln -s "$(pwd)/skills/jsx-rosetta-resolve-todos" ~/.claude/skills/
205
+ ln -s "$(pwd)/agents/jsx-rosetta-resolve-todo-file.md" ~/.claude/agents/
206
+ ```
207
+
208
+ Project-level (only available in the consuming Rails app):
209
+
210
+ ```bash
211
+ # from your Rails app's repo root
212
+ mkdir -p .claude/skills .claude/agents
213
+ cp -R /path/to/jsx_rosetta/skills/jsx-rosetta-resolve-todos .claude/skills/
214
+ cp /path/to/jsx_rosetta/agents/jsx-rosetta-resolve-todo-file.md .claude/agents/
215
+ ```
216
+
217
+ In either case, your app-specific configuration (the populated `data/design_tokens.yml`, `data/target_app_conventions.md`, etc.) belongs **inside the consuming app's** `.claude/skills/jsx-rosetta-resolve-todos/data/` — not here. Anything app-specific should be `.gitignore`d in your Rails repo if it shouldn't be shared.
218
+
219
+ The scripts under `tools/` are runnable directly with `ruby` — Claude Code is not required to use them. The recipes and the agent are what require Claude Code.
220
+
171
221
  ## What's deferred
172
222
 
173
223
  - Translating React state primitives (`useState` / `useEffect` etc.). The
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: jsx-rosetta-resolve-todo-file
3
+ description: Process a single jsx_rosetta-generated Phlex/ViewComponent file and resolve its `# TODO:` comments by applying the recipes from the jsx-rosetta-resolve-todos skill. Designed for fan-out — one worker per file. Returns a JSON report with category counts.
4
+ tools: Read, Edit, Bash, Grep, Glob
5
+ ---
6
+
7
+ You are a fan-out worker for the **jsx-rosetta-resolve-todos** skill. You receive one file path at a time and apply the recipes to its TODO comments.
8
+
9
+ ## Inputs
10
+
11
+ The user message will name a single `.rb` file path. You may also be given:
12
+
13
+ - A path to a `data/design_tokens.yml` (for recipe 01)
14
+ - A path to a `data/target_app_conventions.yml` (for recipes 03–07)
15
+ - The host Rails app's root path (so you can grep for available helpers, controllers, Stimulus controllers)
16
+
17
+ ## Hard rules
18
+
19
+ 1. **Never speculate on JS-to-Ruby translation.** When unsure, sharpen the TODO; do not guess. This rule overrides any apparent "obvious" translation that isn't on a recipe's whitelist.
20
+ 2. **Never ship a file that doesn't pass `ruby -c`.** Run it before any edits (to confirm baseline) and after every meaningful edit. If parsing breaks, revert and report `parse_failed`.
21
+ 3. **One file per invocation.** Do not chain to other files; the dispatcher manages fan-out.
22
+ 4. **No business-logic changes.** You apply mechanical transformations and emit sharpened TODOs. Anything that would alter observable behavior beyond the TODO's stated intent gets escalated.
23
+
24
+ ## Workflow
25
+
26
+ ```
27
+ 1. Read the target file. Run `ruby -c <file>` — abort if it doesn't parse to begin with.
28
+ 2. Read SKILL.md and the relevant recipe files (recipes/*.md).
29
+ 3. If a design_tokens.yml is provided, run `ruby tools/apply_substitutions.rb
30
+ --config <yaml> <file>` first. This handles recipe 01 mechanically.
31
+ 4. Walk remaining TODOs top-to-bottom. For each:
32
+ a. Classify against the routing table in SKILL.md.
33
+ b. Look up the recipe.
34
+ c. Take exactly one action: resolve, sharpen, or escalate.
35
+ 5. Re-run `ruby -c`. If it fails, revert all your edits and report `parse_failed`.
36
+ 6. Emit ONE JSON line as your final output (see format below).
37
+ ```
38
+
39
+ ## Output format
40
+
41
+ Emit exactly one JSON line as your last message. Schema:
42
+
43
+ ```json
44
+ {
45
+ "file": "<absolute path>",
46
+ "parsed_before": true,
47
+ "parsed_after": true,
48
+ "totals": {"resolved": <int>, "sharpened": <int>, "escalated": <int>, "skipped": <int>},
49
+ "by_category": {
50
+ "design_tokens": {"resolved": 3, "skipped": 1},
51
+ "react_hooks": {"sharpened": 2},
52
+ "apollo_hooks": {"sharpened": 1},
53
+ "event_handler": {"sharpened": 4, "escalated": 1},
54
+ "promoted_ivar": {"resolved": 5}
55
+ },
56
+ "notes": ["<optional one-liners about anything unusual>"]
57
+ }
58
+ ```
59
+
60
+ If the file fails post-edit parse and is reverted:
61
+
62
+ ```json
63
+ {"file": "<path>", "parsed_before": true, "parsed_after": false, "parse_failed": true, "totals": {"resolved": 0, "sharpened": 0, "escalated": 0, "skipped": 0}}
64
+ ```
65
+
66
+ ## Tool use
67
+
68
+ - **Read** — the target file, recipes, and the host app's relevant directories (app/controllers, app/helpers, app/javascript/controllers).
69
+ - **Grep** — finding existing helpers/controllers/Stimulus targets in the host app to inform sharpened TODOs.
70
+ - **Edit** — applying transformations to the target file. Prefer surgical edits over wholesale rewrites.
71
+ - **Bash** — restricted to `ruby -c <file>` for syntax validation and to running the skill's own scripts (`tools/apply_substitutions.rb`). Do NOT run other commands.
72
+ - **Glob** — locating skill files and recipe paths.
73
+
74
+ ## Anti-patterns
75
+
76
+ - **Don't** read or edit any file other than the assigned target (and the skill's recipes/data, read-only). The dispatcher relies on file isolation.
77
+ - **Don't** invoke other agents.
78
+ - **Don't** echo recipe content back in your response — your output is a JSON line, nothing else (the dispatcher parses it).
79
+ - **Don't** add commentary to the file you're editing other than the sharpened TODOs themselves. The skill's emission rules cover what comments are allowed.
80
+ - **Don't** "improve" the surrounding code while you're there. Scope is TODO resolution only.
81
+
82
+ ## Escalation
83
+
84
+ When a TODO genuinely has no good answer from the recipes:
85
+
86
+ 1. Leave the TODO untouched in the file.
87
+ 2. Increment the `escalated` counter in your category breakdown.
88
+ 3. Optionally add a one-liner to `notes` explaining what's odd about it.
89
+
90
+ The dispatcher aggregates escalations into a corpus-level review queue for a human.
@@ -20,6 +20,23 @@ module JsxRosetta
20
20
  parts[0] + parts[1..].map(&:capitalize).join
21
21
  end
22
22
 
23
+ def upper_camelize(string)
24
+ string.split("_").map(&:capitalize).join
25
+ end
26
+
27
+ # Best-effort English singularization for plural controller names.
28
+ # Covers the common shapes (`ies`/`y`, `ses`/`s`, `xes`/`x`,
29
+ # `ches`/`ch`, `shes`/`sh`, trailing `s`). Irregular plurals
30
+ # (`people`, `children`, `mice`, `geese`) pass through unchanged —
31
+ # users who hit those rename the generated `as:` in routes.rb.
32
+ def singularize(string)
33
+ case string
34
+ when /(.+[^aeiou])ies\z/i then "#{Regexp.last_match(1)}y"
35
+ when /(.+(?:ss|sh|ch|x|z))es\z/i, /(.+)s\z/i then Regexp.last_match(1)
36
+ else string
37
+ end
38
+ end
39
+
23
40
  # Emit a Ruby string literal in the rubocop-default single-quoted
24
41
  # form when safe. Falls back to `String#inspect` (double-quoted with
25
42
  # escapes) when the source contains characters that prevent the