ariadna 1.3.0 → 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 (149) 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. data/lib/ariadna.rb +1 -0
  61. metadata +20 -91
  62. data/data/agents/ariadna-backend-executor.md +0 -261
  63. data/data/agents/ariadna-frontend-executor.md +0 -259
  64. data/data/agents/ariadna-integration-checker.md +0 -418
  65. data/data/agents/ariadna-phase-researcher.md +0 -469
  66. data/data/agents/ariadna-plan-checker.md +0 -622
  67. data/data/agents/ariadna-project-researcher.md +0 -618
  68. data/data/agents/ariadna-research-synthesizer.md +0 -236
  69. data/data/agents/ariadna-test-executor.md +0 -266
  70. data/data/ariadna/references/checkpoints.md +0 -772
  71. data/data/ariadna/references/continuation-format.md +0 -249
  72. data/data/ariadna/references/decimal-phase-calculation.md +0 -65
  73. data/data/ariadna/references/git-integration.md +0 -248
  74. data/data/ariadna/references/git-planning-commit.md +0 -38
  75. data/data/ariadna/references/model-profile-resolution.md +0 -32
  76. data/data/ariadna/references/model-profiles.md +0 -73
  77. data/data/ariadna/references/phase-argument-parsing.md +0 -61
  78. data/data/ariadna/references/planning-config.md +0 -194
  79. data/data/ariadna/references/questioning.md +0 -153
  80. data/data/ariadna/references/rails-conventions.md +0 -416
  81. data/data/ariadna/references/tdd.md +0 -267
  82. data/data/ariadna/references/ui-brand.md +0 -160
  83. data/data/ariadna/references/verification-patterns.md +0 -853
  84. data/data/ariadna/templates/codebase/architecture.md +0 -481
  85. data/data/ariadna/templates/codebase/concerns.md +0 -380
  86. data/data/ariadna/templates/codebase/conventions.md +0 -434
  87. data/data/ariadna/templates/codebase/integrations.md +0 -328
  88. data/data/ariadna/templates/codebase/stack.md +0 -189
  89. data/data/ariadna/templates/codebase/structure.md +0 -418
  90. data/data/ariadna/templates/codebase/testing.md +0 -606
  91. data/data/ariadna/templates/context.md +0 -283
  92. data/data/ariadna/templates/continue-here.md +0 -78
  93. data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
  94. data/data/ariadna/templates/phase-prompt.md +0 -609
  95. data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
  96. data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
  97. data/data/ariadna/templates/research-project/FEATURES.md +0 -168
  98. data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
  99. data/data/ariadna/templates/research-project/STACK.md +0 -251
  100. data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
  101. data/data/ariadna/templates/state.md +0 -176
  102. data/data/ariadna/templates/summary-complex.md +0 -59
  103. data/data/ariadna/templates/summary-minimal.md +0 -41
  104. data/data/ariadna/templates/summary-standard.md +0 -48
  105. data/data/ariadna/templates/user-setup.md +0 -310
  106. data/data/ariadna/workflows/add-phase.md +0 -111
  107. data/data/ariadna/workflows/add-todo.md +0 -157
  108. data/data/ariadna/workflows/audit-milestone.md +0 -241
  109. data/data/ariadna/workflows/check-todos.md +0 -176
  110. data/data/ariadna/workflows/complete-milestone.md +0 -644
  111. data/data/ariadna/workflows/diagnose-issues.md +0 -219
  112. data/data/ariadna/workflows/discovery-phase.md +0 -289
  113. data/data/ariadna/workflows/discuss-phase.md +0 -408
  114. data/data/ariadna/workflows/execute-plan.md +0 -448
  115. data/data/ariadna/workflows/help.md +0 -470
  116. data/data/ariadna/workflows/insert-phase.md +0 -129
  117. data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
  118. data/data/ariadna/workflows/pause-work.md +0 -122
  119. data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
  120. data/data/ariadna/workflows/remove-phase.md +0 -154
  121. data/data/ariadna/workflows/research-phase.md +0 -74
  122. data/data/ariadna/workflows/resume-project.md +0 -306
  123. data/data/ariadna/workflows/set-profile.md +0 -80
  124. data/data/ariadna/workflows/settings.md +0 -145
  125. data/data/ariadna/workflows/transition.md +0 -493
  126. data/data/ariadna/workflows/update.md +0 -212
  127. data/data/ariadna/workflows/verify-phase.md +0 -226
  128. data/data/commands/ariadna/add-todo.md +0 -42
  129. data/data/commands/ariadna/audit-milestone.md +0 -42
  130. data/data/commands/ariadna/check-todos.md +0 -41
  131. data/data/commands/ariadna/complete-milestone.md +0 -136
  132. data/data/commands/ariadna/discuss-phase.md +0 -86
  133. data/data/commands/ariadna/help.md +0 -22
  134. data/data/commands/ariadna/list-phase-assumptions.md +0 -50
  135. data/data/commands/ariadna/pause-work.md +0 -35
  136. data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
  137. data/data/commands/ariadna/reapply-patches.md +0 -110
  138. data/data/commands/ariadna/research-phase.md +0 -187
  139. data/data/commands/ariadna/resume-work.md +0 -40
  140. data/data/commands/ariadna/set-profile.md +0 -34
  141. data/data/commands/ariadna/settings.md +0 -36
  142. data/data/commands/ariadna/update.md +0 -37
  143. data/data/guides/backend.md +0 -3069
  144. data/data/guides/frontend.md +0 -1479
  145. data/data/guides/performance.md +0 -1193
  146. data/data/guides/security.md +0 -1522
  147. data/data/guides/style-guide.md +0 -1091
  148. data/data/guides/testing.md +0 -504
  149. 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
+ ```