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
@@ -1,39 +1,34 @@
1
1
  ---
2
2
  name: ariadna:verify-work
3
- description: Validate built features through conversational UAT
3
+ description: Validate built features against phase goals — goal-backward, not task-backward
4
4
  argument-hint: "[phase number, e.g., '4']"
5
5
  allowed-tools:
6
6
  - Read
7
+ - Write
8
+ - Edit
7
9
  - Bash
8
10
  - Glob
9
11
  - Grep
10
- - Edit
11
- - Write
12
12
  - Task
13
13
  ---
14
14
  <objective>
15
- Validate built features through conversational testing with persistent state.
16
-
17
- Purpose: Confirm what Claude built actually works from user's perspective. One test at a time, plain text responses, no interrogation. When issues are found, automatically diagnose, plan fixes, and prepare for execution.
15
+ Confirm that what was built delivers the phase goal, not merely that tasks were completed. Produces VERIFICATION.md with pass/fail/gap status and feeds gaps back into planning.
18
16
 
19
- Output: {phase}-UAT.md tracking all test results. If issues found: diagnosed gaps, verified fix plans ready for /ariadna:execute-phase
17
+ Spawn `ariadna-verifier` to check goal achievement against the actual codebase.
20
18
  </objective>
21
19
 
22
- <execution_context>
23
- @~/.claude/ariadna/workflows/verify-work.md
24
- @~/.claude/ariadna/templates/UAT.md
25
- </execution_context>
26
-
27
20
  <context>
28
- Phase: $ARGUMENTS (optional)
29
- - If provided: Test specific phase (e.g., "4")
30
- - If not provided: Check for active sessions or prompt for phase
21
+ Phase: $ARGUMENTS (optional — checks active session or prompts if omitted)
31
22
 
32
- @.ariadna_planning/STATE.md
33
- @.ariadna_planning/ROADMAP.md
23
+ Follow the workflow in `~/.claude/ariadna/workflows/verify-work.md` end-to-end.
34
24
  </context>
35
25
 
36
26
  <process>
37
- Execute the verify-work workflow from @~/.claude/ariadna/workflows/verify-work.md end-to-end.
38
- Preserve all workflow gates (session management, test presentation, diagnosis, fix planning, routing).
27
+ 1. Run `ariadna-tools init verify-work "$PHASE_ARG"` to load context as JSON.
28
+ 2. If `has_verification: true`, offer to re-verify or show existing report.
29
+ 3. Spawn `ariadna-verifier`; verifier checks `must_haves` from plan frontmatter against files on disk.
30
+ 4. On completion: update `memory/progress.md` and STATE.md.
31
+ 5. If `gaps_found`: display gap summary and offer `/ariadna:plan-phase {N} --gaps`.
32
+ 6. If `human_needed`: list items for manual testing; wait for confirmation before marking verified.
33
+ 7. If `passed`: mark phase verified in STATE.md and ROADMAP.md.
39
34
  </process>
