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.
- checksums.yaml +4 -4
- data/ariadna.gemspec +0 -1
- data/data/agents/ariadna-codebase-mapper.md +34 -722
- data/data/agents/ariadna-debugger.md +44 -1139
- data/data/agents/ariadna-executor.md +75 -396
- data/data/agents/ariadna-planner.md +78 -1215
- data/data/agents/ariadna-roadmapper.md +55 -582
- data/data/agents/ariadna-verifier.md +60 -702
- data/data/ariadna/templates/config.json +8 -33
- data/data/ariadna/workflows/debug.md +28 -0
- data/data/ariadna/workflows/execute-phase.md +31 -513
- data/data/ariadna/workflows/map-codebase.md +20 -319
- data/data/ariadna/workflows/new-milestone.md +20 -365
- data/data/ariadna/workflows/new-project.md +19 -880
- data/data/ariadna/workflows/plan-phase.md +24 -443
- data/data/ariadna/workflows/progress.md +20 -376
- data/data/ariadna/workflows/quick.md +19 -221
- data/data/ariadna/workflows/roadmap-ops.md +28 -0
- data/data/ariadna/workflows/verify-work.md +23 -560
- data/data/commands/ariadna/add-phase.md +11 -22
- data/data/commands/ariadna/debug.md +11 -143
- data/data/commands/ariadna/execute-phase.md +12 -30
- data/data/commands/ariadna/insert-phase.md +7 -14
- data/data/commands/ariadna/map-codebase.md +16 -49
- data/data/commands/ariadna/new-milestone.md +12 -25
- data/data/commands/ariadna/new-project.md +22 -26
- data/data/commands/ariadna/plan-phase.md +13 -22
- data/data/commands/ariadna/progress.md +16 -6
- data/data/commands/ariadna/quick.md +9 -11
- data/data/commands/ariadna/remove-phase.md +9 -12
- data/data/commands/ariadna/verify-work.md +14 -19
- data/data/skills/rails-backend/API.md +138 -0
- data/data/skills/rails-backend/CONTROLLERS.md +154 -0
- data/data/skills/rails-backend/JOBS.md +132 -0
- data/data/skills/rails-backend/MODELS.md +213 -0
- data/data/skills/rails-backend/SKILL.md +169 -0
- data/data/skills/rails-frontend/ASSETS.md +154 -0
- data/data/skills/rails-frontend/COMPONENTS.md +253 -0
- data/data/skills/rails-frontend/SKILL.md +187 -0
- data/data/skills/rails-frontend/VIEWS.md +168 -0
- data/data/skills/rails-performance/PROFILING.md +106 -0
- data/data/skills/rails-performance/SKILL.md +217 -0
- data/data/skills/rails-security/AUDIT.md +118 -0
- data/data/skills/rails-security/SKILL.md +422 -0
- data/data/skills/rails-testing/FIXTURES.md +78 -0
- data/data/skills/rails-testing/SKILL.md +160 -0
- data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
- data/lib/ariadna/installer.rb +11 -15
- data/lib/ariadna/tools/cli.rb +0 -12
- data/lib/ariadna/tools/config_manager.rb +10 -72
- data/lib/ariadna/tools/frontmatter.rb +23 -1
- data/lib/ariadna/tools/init.rb +201 -401
- data/lib/ariadna/tools/model_profiles.rb +6 -14
- data/lib/ariadna/tools/phase_manager.rb +1 -10
- data/lib/ariadna/tools/state_manager.rb +170 -451
- data/lib/ariadna/tools/template_filler.rb +4 -12
- data/lib/ariadna/tools/verification.rb +21 -399
- data/lib/ariadna/uninstaller.rb +9 -0
- data/lib/ariadna/version.rb +1 -1
- data/lib/ariadna.rb +1 -0
- metadata +20 -91
- data/data/agents/ariadna-backend-executor.md +0 -261
- data/data/agents/ariadna-frontend-executor.md +0 -259
- data/data/agents/ariadna-integration-checker.md +0 -418
- data/data/agents/ariadna-phase-researcher.md +0 -469
- data/data/agents/ariadna-plan-checker.md +0 -622
- data/data/agents/ariadna-project-researcher.md +0 -618
- data/data/agents/ariadna-research-synthesizer.md +0 -236
- data/data/agents/ariadna-test-executor.md +0 -266
- data/data/ariadna/references/checkpoints.md +0 -772
- data/data/ariadna/references/continuation-format.md +0 -249
- data/data/ariadna/references/decimal-phase-calculation.md +0 -65
- data/data/ariadna/references/git-integration.md +0 -248
- data/data/ariadna/references/git-planning-commit.md +0 -38
- data/data/ariadna/references/model-profile-resolution.md +0 -32
- data/data/ariadna/references/model-profiles.md +0 -73
- data/data/ariadna/references/phase-argument-parsing.md +0 -61
- data/data/ariadna/references/planning-config.md +0 -194
- data/data/ariadna/references/questioning.md +0 -153
- data/data/ariadna/references/rails-conventions.md +0 -416
- data/data/ariadna/references/tdd.md +0 -267
- data/data/ariadna/references/ui-brand.md +0 -160
- data/data/ariadna/references/verification-patterns.md +0 -853
- data/data/ariadna/templates/codebase/architecture.md +0 -481
- data/data/ariadna/templates/codebase/concerns.md +0 -380
- data/data/ariadna/templates/codebase/conventions.md +0 -434
- data/data/ariadna/templates/codebase/integrations.md +0 -328
- data/data/ariadna/templates/codebase/stack.md +0 -189
- data/data/ariadna/templates/codebase/structure.md +0 -418
- data/data/ariadna/templates/codebase/testing.md +0 -606
- data/data/ariadna/templates/context.md +0 -283
- data/data/ariadna/templates/continue-here.md +0 -78
- data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
- data/data/ariadna/templates/phase-prompt.md +0 -609
- data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
- data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
- data/data/ariadna/templates/research-project/FEATURES.md +0 -168
- data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
- data/data/ariadna/templates/research-project/STACK.md +0 -251
- data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
- data/data/ariadna/templates/state.md +0 -176
- data/data/ariadna/templates/summary-complex.md +0 -59
- data/data/ariadna/templates/summary-minimal.md +0 -41
- data/data/ariadna/templates/summary-standard.md +0 -48
- data/data/ariadna/templates/user-setup.md +0 -310
- data/data/ariadna/workflows/add-phase.md +0 -111
- data/data/ariadna/workflows/add-todo.md +0 -157
- data/data/ariadna/workflows/audit-milestone.md +0 -241
- data/data/ariadna/workflows/check-todos.md +0 -176
- data/data/ariadna/workflows/complete-milestone.md +0 -644
- data/data/ariadna/workflows/diagnose-issues.md +0 -219
- data/data/ariadna/workflows/discovery-phase.md +0 -289
- data/data/ariadna/workflows/discuss-phase.md +0 -408
- data/data/ariadna/workflows/execute-plan.md +0 -448
- data/data/ariadna/workflows/help.md +0 -470
- data/data/ariadna/workflows/insert-phase.md +0 -129
- data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
- data/data/ariadna/workflows/pause-work.md +0 -122
- data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
- data/data/ariadna/workflows/remove-phase.md +0 -154
- data/data/ariadna/workflows/research-phase.md +0 -74
- data/data/ariadna/workflows/resume-project.md +0 -306
- data/data/ariadna/workflows/set-profile.md +0 -80
- data/data/ariadna/workflows/settings.md +0 -145
- data/data/ariadna/workflows/transition.md +0 -493
- data/data/ariadna/workflows/update.md +0 -212
- data/data/ariadna/workflows/verify-phase.md +0 -226
- data/data/commands/ariadna/add-todo.md +0 -42
- data/data/commands/ariadna/audit-milestone.md +0 -42
- data/data/commands/ariadna/check-todos.md +0 -41
- data/data/commands/ariadna/complete-milestone.md +0 -136
- data/data/commands/ariadna/discuss-phase.md +0 -86
- data/data/commands/ariadna/help.md +0 -22
- data/data/commands/ariadna/list-phase-assumptions.md +0 -50
- data/data/commands/ariadna/pause-work.md +0 -35
- data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
- data/data/commands/ariadna/reapply-patches.md +0 -110
- data/data/commands/ariadna/research-phase.md +0 -187
- data/data/commands/ariadna/resume-work.md +0 -40
- data/data/commands/ariadna/set-profile.md +0 -34
- data/data/commands/ariadna/settings.md +0 -36
- data/data/commands/ariadna/update.md +0 -37
- data/data/guides/backend.md +0 -3069
- data/data/guides/frontend.md +0 -1479
- data/data/guides/performance.md +0 -1193
- data/data/guides/security.md +0 -1522
- data/data/guides/style-guide.md +0 -1091
- data/data/guides/testing.md +0 -504
- data/data/templates.md +0 -94
|
@@ -1,39 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ariadna:verify-work
|
|
3
|
-
description: Validate built features
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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)` |
|