jsx_rosetta 0.4.0 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +342 -11
  3. data/CLAUDE.md +70 -0
  4. data/README.md +50 -0
  5. data/ROADMAP.md +92 -0
  6. data/agents/jsx-rosetta-resolve-todo-file.md +90 -0
  7. data/lib/jsx_rosetta/ast/inflector.rb +32 -0
  8. data/lib/jsx_rosetta/backend/phlex.rb +1421 -158
  9. data/lib/jsx_rosetta/backend/rails_view.rb +1 -1
  10. data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +357 -33
  11. data/lib/jsx_rosetta/backend/view_component.rb +261 -31
  12. data/lib/jsx_rosetta/cli.rb +175 -37
  13. data/lib/jsx_rosetta/icons/lucide.json +37 -0
  14. data/lib/jsx_rosetta/icons.rb +44 -0
  15. data/lib/jsx_rosetta/ir/lowering.rb +1164 -70
  16. data/lib/jsx_rosetta/ir/module_shape_classifier.rb +20 -1
  17. data/lib/jsx_rosetta/ir/radix_registry.rb +84 -0
  18. data/lib/jsx_rosetta/ir/types.rb +264 -19
  19. data/lib/jsx_rosetta/ir.rb +5 -4
  20. data/lib/jsx_rosetta/pages_routing.rb +640 -0
  21. data/lib/jsx_rosetta/version.rb +1 -1
  22. data/lib/jsx_rosetta.rb +8 -6
  23. data/plans/nextjs_pages_to_rails.md +200 -0
  24. data/plans/nextjs_pages_to_rails_slice_2.md +118 -0
  25. data/plans/nextjs_pages_to_rails_slice_3.md +121 -0
  26. data/plans/nextjs_pages_to_rails_slice_4.md +301 -0
  27. data/plans/translator_widening_and_pages_followups.md +120 -0
  28. data/plans/translator_widening_slice_a.md +208 -0
  29. data/skills/jsx-rosetta-resolve-todos/SKILL.md +206 -0
  30. data/skills/jsx-rosetta-resolve-todos/data/design_tokens.template.yml +71 -0
  31. data/skills/jsx-rosetta-resolve-todos/data/target_app_conventions.template.yml +107 -0
  32. data/skills/jsx-rosetta-resolve-todos/examples/design_tokens.ant_design_v5.yml +190 -0
  33. data/skills/jsx-rosetta-resolve-todos/recipes/01_design_tokens.md +74 -0
  34. data/skills/jsx-rosetta-resolve-todos/recipes/02_promoted_ivar.md +49 -0
  35. data/skills/jsx-rosetta-resolve-todos/recipes/03_react_hooks.md +34 -0
  36. data/skills/jsx-rosetta-resolve-todos/recipes/04_apollo_hooks.md +34 -0
  37. data/skills/jsx-rosetta-resolve-todos/recipes/05_event_handlers.md +45 -0
  38. data/skills/jsx-rosetta-resolve-todos/recipes/06_module_constants.md +29 -0
  39. data/skills/jsx-rosetta-resolve-todos/recipes/07_nextjs_navigation.md +44 -0
  40. data/skills/jsx-rosetta-resolve-todos/recipes/08_generic_js_bailouts.md +55 -0
  41. data/skills/jsx-rosetta-resolve-todos/tools/apply_promoted_ivar.rb +189 -0
  42. data/skills/jsx-rosetta-resolve-todos/tools/apply_substitutions.rb +292 -0
  43. data/skills/jsx-rosetta-resolve-todos/tools/diff_corpus.rb +161 -0
  44. data/skills/jsx-rosetta-resolve-todos/tools/discover_bailouts.rb +211 -0
  45. metadata +30 -1
@@ -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.
@@ -19,6 +19,38 @@ module JsxRosetta
19
19
  parts = string.split("_")
20
20
  parts[0] + parts[1..].map(&:capitalize).join
21
21
  end
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
+
40
+ # Emit a Ruby string literal in the rubocop-default single-quoted
41
+ # form when safe. Falls back to `String#inspect` (double-quoted with
42
+ # escapes) when the source contains characters that prevent the
43
+ # single-quoted form: single quotes themselves, backslashes (Ruby
44
+ # single-quoted strings only escape `\\` and `\'`), or control
45
+ # characters (`\n`, `\t`, etc — single-quoted strings render those
46
+ # literally). Non-ASCII characters (emojis, unicode) are fine in
47
+ # single-quoted strings, so they don't force the fallback.
48
+ def ruby_string_literal(value)
49
+ str = value.to_s
50
+ return str.inspect if str.include?("'") || str.include?("\\") || str.match?(/[\x00-\x1f\x7f]/)
51
+
52
+ "'#{str}'"
53
+ end
22
54
  end
23
55
  end
24
56
  end