@@ -0,0 +1,138 @@
1
+ # Rails Backend: API Design
2
+
3
+ JSON responses, serialization, pagination, versioning, and webhook patterns.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ The application is primarily Turbo/Hotwire, but controllers respond to JSON where needed. The same domain model methods serve both formats — no separate API layer.
10
+
11
+ ```ruby
12
+ def create
13
+ @card.gild
14
+
15
+ respond_to do |format|
16
+ format.turbo_stream { render_card_replacement }
17
+ format.json { head :no_content }
18
+ end
19
+ end
20
+ ```
21
+
22
+ ---
23
+
24
+ ## RESTful URL Design
25
+
26
+ State changes are modeled as resources, never custom actions:
27
+
28
+ ```
29
+ POST /:account_id/boards/:board_id/cards/:card_id/closure → close card
30
+ DELETE /:account_id/boards/:board_id/cards/:card_id/closure → reopen card
31
+ POST /:account_id/boards/:board_id/cards/:card_id/goldness → gild card
32
+ POST /:account_id/boards/:board_id/cards/:card_id/assignments → assign user
33
+ ```
34
+
35
+ The `{account_id}` path segment is the multi-tenancy key — middleware extracts it to set `Current.account`.
36
+
37
+ ---
38
+
39
+ ## Standard JSON Response Patterns
40
+
41
+ ```ruby
42
+ format.json { head :no_content } # Success, no body
43
+ format.json { render json: @card } # Success with resource
44
+ format.json { render json: @card, status: :created, location: @card } # Created
45
+ format.json { render json: @card.errors, status: :unprocessable_entity } # Validation failure
46
+ format.json { head :forbidden } # Not authorized
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Serialization
52
+
53
+ Keep serialization out of models. Use a dedicated serializer object for complex shapes:
54
+
55
+ ```ruby
56
+ class CardSerializer
57
+ def initialize(card)
58
+ @card = card
59
+ end
60
+
61
+ def as_json
62
+ {
63
+ id: @card.number, # Always expose number as public ID, never database id
64
+ title: @card.title,
65
+ closed: @card.closed?,
66
+ golden: @card.golden?,
67
+ creator: { id: @card.creator.id, name: @card.creator.name }
68
+ }
69
+ end
70
+ end
71
+
72
+ render json: CardSerializer.new(@card).as_json
73
+ ```
74
+
75
+ **Always expose `number` as the public card identifier, not `id`.**
76
+
77
+ ---
78
+
79
+ ## Pagination in API Responses
80
+
81
+ ```ruby
82
+ def index
83
+ set_page_and_extract_portion_from Current.account.cards.active.ordered
84
+
85
+ respond_to do |format|
86
+ format.json do
87
+ render json: {
88
+ cards: @page.records.map { |c| CardSerializer.new(c).as_json },
89
+ meta: {
90
+ page: @page.number,
91
+ last_page: @page.last?,
92
+ next_page: @page.last? ? nil : @page.next_param,
93
+ total: @page.recordset.records_count
94
+ }
95
+ }
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ For large datasets (100k+ rows), use cursor-based pagination:
102
+
103
+ ```ruby
104
+ set_page_and_extract_portion_from Event.all, ordered_by: { created_at: :desc, id: :desc }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Versioning
110
+
111
+ Namespace routes and controllers when versioning is needed:
112
+
113
+ ```ruby
114
+ # config/routes.rb
115
+ namespace :api do
116
+ namespace :v1 do
117
+ resources :boards do
118
+ resources :cards
119
+ end
120
+ end
121
+ end
122
+ ```
123
+
124
+ Controllers in `app/controllers/api/v1/` reuse the same domain model methods. Only serialization and routing differ between versions.
125
+
126
+ ---
127
+
128
+ ## Webhook Events
129
+
130
+ Webhooks are driven by the event system. Events carry a `particulars` JSON hash with action-specific context:
131
+
132
+ ```ruby
133
+ event.action # => "card_board_changed"
134
+ event.eventable # => the Card record
135
+ event.particulars # => { "old_board" => "Project A", "new_board" => "Project B" }
136
+ ```
137
+
138
+ Delivery is handled by `Webhook::DeliveryJob` — decoupled from request handling via `after_create_commit` on the Event model.
@@ -0,0 +1,154 @@
1
+ # Rails Backend: Controllers
2
+
3
+ HTTP boundary conventions — thin controllers, RESTful resource modeling, concerns, pagination.
4
+
5
+ ---
6
+
7
+ ## Thin Controllers
8
+
9
+ Controllers have exactly 3 responsibilities: **setup, call model, respond.** No business logic.
10
+
11
+ ```ruby
12
+ class Cards::GoldnessesController < ApplicationController
13
+ include CardScoped # Sets @card and @board via before_action
14
+
15
+ def create
16
+ @card.gild # Single model method call
17
+
18
+ respond_to do |format|
19
+ format.turbo_stream { render_card_replacement }
20
+ format.json { head :no_content }
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ @card.ungild
26
+ respond_to { |f| f.turbo_stream { render_card_replacement } }
27
+ end
28
+ end
29
+ ```
30
+
31
+ That's the entire controller. Must not contain business logic, build complex queries, or directly manipulate multiple models.
32
+
33
+ ---
34
+
35
+ ## RESTful Resource Nesting
36
+
37
+ Model every state change as a resource. Never add custom action methods.
38
+
39
+ ```ruby
40
+ # Bad: custom actions
41
+ resources :cards do
42
+ post :close
43
+ post :gild
44
+ end
45
+
46
+ # Good: each state change is its own resource
47
+ resources :cards do
48
+ scope module: :cards do
49
+ resource :closure # POST creates (close), DELETE destroys (reopen)
50
+ resource :goldness # POST creates (gild), DELETE destroys (ungild)
51
+ resource :pin
52
+ resource :watch
53
+ resources :assignments
54
+ resources :comments
55
+ end
56
+ end
57
+ ```
58
+
59
+ **Rule of thumb**: If the action creates, updates, or destroys something — that "something" is the resource.
60
+ - Close card → creates `Closure`
61
+ - Gild card → creates `Goldness`
62
+ - Assign user → creates `Assignment`
63
+
64
+ ---
65
+
66
+ ## Controller Concerns
67
+
68
+ Extract repeated `before_action` patterns:
69
+
70
+ ```ruby
71
+ module CardScoped
72
+ extend ActiveSupport::Concern
73
+
74
+ included do
75
+ before_action :set_card, :set_board
76
+ end
77
+
78
+ private
79
+ def set_card
80
+ # Cards found by :number (user-facing ID), scoped to accessible cards
81
+ @card = Current.user.accessible_cards.find_by!(number: params[:card_id])
82
+ end
83
+
84
+ def set_board = @board = @card.board
85
+
86
+ def render_card_replacement
87
+ render turbo_stream: turbo_stream.replace(
88
+ [@card, :card_container],
89
+ partial: "cards/container",
90
+ method: :morph,
91
+ locals: { card: @card.reload }
92
+ )
93
+ end
94
+ end
95
+
96
+ module BoardScoped
97
+ extend ActiveSupport::Concern
98
+ included do
99
+ before_action :set_board
100
+ end
101
+ private
102
+ def set_board = @board = Current.user.boards.find(params[:board_id])
103
+ def ensure_admin = head(:forbidden) unless Current.user.can_administer_board?(@board)
104
+ end
105
+ ```
106
+
107
+ Create a controller concern when 3+ controllers share the same `before_action` or resource loading.
108
+
109
+ ---
110
+
111
+ ## Pagination
112
+
113
+ Use `geared_pagination` — one call, variable-speed page sizes (15 → 30 → 50 → 100):
114
+
115
+ ```ruby
116
+ def index
117
+ set_page_and_extract_portion_from Current.account.cards.active.ordered
118
+ # Sets @page with records, page number, last? flag, next_param
119
+ end
120
+ ```
121
+
122
+ Cursor-based for large datasets (100k+ rows):
123
+
124
+ ```ruby
125
+ set_page_and_extract_portion_from Event.all,
126
+ ordered_by: { created_at: :desc, id: :desc } # O(1) seeks, no OFFSET
127
+ ```
128
+
129
+ Never use manual `limit`/`offset`. Never build a custom pagination service object.
130
+
131
+ ---
132
+
133
+ ## Error Handling and Strong Parameters
134
+
135
+ ```ruby
136
+ # Raise on missing resource — Rails rescues with 404
137
+ @card = Current.user.accessible_cards.find_by!(number: params[:card_id])
138
+
139
+ # Validation failure
140
+ def create
141
+ if @board.update(board_params)
142
+ redirect_to @board
143
+ else
144
+ render :edit, status: :unprocessable_entity
145
+ end
146
+ end
147
+
148
+ private
149
+ def card_params
150
+ params.require(:card).permit(:title, :description, :column_id, tag_ids: [])
151
+ end
152
+ ```
153
+
154
+ Never use `permit!`. Always permit only what's needed for the action.
@@ -0,0 +1,132 @@
1
+ # Rails Backend: Jobs
2
+
3
+ ActiveJob patterns — ultra-thin jobs, _now/_later naming, automatic multi-tenancy, retry logic.
4
+
5
+ ---
6
+
7
+ ## Core Rule
8
+
9
+ Jobs are thin wrappers around model methods. **All business logic belongs in models.** A job should be 3-6 lines.
10
+
11
+ ```ruby
12
+ # Good
13
+ class NotifyRecipientsJob < ApplicationJob
14
+ def perform(notifiable) = notifiable.notify_recipients
15
+
16
+ # Bad: 50 lines of logic that can't be called synchronously or tested easily
17
+ class NotifyRecipientsJob < ApplicationJob
18
+ def perform(comment)
19
+ # logic that belongs in comment.notify_recipients
20
+ end
21
+ end
22
+ ```
23
+
24
+ ---
25
+
26
+ ## The _now/_later Pattern
27
+
28
+ For every async operation, create a matched pair on the model plus a job class:
29
+
30
+ ```ruby
31
+ # In the model/concern:
32
+ module Notifiable
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ after_create_commit :notify_recipients_later # Trigger async after commit
37
+ end
38
+
39
+ def notify_recipients # Synchronous — call from anywhere
40
+ Notifier.for(self)&.notify
41
+ end
42
+
43
+ private
44
+ def notify_recipients_later # Async wrapper
45
+ NotifyRecipientsJob.perform_later self
46
+ end
47
+ end
48
+
49
+ # Job:
50
+ class NotifyRecipientsJob < ApplicationJob
51
+ def perform(notifiable) = notifiable.notify_recipients
52
+ end
53
+ ```
54
+
55
+ ### Why
56
+
57
+ ```ruby
58
+ # Async vs sync is explicit:
59
+ comment.notify_recipients # Synchronous — use in console, tests, other methods
60
+ comment.notify_recipients_later # Async — enqueues job
61
+
62
+ # Synchronous method enables:
63
+ # - Fast unit tests (no queue)
64
+ # - Rails console usage
65
+ # - Calling from other model methods
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Job Examples
71
+
72
+ ```ruby
73
+ class Webhook::DeliveryJob < ApplicationJob
74
+ queue_as :webhooks
75
+ def perform(delivery) = delivery.deliver
76
+ end
77
+
78
+ class ExportAccountDataJob < ApplicationJob
79
+ queue_as :backend
80
+ def perform(export) = export.build
81
+ end
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Multi-Tenancy in Jobs
87
+
88
+ A global `ActiveJob` extension handles tenant context transparently. **Never pass account manually.**
89
+
90
+ ```ruby
91
+ # Don't do this:
92
+ SomeJob.perform_later(record, account: Current.account)
93
+
94
+ # Do this — account captured automatically at enqueue time:
95
+ SomeJob.perform_later(record)
96
+ ```
97
+
98
+ How it works:
99
+ - **On enqueue**: captures `Current.account` (serialized as GlobalID)
100
+ - **On execute**: wraps `perform` in `Current.with_account(@account) { ... }`
101
+
102
+ All queries inside `perform` have `Current.account` set correctly. Jobs are always enqueued `after_transaction_commit` — prevents jobs for rolled-back records.
103
+
104
+ ---
105
+
106
+ ## Recurring Jobs
107
+
108
+ ```yaml
109
+ # config/recurring.yml
110
+ auto_postpone_cards:
111
+ schedule: "50 * * * *" # Every hour at :50
112
+ command: "Card.auto_postpone_all_due"
113
+ ```
114
+
115
+ Commands call class methods on models — consistent with keeping logic in models.
116
+
117
+ ---
118
+
119
+ ## Retry Logic
120
+
121
+ ```ruby
122
+ class ImportJob < ApplicationJob
123
+ retry_on Net::TimeoutError, wait: :polynomially_longer, attempts: 5
124
+ discard_on ActiveJob::DeserializationError # Record deleted before job ran
125
+
126
+ def perform(import) = import.run
127
+ end
128
+ ```
129
+
130
+ - `retry_on` for transient failures (network timeouts, rate limits)
131
+ - `discard_on` for permanent failures where retrying is pointless
132
+ - Keep retry configuration on the job class; keep the work in model methods
@@ -0,0 +1,213 @@
1
+ # Rails Backend: Models
2
+
3
+ ActiveRecord patterns, concern architecture, associations, scoping, and callbacks.
4
+
5
+ ---
6
+
7
+ ## Concern Architecture
8
+
9
+ Concerns are the most distinctive pattern. Models compose behavior from focused modules.
10
+
11
+ **Shared concerns** (`app/models/concerns/`) — reusable across 3+ unrelated models, adjective naming: `Eventable`, `Notifiable`, `Searchable`, `Attachments`
12
+
13
+ **Model-specific concerns** (`app/models/card/`, `app/models/board/`) — namespaced, tightly coupled: `Card::Closeable`, `Card::Golden`, `Board::Accessible`
14
+
15
+ ### Anatomy
16
+
17
+ ```ruby
18
+ module Card::Closeable
19
+ extend ActiveSupport::Concern # Required first line
20
+
21
+ included do
22
+ has_one :closure, dependent: :destroy
23
+ scope :closed, -> { joins(:closure) }
24
+ scope :open, -> { where.missing(:closure) }
25
+ end
26
+
27
+ def closed? = closure.present?
28
+ def open? = !closed?
29
+
30
+ def close(user: Current.user)
31
+ unless closed?
32
+ transaction do
33
+ create_closure! user: user
34
+ track_event :closed, creator: user
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+ # Private helpers indented under private
41
+ end
42
+ ```
43
+
44
+ ### Concern Composition
45
+
46
+ ```ruby
47
+ class Card < ApplicationRecord
48
+ include Assignable, Attachments, Closeable, Eventable, Golden, Mentions,
49
+ Notifiable, Postponable, Searchable, Storage::Tracked, Taggable, Watchable
50
+
51
+ belongs_to :board
52
+ belongs_to :account, default: -> { board.account } # board declared first
53
+ belongs_to :creator, class_name: "User", default: -> { Current.user }
54
+ end
55
+ ```
56
+
57
+ ### When to Create
58
+
59
+ - **Shared concern**: 3+ unrelated models, cross-cutting behavior (events, search, notifications)
60
+ - **Model-specific**: 50+ lines of cohesive behavior for one model feature
61
+ - **Don't create**: 1-2 simple methods, just grouping unrelated methods
62
+
63
+ ### Template Method Pattern
64
+
65
+ Base concerns define override points; model-specific concerns customize them:
66
+
67
+ ```ruby
68
+ module Eventable
69
+ private
70
+ def should_track_event? = true # Override point
71
+ def eventable_prefix = self.class.name.demodulize.underscore # Override point
72
+ end
73
+
74
+ module Card::Eventable
75
+ extend ActiveSupport::Concern
76
+ include ::Eventable
77
+ private
78
+ def should_track_event? = published? # Override: only track published cards
79
+ end
80
+ ```
81
+
82
+ ### Concerns Delegate Complexity to Plain Ruby Objects
83
+
84
+ ```ruby
85
+ module Notifiable
86
+ included do
87
+ after_create_commit :notify_recipients_later
88
+ end
89
+ def notify_recipients = Notifier.for(self)&.notify # Delegates to Notifier hierarchy
90
+ end
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Intention-Revealing APIs
96
+
97
+ Always provide both query forms, use imperative verbs, delegate for readability:
98
+
99
+ ```ruby
100
+ def closed? = closure.present? # Positive check
101
+ def open? = !closed? # Negative check
102
+
103
+ def close / def reopen # not: set_closed / remove_closure
104
+ def gild / def ungild # not: make_golden / remove_golden
105
+
106
+ def closed_by = closure&.user # Delegates — not: closure&.user in callers
107
+ def closed_at = closure&.created_at
108
+ ```
109
+
110
+ Multi-step operations: transactions wrap everything; async ops go outside:
111
+
112
+ ```ruby
113
+ def close(user: Current.user)
114
+ transaction do
115
+ create_closure! user: user
116
+ track_event :closed, creator: user # Inside transaction
117
+ end
118
+ end
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Smart Association Defaults
124
+
125
+ ```ruby
126
+ belongs_to :board # Declare before use
127
+ belongs_to :account, default: -> { board.account } # Derives from parent
128
+ belongs_to :creator, class_name: "User", default: -> { Current.user }
129
+ # Card.create!(board:, title:) — account and creator set automatically
130
+ ```
131
+
132
+ Use for `account` (multi-tenancy) and `creator`/`user` (from `Current.user`).
133
+
134
+ ---
135
+
136
+ ## Scopes That Tell Stories
137
+
138
+ Business names, not SQL names. Always use `-> { }` lambdas:
139
+
140
+ ```ruby
141
+ scope :closed, -> { joins(:closure) }
142
+ scope :open, -> { where.missing(:closure) }
143
+ scope :closed_by, ->(users) { closed.where(closures: { user_id: Array(users) }) }
144
+
145
+ # Conditional scope maps UI concepts — keeps conditionals out of controllers:
146
+ scope :indexed_by, ->(index) do
147
+ case index
148
+ when "closed" then closed
149
+ when "golden" then golden
150
+ else all
151
+ end
152
+ end
153
+
154
+ # Named preloading scope — prevents N+1:
155
+ scope :preloaded, -> {
156
+ preload(:column, :tags, :closure, :goldness, board: [:entropy, :columns])
157
+ .with_rich_text_description_and_embeds
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Callbacks
164
+
165
+ **Use for:** required data on create, async triggers, touching associations.
166
+ **Don't use for:** business logic, complex orchestration, anything callers want to skip.
167
+
168
+ ```ruby
169
+ before_create :assign_number # Set required data
170
+ after_create_commit :notify_recipients_later # After commit (not after_create)
171
+ after_save -> { board.touch }, if: :published? # Simple one-liners as lambdas
172
+ after_update :handle_board_change, if: :saved_change_to_board_id? # Conditional
173
+ ```
174
+
175
+ Use `_commit` variants for async jobs — prevents running for rolled-back records.
176
+
177
+ ---
178
+
179
+ ## Event Tracking
180
+
181
+ Call `track_event` inside transactions; auto-prefixed by model name:
182
+
183
+ ```ruby
184
+ transaction do
185
+ create_closure! user: user
186
+ track_event :closed, creator: user # Produces "card_closed"
187
+ end
188
+
189
+ transaction do
190
+ update!(board: new_board)
191
+ track_event "board_changed", particulars: { old_board: name_was, new_board: new_board.name }
192
+ end
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Concern Reference
198
+
199
+ | Shared | Key API |
200
+ |--------|---------|
201
+ | `Eventable` | `track_event(action, **particulars)` |
202
+ | `Notifiable` | `notify_recipients`, `notify_recipients_later` |
203
+ | `Searchable` | auto-indexes on save |
204
+ | `Storage::Tracked` | auto-tracks on attachment changes |
205
+
206
+ | Card | Key API |
207
+ |------|---------|
208
+ | `Card::Closeable` | `close`, `reopen`, `closed?`, `open?` |
209
+ | `Card::Golden` | `gild`, `ungild`, `golden?` |
210
+ | `Card::Assignable` | `assign(user)`, `unassign(user)` |
211
+ | `Card::Postponable` | `postpone`, `resume`, `auto_postpone` |
212
+ | `Card::Triageable` | `triage_into(column)`, `send_back_to_triage` |
213
+ | `Card::Watchable` | `watch`, `unwatch`, `watched_by?(user)` |