ariadna 1.3.1 → 2.0.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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/ariadna.gemspec +0 -1
  3. data/data/agents/ariadna-codebase-mapper.md +34 -722
  4. data/data/agents/ariadna-debugger.md +44 -1139
  5. data/data/agents/ariadna-executor.md +75 -396
  6. data/data/agents/ariadna-planner.md +78 -1215
  7. data/data/agents/ariadna-roadmapper.md +55 -582
  8. data/data/agents/ariadna-verifier.md +60 -702
  9. data/data/ariadna/templates/config.json +8 -33
  10. data/data/ariadna/workflows/debug.md +28 -0
  11. data/data/ariadna/workflows/execute-phase.md +31 -513
  12. data/data/ariadna/workflows/map-codebase.md +20 -319
  13. data/data/ariadna/workflows/new-milestone.md +20 -365
  14. data/data/ariadna/workflows/new-project.md +19 -880
  15. data/data/ariadna/workflows/plan-phase.md +24 -443
  16. data/data/ariadna/workflows/progress.md +20 -376
  17. data/data/ariadna/workflows/quick.md +19 -221
  18. data/data/ariadna/workflows/roadmap-ops.md +28 -0
  19. data/data/ariadna/workflows/verify-work.md +23 -560
  20. data/data/commands/ariadna/add-phase.md +11 -22
  21. data/data/commands/ariadna/debug.md +11 -143
  22. data/data/commands/ariadna/execute-phase.md +12 -30
  23. data/data/commands/ariadna/insert-phase.md +7 -14
  24. data/data/commands/ariadna/map-codebase.md +16 -49
  25. data/data/commands/ariadna/new-milestone.md +12 -25
  26. data/data/commands/ariadna/new-project.md +22 -26
  27. data/data/commands/ariadna/plan-phase.md +13 -22
  28. data/data/commands/ariadna/progress.md +16 -6
  29. data/data/commands/ariadna/quick.md +9 -11
  30. data/data/commands/ariadna/remove-phase.md +9 -12
  31. data/data/commands/ariadna/verify-work.md +14 -19
  32. data/data/skills/rails-backend/API.md +138 -0
  33. data/data/skills/rails-backend/CONTROLLERS.md +154 -0
  34. data/data/skills/rails-backend/JOBS.md +132 -0
  35. data/data/skills/rails-backend/MODELS.md +213 -0
  36. data/data/skills/rails-backend/SKILL.md +169 -0
  37. data/data/skills/rails-frontend/ASSETS.md +154 -0
  38. data/data/skills/rails-frontend/COMPONENTS.md +253 -0
  39. data/data/skills/rails-frontend/SKILL.md +187 -0
  40. data/data/skills/rails-frontend/VIEWS.md +168 -0
  41. data/data/skills/rails-performance/PROFILING.md +106 -0
  42. data/data/skills/rails-performance/SKILL.md +217 -0
  43. data/data/skills/rails-security/AUDIT.md +118 -0
  44. data/data/skills/rails-security/SKILL.md +422 -0
  45. data/data/skills/rails-testing/FIXTURES.md +78 -0
  46. data/data/skills/rails-testing/SKILL.md +160 -0
  47. data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
  48. data/lib/ariadna/installer.rb +11 -15
  49. data/lib/ariadna/tools/cli.rb +0 -12
  50. data/lib/ariadna/tools/config_manager.rb +10 -72
  51. data/lib/ariadna/tools/frontmatter.rb +23 -1
  52. data/lib/ariadna/tools/init.rb +201 -401
  53. data/lib/ariadna/tools/model_profiles.rb +6 -14
  54. data/lib/ariadna/tools/phase_manager.rb +1 -10
  55. data/lib/ariadna/tools/state_manager.rb +170 -451
  56. data/lib/ariadna/tools/template_filler.rb +4 -12
  57. data/lib/ariadna/tools/verification.rb +21 -399
  58. data/lib/ariadna/uninstaller.rb +9 -0
  59. data/lib/ariadna/version.rb +1 -1
  60. metadata +20 -91
  61. data/data/agents/ariadna-backend-executor.md +0 -261
  62. data/data/agents/ariadna-frontend-executor.md +0 -259
  63. data/data/agents/ariadna-integration-checker.md +0 -418
  64. data/data/agents/ariadna-phase-researcher.md +0 -469
  65. data/data/agents/ariadna-plan-checker.md +0 -622
  66. data/data/agents/ariadna-project-researcher.md +0 -618
  67. data/data/agents/ariadna-research-synthesizer.md +0 -236
  68. data/data/agents/ariadna-test-executor.md +0 -266
  69. data/data/ariadna/references/checkpoints.md +0 -772
  70. data/data/ariadna/references/continuation-format.md +0 -249
  71. data/data/ariadna/references/decimal-phase-calculation.md +0 -65
  72. data/data/ariadna/references/git-integration.md +0 -248
  73. data/data/ariadna/references/git-planning-commit.md +0 -38
  74. data/data/ariadna/references/model-profile-resolution.md +0 -32
  75. data/data/ariadna/references/model-profiles.md +0 -73
  76. data/data/ariadna/references/phase-argument-parsing.md +0 -61
  77. data/data/ariadna/references/planning-config.md +0 -194
  78. data/data/ariadna/references/questioning.md +0 -153
  79. data/data/ariadna/references/rails-conventions.md +0 -416
  80. data/data/ariadna/references/tdd.md +0 -267
  81. data/data/ariadna/references/ui-brand.md +0 -160
  82. data/data/ariadna/references/verification-patterns.md +0 -853
  83. data/data/ariadna/templates/codebase/architecture.md +0 -481
  84. data/data/ariadna/templates/codebase/concerns.md +0 -380
  85. data/data/ariadna/templates/codebase/conventions.md +0 -434
  86. data/data/ariadna/templates/codebase/integrations.md +0 -328
  87. data/data/ariadna/templates/codebase/stack.md +0 -189
  88. data/data/ariadna/templates/codebase/structure.md +0 -418
  89. data/data/ariadna/templates/codebase/testing.md +0 -606
  90. data/data/ariadna/templates/context.md +0 -283
  91. data/data/ariadna/templates/continue-here.md +0 -78
  92. data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
  93. data/data/ariadna/templates/phase-prompt.md +0 -609
  94. data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
  95. data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
  96. data/data/ariadna/templates/research-project/FEATURES.md +0 -168
  97. data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
  98. data/data/ariadna/templates/research-project/STACK.md +0 -251
  99. data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
  100. data/data/ariadna/templates/state.md +0 -176
  101. data/data/ariadna/templates/summary-complex.md +0 -59
  102. data/data/ariadna/templates/summary-minimal.md +0 -41
  103. data/data/ariadna/templates/summary-standard.md +0 -48
  104. data/data/ariadna/templates/user-setup.md +0 -310
  105. data/data/ariadna/workflows/add-phase.md +0 -111
  106. data/data/ariadna/workflows/add-todo.md +0 -157
  107. data/data/ariadna/workflows/audit-milestone.md +0 -241
  108. data/data/ariadna/workflows/check-todos.md +0 -176
  109. data/data/ariadna/workflows/complete-milestone.md +0 -644
  110. data/data/ariadna/workflows/diagnose-issues.md +0 -219
  111. data/data/ariadna/workflows/discovery-phase.md +0 -289
  112. data/data/ariadna/workflows/discuss-phase.md +0 -408
  113. data/data/ariadna/workflows/execute-plan.md +0 -448
  114. data/data/ariadna/workflows/help.md +0 -470
  115. data/data/ariadna/workflows/insert-phase.md +0 -129
  116. data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
  117. data/data/ariadna/workflows/pause-work.md +0 -122
  118. data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
  119. data/data/ariadna/workflows/remove-phase.md +0 -154
  120. data/data/ariadna/workflows/research-phase.md +0 -74
  121. data/data/ariadna/workflows/resume-project.md +0 -306
  122. data/data/ariadna/workflows/set-profile.md +0 -80
  123. data/data/ariadna/workflows/settings.md +0 -145
  124. data/data/ariadna/workflows/transition.md +0 -493
  125. data/data/ariadna/workflows/update.md +0 -212
  126. data/data/ariadna/workflows/verify-phase.md +0 -226
  127. data/data/commands/ariadna/add-todo.md +0 -42
  128. data/data/commands/ariadna/audit-milestone.md +0 -42
  129. data/data/commands/ariadna/check-todos.md +0 -41
  130. data/data/commands/ariadna/complete-milestone.md +0 -136
  131. data/data/commands/ariadna/discuss-phase.md +0 -86
  132. data/data/commands/ariadna/help.md +0 -22
  133. data/data/commands/ariadna/list-phase-assumptions.md +0 -50
  134. data/data/commands/ariadna/pause-work.md +0 -35
  135. data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
  136. data/data/commands/ariadna/reapply-patches.md +0 -110
  137. data/data/commands/ariadna/research-phase.md +0 -187
  138. data/data/commands/ariadna/resume-work.md +0 -40
  139. data/data/commands/ariadna/set-profile.md +0 -34
  140. data/data/commands/ariadna/settings.md +0 -36
  141. data/data/commands/ariadna/update.md +0 -37
  142. data/data/guides/backend.md +0 -3069
  143. data/data/guides/frontend.md +0 -1479
  144. data/data/guides/performance.md +0 -1193
  145. data/data/guides/security.md +0 -1522
  146. data/data/guides/style-guide.md +0 -1091
  147. data/data/guides/testing.md +0 -504
  148. data/data/templates.md +0 -94
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: rails-backend
3
+ description: Ruby on Rails backend conventions — models, controllers, jobs, API design. Use when implementing Rails backend code, creating models, writing controllers, or designing APIs.
4
+ ---
5
+
6
+ # Rails Backend Skill
7
+
8
+ Opinionated conventions for Rails backend code. Architecture is built on a single principle: **place the domain model at the center.** Controllers, jobs, and the console are all boundaries that orchestrate domain logic — they contain no business logic themselves.
9
+
10
+ **Sub-files:**
11
+ - [MODELS.md](MODELS.md) — Concern architecture, associations, scoping, callbacks
12
+ - [CONTROLLERS.md](CONTROLLERS.md) — REST conventions, strong params, concerns
13
+ - [JOBS.md](JOBS.md) — _now/_later pattern, multi-tenancy context
14
+ - [API.md](API.md) — JSON responses, serialization, versioning
15
+
16
+ ---
17
+
18
+ ## Core Philosophy
19
+
20
+ ### Domain Model at the Center
21
+
22
+ ```
23
+ Controller ──┐
24
+
25
+ Console ──► Domain Model ◄── Job
26
+
27
+ Script ─────┘
28
+ ```
29
+
30
+ `card.close` works identically whether called from a controller, job, console, or test. The domain model is the single source of truth for business behavior.
31
+
32
+ ### No New Architectural Artifacts
33
+
34
+ No service objects, form objects, interactors, or command pattern libraries. Building blocks:
35
+
36
+ - **Models** — domain entities and operations (ActiveRecord and plain Ruby)
37
+ - **Concerns** — organize model behavior into cohesive modules
38
+ - **Controllers** — HTTP boundary only
39
+ - **Jobs** — async boundary; delegate to model methods
40
+ - **Views** — render domain state (templates, not view components)
41
+
42
+ When something doesn't fit in an entity, create a plain Ruby object with a semantic name — not a new architectural pattern. A `Signup` class, not a `SignupService`. A `Notifier`, not a `NotificationInteractor`.
43
+
44
+ ### Preference for Rails Defaults
45
+
46
+ Minitest over RSpec · view templates over view components · ActiveRecord callbacks over observer patterns · `Current` over dependency injection · concerns over decorator libraries.
47
+
48
+ ---
49
+
50
+ ## Domain Model Overview
51
+
52
+ ```
53
+ Account (tenant/organization)
54
+ └── Users (members with roles)
55
+ └── Boards (project spaces)
56
+ └── Columns (workflow stages)
57
+ └── Cards (tasks/issues)
58
+ └── Comments, Assignments, Tags
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Multi-Tenancy
64
+
65
+ URL path-based tenancy — account ID extracted from path by middleware, sets `Current.account`.
66
+
67
+ ```ruby
68
+ class Current < ActiveSupport::CurrentAttributes
69
+ attribute :session, :user, :identity, :account
70
+
71
+ def session=(value)
72
+ super(value)
73
+ self.identity = session.identity if value.present?
74
+ end
75
+
76
+ def identity=(identity)
77
+ super(identity)
78
+ self.user = identity.users.find_by(account: account) if identity.present?
79
+ end
80
+ end
81
+ ```
82
+
83
+ Setting `Current.session` cascades: resolves `identity`, then resolves `user` for the current account.
84
+
85
+ - Always use `Current.user` instead of passing `@user` as a parameter
86
+ - Always use `Current.account` for tenant scoping
87
+ - In tests: `Current.session = sessions(:david)` sets up the full chain
88
+
89
+ ---
90
+
91
+ ## No Service Objects
92
+
93
+ Controllers already fulfill the application service role from DDD — they sit at the boundary and orchestrate domain entities. Adding service objects creates a redundant layer.
94
+
95
+ **The real danger: anemic domain models.** When business logic lives in service objects instead of domain entities, models become empty data holders and logic scatters across a flat list of service classes with no object-oriented structure.
96
+
97
+ ```ruby
98
+ # Bad: service object drains logic from the model
99
+ class CloseCardService
100
+ def call = @card.update!(status: "closed") && EventService.new(@card).call
101
+
102
+ # Good: logic belongs in the domain entity
103
+ module Card::Closeable
104
+ def close(user: Current.user)
105
+ transaction { create_closure!(user:); track_event :closed, creator: user }
106
+ end
107
+ end
108
+ ```
109
+
110
+ **Decision tree: where does logic belong?**
111
+
112
+ - Acts on one entity's state → put in model/concern
113
+ - Coordinates 2-3 entities, triggered by HTTP → controller handles it
114
+ - Coordinates 2-3 entities, domain concept → plain Ruby object (`Signup`, not `SignupService`)
115
+ - Runs async → job delegates to model method
116
+
117
+ ---
118
+
119
+ ## Code Style Conventions
120
+
121
+ ### Conditional Returns
122
+
123
+ Prefer expanded conditionals over guard clauses in the middle of methods. Guard clauses are acceptable at the very start when the method body is non-trivial.
124
+
125
+ ```ruby
126
+ # Avoid mid-method returns
127
+ def process
128
+ return [] unless ids
129
+ find(ids)
130
+ end
131
+
132
+ # Prefer
133
+ def process
134
+ if ids then find(ids) else [] end
135
+ end
136
+ ```
137
+
138
+ ### Method Ordering
139
+
140
+ 1. Class methods (top)
141
+ 2. Public methods (`initialize` first)
142
+ 3. Private methods — indented under `private`, no blank line after `private`
143
+
144
+ ```ruby
145
+ private
146
+ def method_one; end
147
+ def method_two; end
148
+ ```
149
+
150
+ Private methods are ordered by invocation — top-to-bottom following execution flow.
151
+
152
+ ### Bang Methods
153
+
154
+ Only use `!` for methods that have a non-bang counterpart (`save`/`save!`). Do not use `!` to flag destructive actions with no non-bang version.
155
+
156
+ ---
157
+
158
+ ## Common Gotchas
159
+
160
+ 1. **Business logic in controllers** — move it to models
161
+ 2. **Custom route actions** — use `resource :closure` not `post :close`
162
+ 3. **Logic in jobs** — jobs are thin wrappers; logic lives in model methods
163
+ 4. **Association declaration order** — declare `belongs_to :board` before `belongs_to :account, default: -> { board.account }`
164
+ 5. **Finding cards by `:id`** — cards use `:number`: `accessible_cards.find_by!(number: params[:card_id])`
165
+ 6. **Multi-step operations without transactions** — always wrap in `transaction do`
166
+
167
+ ---
168
+
169
+ See [MODELS.md](MODELS.md) for ActiveRecord patterns, [CONTROLLERS.md](CONTROLLERS.md) for HTTP conventions, [JOBS.md](JOBS.md) for background job patterns, and [API.md](API.md) for API design.
@@ -0,0 +1,154 @@
1
+ # Assets — CSS, JS Bundling, Design Tokens
2
+
3
+ ## CSS Architecture
4
+
5
+ **Pure custom CSS — no Tailwind, Bootstrap, or utility frameworks.** Built on modern CSS: custom properties, OKLCH colors, CSS layers, and logical properties.
6
+
7
+ ### Layer Organization
8
+
9
+ ```css
10
+ @layer reset; /* Browser normalization */
11
+ @layer base; /* Base element styles */
12
+ @layer components; /* Component-specific styles */
13
+ @layer modules; /* Feature modules */
14
+ @layer utilities; /* Utility classes */
15
+ ```
16
+
17
+ ### File Organization
18
+
19
+ ```
20
+ app/assets/stylesheets/
21
+ ├── _global.css # Root variables and @layer definitions
22
+ ├── base.css # Base element styling
23
+ ├── utilities.css # Utility classes
24
+ ├── buttons.css # Button components
25
+ ├── inputs.css # Form inputs
26
+ ├── cards.css # Card components
27
+ ├── layout.css # Main layout
28
+ └── [feature].css # Feature-specific styles
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Design Tokens
34
+
35
+ ### Colors — OKLCH
36
+
37
+ Use OKLCH for perceptual uniformity across light/dark modes. Always use semantic variables:
38
+
39
+ | Variable | Purpose |
40
+ |----------|---------|
41
+ | `--color-ink` | Primary text |
42
+ | `--color-ink-light` | Secondary text |
43
+ | `--color-canvas` | Background |
44
+ | `--color-link` | Interactive elements (blue) |
45
+ | `--color-positive` | Success (green) |
46
+ | `--color-negative` | Error (red) |
47
+
48
+ Dark mode via attribute and media query:
49
+
50
+ ```css
51
+ html[data-theme="dark"] { ... }
52
+ @media (prefers-color-scheme: dark) { html:not([data-theme]) { ... } }
53
+ ```
54
+
55
+ ### Spacing — Logical Properties
56
+
57
+ Use `block` (vertical) and `inline` (horizontal) instead of top/bottom/left/right for RTL support:
58
+
59
+ ```css
60
+ --inline-space: 1ch;
61
+ --block-space: 1rem;
62
+ ```
63
+
64
+ Key utilities: `.pad`, `.pad-block`, `.pad-inline`, `.margin`, `.gap`, `.gap-half`
65
+
66
+ ### Typography
67
+
68
+ ```css
69
+ --font-sans: "Adwaita Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
70
+ --font-mono: ui-monospace, monospace;
71
+ ```
72
+
73
+ Scale: `--text-xx-small` (0.55rem) through `--text-xx-large` (2.5rem). Always use `rem`, never `px` for font sizes.
74
+
75
+ ---
76
+
77
+ ## Icons
78
+
79
+ CSS mask-based icon system — icons inherit `currentColor`, easy to style contextually:
80
+
81
+ ```erb
82
+ <%= icon_tag "check" %>
83
+ <%= icon_tag "pencil", class: "txt-subtle" %>
84
+ ```
85
+
86
+ Generates: `<span class="icon icon--check" aria-hidden="true"></span>`
87
+
88
+ Icon-only buttons — use `aria-label` or `.for-screen-reader` for accessibility:
89
+
90
+ ```erb
91
+ <%= button_to path, class: "btn", aria: { label: "Edit" } do %>
92
+ <%= icon_tag "pencil" %>
93
+ <% end %>
94
+ ```
95
+
96
+ New SVG requirements: `viewBox="0 0 24 24"`, no fixed `width`/`height`, `fill="currentColor"`, monochrome for mask-image compatibility.
97
+
98
+ ---
99
+
100
+ ## Component Patterns
101
+
102
+ ### Buttons
103
+
104
+ ```css
105
+ .btn /* Base — rounded pill */
106
+ .btn--positive /* Green */
107
+ .btn--negative /* Red/destructive */
108
+ .btn--link /* Text-only */
109
+ .btn--circle /* Circular icon button */
110
+ ```
111
+
112
+ ### Inputs
113
+
114
+ All inputs use `font-size: max(16px, 1em)` to prevent iOS auto-zoom.
115
+
116
+ ```erb
117
+ <%= form.text_field :name, class: "input" %>
118
+ <%= form.select :status, options, {}, class: "input input--select" %>
119
+ <%= form.text_area :body, class: "input input--textarea", rows: 1 %>
120
+ ```
121
+
122
+ Auto-resizing textarea (modern browsers): `field-sizing: content`
123
+
124
+ ### Focus & Motion
125
+
126
+ ```css
127
+ --focus-ring: 2px solid var(--color-link); /* Applied via :focus-visible */
128
+
129
+ @media (prefers-reduced-motion: reduce) {
130
+ animation-duration: 0.01ms !important;
131
+ transition-duration: 0.01ms !important;
132
+ }
133
+ ```
134
+
135
+ Always respect `prefers-reduced-motion`. Always apply focus states on interactive elements.
136
+
137
+ ---
138
+
139
+ ## JS Bundling
140
+
141
+ Controllers are auto-discovered from `app/javascript/controllers/`. No manual registration.
142
+
143
+ Importmap (default) or esbuild depending on project setup. Third-party libraries bridged through Stimulus value callbacks — keep the controller as the integration boundary, not inline `<script>` tags.
144
+
145
+ ---
146
+
147
+ ## Rules
148
+
149
+ - Use semantic color variables, never hard-coded values
150
+ - Use logical properties (`block-start` not `top`, `inline-size` not `width`)
151
+ - Use CSS custom properties for component variants
152
+ - Use `clamp()` for fluid responsive values
153
+ - Test in both light and dark modes
154
+ - Never create z-index values without adding to the defined stack
@@ -0,0 +1,253 @@
1
+ # Components — Stimulus Controllers & Presenter Pattern
2
+
3
+ ## Stimulus Controller Architecture
4
+
5
+ **File naming:** `app/javascript/controllers/{name}_controller.js`
6
+
7
+ | File | Class | Identifier |
8
+ |------|-------|------------|
9
+ | `upload_preview_controller.js` | `UploadPreviewController` | `upload-preview` |
10
+ | `broadcast_channel_controller.js` | `BroadcastChannelController` | `broadcast-channel` |
11
+
12
+ Controllers in `app/javascript/controllers/` are auto-discovered. No manual `application.register()` needed.
13
+
14
+ ### Contract-First Declaration
15
+
16
+ Declare the full interface at the top before any methods:
17
+
18
+ ```javascript
19
+ import { Controller } from "@hotwired/stimulus"
20
+
21
+ export default class extends Controller {
22
+ static values = { url: String, refreshInterval: Number, active: Boolean }
23
+ static targets = ["output", "spinner", "emptyState"]
24
+ static outlets = ["filter", "notification"]
25
+ static classes = ["loading", "hidden"]
26
+
27
+ connect() { }
28
+ disconnect() { }
29
+ }
30
+ ```
31
+
32
+ **Single-purpose controllers.** Compose multiple controllers rather than one large one:
33
+
34
+ ```html
35
+ <div data-controller="clipboard toggle tooltip"
36
+ data-clipboard-text-value="https://example.com/share/abc123">
37
+ <button data-action="clipboard#copy toggle#toggle">Copy Link</button>
38
+ </div>
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Lifecycle
44
+
45
+ **Every `connect()` resource must be released in `disconnect()`.** Turbo navigations and morphs trigger these repeatedly:
46
+
47
+ ```javascript
48
+ connect() {
49
+ this.broadcast = new BroadcastChannel(this.channelValue)
50
+ this.broadcast.onmessage = this.handleMessage.bind(this)
51
+ this.refreshTimer = setInterval(() => this.refresh(), 30000)
52
+ }
53
+
54
+ disconnect() {
55
+ this.broadcast.close()
56
+ clearInterval(this.refreshTimer)
57
+ }
58
+ ```
59
+
60
+ | Resource | Setup | Teardown |
61
+ |----------|-------|----------|
62
+ | BroadcastChannel | `new BroadcastChannel()` | `.close()` |
63
+ | Blob URL | `URL.createObjectURL()` | `URL.revokeObjectURL()` |
64
+ | Timer | `setInterval()` / `setTimeout()` | `clearInterval()` / `clearTimeout()` |
65
+ | Observer | `.observe()` | `.disconnect()` |
66
+ | EventListener (window/doc) | `addEventListener()` | `removeEventListener()` |
67
+
68
+ **Guard `valueChanged` callbacks** — they fire before `connect()` completes; targets may not exist yet:
69
+
70
+ ```javascript
71
+ urlValueChanged(url) {
72
+ if (!this.hasFrameTarget) return
73
+ this.frameTarget.src = url
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Values (Reactive State)
80
+
81
+ Values are the single source of truth. Never duplicate state in dataset entries or instance variables.
82
+
83
+ ```javascript
84
+ static values = {
85
+ url: String, // default: ""
86
+ count: Number, // default: 0
87
+ active: Boolean, // default: false
88
+ filters: Object, // default: {}
89
+ items: Array, // default: []
90
+ }
91
+ ```
92
+
93
+ React with `{name}ValueChanged`. The `previous` argument is `undefined` on initial load:
94
+
95
+ ```javascript
96
+ pageValueChanged(current, previous) {
97
+ if (previous !== undefined) this.fetchPage(current)
98
+ this.counterTarget.textContent = `Page ${current}`
99
+ }
100
+
101
+ next() { this.pageValue++ } // Triggers pageValueChanged automatically
102
+ ```
103
+
104
+ Bridge third-party libraries through value callbacks — the value is the source of truth, the callback translates to the library API:
105
+
106
+ ```javascript
107
+ dataValueChanged(data) {
108
+ if (!this.chart) return
109
+ this.chart.data = data
110
+ this.chart.update()
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Targets
117
+
118
+ Target callbacks fire when the DOM changes — essential for Turbo Stream integration:
119
+
120
+ ```javascript
121
+ static targets = ["item", "counter", "emptyState"]
122
+
123
+ itemTargetConnected(element) {
124
+ this.updateCount()
125
+ element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 })
126
+ }
127
+
128
+ itemTargetDisconnected() { this.updateCount() }
129
+
130
+ updateCount() {
131
+ this.counterTarget.textContent = this.itemTargets.length
132
+ this.emptyStateTarget.hidden = this.itemTargets.length > 0
133
+ }
134
+ ```
135
+
136
+ **Keep target callbacks idempotent** — morphs can trigger `TargetConnected` multiple times:
137
+
138
+ ```javascript
139
+ // Bad — adds duplicate listeners on reconnect
140
+ itemTargetConnected(element) {
141
+ element.addEventListener("click", this.handleClick)
142
+ }
143
+
144
+ // Good
145
+ itemTargetConnected(element) {
146
+ element.handleClick ||= this.handleClick.bind(this)
147
+ element.removeEventListener("click", element.handleClick)
148
+ element.addEventListener("click", element.handleClick)
149
+ }
150
+ ```
151
+
152
+ Derive computed state from targets rather than tracking separate values:
153
+
154
+ ```javascript
155
+ get isEmpty() { return this.itemTargets.length === 0 }
156
+ get selectedItems() { return this.itemTargets.filter(el => el.dataset.selected === "true") }
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Outlets (Controller-to-Controller)
162
+
163
+ Prefer outlets over custom events or `getControllerForElementAndIdentifier` for direct communication:
164
+
165
+ ```javascript
166
+ // dashboard_controller.js
167
+ static outlets = ["chart", "filter"]
168
+
169
+ apply() {
170
+ const filters = this.filterOutlet.currentFilters
171
+ this.chartOutlets.forEach(chart => chart.reload(filters))
172
+ }
173
+ ```
174
+
175
+ ```html
176
+ <div data-controller="dashboard"
177
+ data-dashboard-chart-outlet=".chart-widget"
178
+ data-dashboard-filter-outlet="#main-filter">
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Action Parameters
184
+
185
+ Pass typed data from HTML without manual `dataset` parsing:
186
+
187
+ ```html
188
+ <button data-action="cart#add"
189
+ data-cart-id-param="42"
190
+ data-cart-name-param="Widget"
191
+ data-cart-price-param="19.99">
192
+ ```
193
+
194
+ ```javascript
195
+ add({ params: { id, name, price } }) {
196
+ this.addItem(id, name, price) // id = 42 (Number), name = "Widget" (String)
197
+ }
198
+ ```
199
+
200
+ Keyboard filters:
201
+
202
+ ```html
203
+ <textarea data-action="keydown.ctrl+s->editor#save
204
+ keydown.meta+s->editor#save
205
+ keydown.esc->editor#cancel">
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Presenter Pattern
211
+
212
+ Plain Ruby classes in `app/models/` — no special directory, no inheritance:
213
+
214
+ ```ruby
215
+ class Event::Description
216
+ include ActionView::Helpers::TagHelper
217
+ include ERB::Util
218
+
219
+ def initialize(event, user) = @event, @user = event, user
220
+
221
+ def to_html = to_sentence(creator_tag, card_title_tag).html_safe
222
+ def to_plain_text = to_sentence(creator_name, quoted(card.title))
223
+
224
+ private
225
+ def creator_tag = tag.span(event.creator.name, class: "creator")
226
+ end
227
+ ```
228
+
229
+ Factory method on the model keeps the API discoverable:
230
+
231
+ ```ruby
232
+ # app/models/event.rb
233
+ def description_for(user) = Event::Description.new(self, user)
234
+ ```
235
+
236
+ ```erb
237
+ <%= event.description_for(Current.user).to_html %>
238
+ ```
239
+
240
+ Controller concern for cross-controller instantiation:
241
+
242
+ ```ruby
243
+ module FilterScoped
244
+ extend ActiveSupport::Concern
245
+ included do
246
+ before_action :set_user_filtering
247
+ end
248
+ private
249
+ def set_user_filtering
250
+ @user_filtering = User::Filtering.new(Current.user, @filter, expanded: expanded_param)
251
+ end
252
+ end
253
+ ```