studio-engine 0.69.3 → 0.69.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ade07348f1d55403ac332ea2a030b8af81ff8b9b8d4a49b637326eb40168847
4
- data.tar.gz: e5ee5053749871945628e8f522fffee01569e0ae3052016b9388c79b98be59d4
3
+ metadata.gz: 44ac04785253cb277ef70cd1d6398b81718e223adc7e300c1a19329183a88be5
4
+ data.tar.gz: 01c13c3974eb5da7887fc82e77f3526eb50af53a3e590b993a7602914e8daa81
5
5
  SHA512:
6
- metadata.gz: baf91b4f29446f4cc53ef15890ba67e9691046c769af46ac3beec04f9ad530ad3f67ee9d4d686d85d37ed3b0f7e78c1e453eec0476e4146eaa97bbd8fdf4fd68
7
- data.tar.gz: 5ae6368f5e8c1ba95081f8608acc3193e3b82436e42e6a2cef059a6e979fb1b025da56058f980bc544f7d0add642d2972d7aefcc5b37c1fda22ca65c71c269cf
6
+ metadata.gz: db96cbe447a67732cc7d2babc54f605071ccd9e3d77d20da4fb7486c5e89f5354e21dc34066757ec7bde7f66fb7fb08e257a15c364753d3a98f34ea8065df25b
7
+ data.tar.gz: f8a47251bf78b0c3bab42dfc743565c2dba66502f6f204516718144cd9506a5206bf6342b7ce496fa12f6f67a34727dc8fc7cbc5e03529ce5d3ab1e3727b4761
data/CHANGELOG.md CHANGED
@@ -55,6 +55,91 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
55
55
 
56
56
  ### Fixed
57
57
 
58
+ - **Onboarding no longer truncates a surname to fit a first name's cap.**
59
+ `Studio::OnboardingController#first_name` measured the WHOLE typed answer with
60
+ `Studio::FIRST_NAME_MAX_LENGTH` — the PER-FIELD cap, 40. The field asks for a
61
+ name and people type a full one, so "Bartholomew Fitzwilliam
62
+ Montgomery-Smythe" (41 characters) stored 40 of them and handed the account
63
+ back its own surname misspelled, **"Montgomery-Smyth"**, on every surface
64
+ `name` feeds.
65
+
66
+ **ONE CONSTANT WAS ANSWERING TWO QUESTIONS**, which is why neither call site
67
+ looked wrong. `/profile` applies the same number PER FIELD (`normalized_name`,
68
+ once to each of its two inputs); onboarding applied it to first name PLUS
69
+ surname in one string. The fix NAMES the second question rather than
70
+ re-scoping the first: **`Studio::FULL_NAME_MAX_LENGTH`**, the whole-answer
71
+ cap, `(FIRST_NAME_MAX_LENGTH * 2) + 1` = **81**. It is DERIVED, not picked —
72
+ first(40) + a space + last(40) is the longest answer whose two halves BOTH
73
+ still fit the per-field cap, so onboarding can never accept a name `/profile`
74
+ would silently shorten on the next save, which is the drift the shared
75
+ constant existed to prevent. `FIRST_NAME_MAX_LENGTH` keeps its value and its
76
+ meaning, and `test/integration/profile_name_field_caps_test.rb` now proves
77
+ that by sending BOTH fields over-long in one request and reading two 40s back
78
+ — a single-field assertion cannot tell "per field" from "per whole name".
79
+
80
+ **PAST THE CAP IT REFUSES INSTEAD OF TRUNCATING** (422 + `{ error: … }`,
81
+ rendered inline by the modal). `.first(cap)` on a name is not a cap but a
82
+ rewrite: it accepted the answer, stored something nobody typed, and reported
83
+ success — and raising the number alone would only move that cliff one
84
+ character further out. Two independent rules, because the whole answer becomes
85
+ `name` while each half becomes its own column: the answer must fit the
86
+ whole-answer cap, and each derived half must fit the per-field cap (a single
87
+ 60-character word passes the first and would still land a `first_name`
88
+ `/profile` shortens). The onboarding input's `maxlength` default was a LITERAL
89
+ `40` — a third copy of the per-field number, on the one field that receives a
90
+ whole name — and now reads `FULL_NAME_MAX_LENGTH`, so the browser bound and
91
+ the server bound cannot drift.
92
+
93
+ **NEITHER CAP IS A COLUMN CEILING.** Measured 2026-09-06 across all six
94
+ consumer databases (mcritchie-studio, turf-monster, mcritchie-industries;
95
+ production and QA): `name`, `first_name` and `last_name` are unbounded
96
+ `character varying` — `character_maximum_length` NULL, no CHECK constraint, no
97
+ model length validation. Raising the bound cannot trade a silent truncation
98
+ for a raised error at any value, so this is a product decision start to
99
+ finish.
100
+
101
+ **NO DATA REPAIR SHIPS.** Re-measured for THIS defect on the same six
102
+ databases: **zero rows** carry a `name`, `first_name` or `last_name` at or
103
+ over 40 characters, and the longest `name` in the fleet is 27 (control:
104
+ 5 / 6 / 46 / 15 / 4 / 4 users scanned, `max(length(name))` non-null in every
105
+ one). Nothing was truncated in production, so there is nothing to repair — and
106
+ a row whose halves disagree with its `name` because of a `/profile` edit must
107
+ never be auto-repaired anyway.
108
+
109
+ - **Onboarding no longer stores a two-word answer as the whole first name.**
110
+ `Studio::OnboardingController#first_name` writes with `update_columns`, which
111
+ skips callbacks — so the host's `before_save :set_name_parts` never ran and
112
+ someone answering "Ada Lovelace" landed `first_name = "Ada Lovelace"`,
113
+ `last_name` NULL. It never self-healed either: `set_name_parts` is gated on
114
+ `name_changed?`, and no later save sees a name change. The derivation now
115
+ comes to the writer instead — a new pure primitive, **`Studio::NameParts.from`**
116
+ (`lib/studio/name_parts.rb`), carrying the same rule the host callback runs,
117
+ including the deliberate OMISSION of `last_name` for a one-word name so a
118
+ surname already on file survives a first-name prompt.
119
+ **`update_columns` STAYS**, and the fix does not touch it: `Sluggable`'s
120
+ `before_save :set_slug` is UNGATED and mcritchie-studio's `User#name_slug` is
121
+ built from `name`, so a full save here would re-point the slug the account
122
+ answers on — on a uniquely-indexed column, at every signup.
123
+ `test/integration/onboarding_name_parts_test.rb` carries that as a live
124
+ control (`update!` moves `pat-pat@example.com` → `ada-lovelace-pat@…`), not as
125
+ a claim. `last_name` is written only where the column exists: the engine's own
126
+ standard-columns migration does not carry it, so a host that opts into these
127
+ routes before it runs that column would otherwise take a
128
+ `MissingAttributeError` 500 on every two-word answer
129
+ (`test/integration/onboarding_thin_host_test.rb`). **Corrected 2026-09-06:**
130
+ an earlier version of this entry named mcritchie-industries, moms-app and
131
+ acquisition-studio as apps that would have taken that 500. They carry no
132
+ `last_name`, but they do not mount these endpoints at all —
133
+ `Studio.draw_onboarding_routes` defaults to false and only mcritchie-studio
134
+ and turf-monster set it true, and both of those HAVE the column. The guard is
135
+ a standing one for the next host to opt in, not a live save.
136
+ NO DATA REPAIR SHIPS: measured 2026-09-05 across both production databases
137
+ (mcritchie-studio 5 users, turf-monster 43) and both QA ones, **zero rows**
138
+ carry a space in `first_name`. Repairing rows whose halves merely disagree
139
+ with `name` would be data loss — `/profile` writes those columns directly —
140
+ and a fossil repair belongs to the consuming app that owns its `users` table,
141
+ not to the gem.
142
+
58
143
  - **Knowledge layer hardening** — the five findings from the 0.67.0 reviews:
59
144
  upload keys carry a random suffix (same-named uploads in the same second no
60
145
  longer overwrite the first object before the unique index can refuse);
@@ -23,36 +23,72 @@ module Studio
23
23
  # Both actions are for the freshly signed-in user, so authentication is the
24
24
  # host's default require_authentication — no skip_before_action here.
25
25
 
26
- # The shared cap (Studio::FIRST_NAME_MAX_LENGTH). Kept under the old name
27
- # here because it is referenced from outside this class; what changed is that
28
- # it is no longer a SECOND definition of the same number. /profile writes the
29
- # same column and now reads the same constant.
26
+ # The PER-FIELD cap (Studio::FIRST_NAME_MAX_LENGTH) what one derived half
27
+ # may be. Kept under the old name here because it is referenced from outside
28
+ # this class; what changed is that it is no longer a SECOND definition of
29
+ # the same number. /profile writes the same columns and reads the same
30
+ # constant.
30
31
  MAX_FIRST_NAME = Studio::FIRST_NAME_MAX_LENGTH
31
32
 
33
+ # The WHOLE-ANSWER cap (Studio::FULL_NAME_MAX_LENGTH) — what this endpoint
34
+ # actually bounds. It is a different question from the one above and now has
35
+ # a different name: the field asks for a name and people type a full one, so
36
+ # the string arriving here is first name PLUS surname, and measuring it with
37
+ # the per-field number stored 40 characters of a 41-character name and
38
+ # handed the account back a misspelled surname.
39
+ MAX_FULL_NAME = Studio::FULL_NAME_MAX_LENGTH
40
+
32
41
  # POST /onboarding/first_name
33
42
  #
34
43
  # Writes users.first_name, and backfills `name` when it is blank so the
35
44
  # display-name chain has something better than an email prefix to show.
36
45
  def first_name
37
- value = params[:first_name].to_s.strip.gsub(/\s+/, " ").first(MAX_FIRST_NAME)
46
+ value = params[:first_name].to_s.strip.gsub(/\s+/, " ")
38
47
 
39
48
  if value.blank?
40
- return render json: { ok: false, error: "Enter your first name, or skip for now." },
41
- status: :unprocessable_entity
49
+ return refuse("Enter your first name, or skip for now.")
50
+ end
51
+
52
+ # DERIVE FIRST, THEN GUARD, THEN WRITE. The halves are what the length
53
+ # rules are about, so they are computed once and both the guard and the
54
+ # write read the same hash — a guard measuring a second derivation could
55
+ # pass over values the write then stored.
56
+ attrs = name_columns(value)
57
+
58
+ if (message = length_refusal(value, attrs))
59
+ return refuse(message)
42
60
  end
43
61
 
44
62
  rescue_and_log(target: current_user) do
45
- # update_columns, not update!: this runs seconds after signup, on an
46
- # account that may be mid-onboarding, and a validation failure elsewhere
47
- # on the record (a grandfathered reserved username, say) must not block a
48
- # first name. It also steps around any host before_save that DERIVES
49
- # first_name FROM name turf's set_name_parts does exactly that, and
50
- # writing through it would discard the value we were just handed.
51
- attrs = { first_name: value }
63
+ # update_columns, not update!, for THREE reasons all still true:
64
+ #
65
+ # 1. This runs seconds after signup, on an account that may be
66
+ # mid-onboarding, and a validation failure elsewhere on the record
67
+ # (a grandfathered reserved username, say) must not block a name.
68
+ # 2. It steps around any host before_save that DERIVES first_name FROM
69
+ # name set_name_parts does exactly that — which would discard the
70
+ # value we were just handed.
71
+ # 3. THE SLUG. Sluggable's `before_save :set_slug` is UNGATED, and
72
+ # mcritchie-studio's User#name_slug is built from `name`. A full
73
+ # save right after this writes `name` would therefore re-point the
74
+ # slug the account answers on — a URL change, on a column carrying
75
+ # a unique index, for every signup. That is the constraint this
76
+ # endpoint has always been protecting; it is not a shortcut.
77
+ #
78
+ # WHAT SKIPPING CALLBACKS USED TO COST. Because set_name_parts never
79
+ # ran, `first_name` got the WHOLE typed value: someone answering "Ada
80
+ # Lovelace" landed first_name="Ada Lovelace", last_name NULL — and it
81
+ # never self-healed, because set_name_parts is gated on `name_changed?`
82
+ # and no later save sees a name change. So the derivation comes to the
83
+ # writer instead: Studio::NameParts is the same rule the callback runs.
84
+ # (`attrs` was derived above, so the guard and this write cannot disagree.)
52
85
  attrs[:name] = value if current_user.name.blank?
53
86
  current_user.update_columns(attrs)
54
87
 
55
- render json: { ok: true, first_name: value, next: remaining_steps }
88
+ # The STORED first name, not the typed string — they differ for exactly
89
+ # the case above, and a response that disagreed with the row would be
90
+ # the same bug wearing a different hat.
91
+ render json: { ok: true, first_name: attrs[:first_name], next: remaining_steps }
56
92
  end
57
93
  end
58
94
 
@@ -68,6 +104,63 @@ module Studio
68
104
 
69
105
  private
70
106
 
107
+ # The one refusal shape both guards answer with. The modal renders `error`
108
+ # inline and leaves the field filled, so a refusal here is a correction the
109
+ # person can act on rather than a dead end.
110
+ def refuse(message)
111
+ render json: { ok: false, error: message }, status: :unprocessable_entity
112
+ end
113
+
114
+ # WHY THIS REFUSES INSTEAD OF TRUNCATING, which is the product decision this
115
+ # endpoint used to make silently and badly.
116
+ #
117
+ # `.first(cap)` on a name is not a cap, it is a rewrite: it accepted the
118
+ # answer, stored something the person never typed, and reported success.
119
+ # "Bartholomew Fitzwilliam Montgomery-Smythe" came back as
120
+ # "…Montgomery-Smyth" — plausible enough to miss, which is the worst kind of
121
+ # wrong for a name. Raising the number alone only moves that cliff one
122
+ # character further out; a bound that rewrites its input is the defect at
123
+ # any value. So the bound goes up (MAX_FULL_NAME now fits a real full name)
124
+ # AND anything past it is refused with a sentence saying so.
125
+ #
126
+ # BOTH LENGTHS ARE CHECKED, and they are independent rather than redundant.
127
+ # The whole answer becomes `name`; each half becomes its own column that
128
+ # /profile caps separately. A single 60-character word passes the first rule
129
+ # and would still land a `first_name` /profile silently shortens on the next
130
+ # save — the very drift the two constants exist to prevent — so the halves
131
+ # are measured too.
132
+ #
133
+ # UNREACHABLE FROM THE MODAL, ON PURPOSE. The partial's input carries the
134
+ # same MAX_FULL_NAME as its maxlength, so the browser stops a typist first.
135
+ # This is the server being the real bound, for a direct POST or for a host
136
+ # that passes a larger `max_length` local.
137
+ #
138
+ # Returns the message to refuse with, or nil to proceed.
139
+ def length_refusal(value, attrs)
140
+ if value.length > MAX_FULL_NAME
141
+ return "Please shorten that to #{MAX_FULL_NAME} characters or fewer."
142
+ end
143
+
144
+ if attrs.values.compact.any? { |part| part.length > MAX_FIRST_NAME }
145
+ return "Please keep each name to #{MAX_FIRST_NAME} characters or fewer."
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ # The name halves to write, derived exactly as the host's set_name_parts
152
+ # would derive them from the same string.
153
+ #
154
+ # `last_name` is dropped for a host that has no such column —
155
+ # mcritchie-industries' users table is eight columns wide and update_columns
156
+ # on an absent column raises. Same respond_to? guard /profile already
157
+ # carries for the same reason.
158
+ def name_columns(value)
159
+ parts = Studio::NameParts.from(value)
160
+ parts.delete(:last_name) unless current_user.respond_to?(:last_name)
161
+ parts
162
+ end
163
+
71
164
  # What is left AFTER this write. The host's resolver decides; the engine's
72
165
  # default is "nothing further", which is the right answer for an app whose
73
166
  # only onboarding ask is the name.
@@ -15,7 +15,10 @@
15
15
  heading — (default "What should we call you?")
16
16
  subtext — the one-line why (default speaks about emails)
17
17
  placeholder — (default "Alex")
18
- max_length — (default 40; the server stays the real bound)
18
+ max_length — (default Studio::FULL_NAME_MAX_LENGTH; the server stays the
19
+ real bound, and it REFUSES past that rather than truncating,
20
+ so a host raising this makes the endpoint answer 422 rather
21
+ than store a name nobody typed)
19
22
  progress — [current, total] to render the segmented pill, or nil for none
20
23
  modal_store — Alpine store name (default "modals"; the living style guide
21
24
  mounts its own page-scoped host and passes "dsModals")
@@ -41,7 +44,7 @@
41
44
  subtext = local_assigns.fetch(:subtext,
42
45
  "Just your first name — we use it to address you in emails.")
43
46
  placeholder = local_assigns.fetch(:placeholder, "Alex")
44
- max_length = local_assigns.fetch(:max_length, 40)
47
+ max_length = local_assigns.fetch(:max_length, Studio::FULL_NAME_MAX_LENGTH)
45
48
  progress = local_assigns.fetch(:progress, nil)
46
49
  modal_store = local_assigns.fetch(:modal_store, "modals")
47
50
  done_event = local_assigns.fetch(:done_event, "onboarding-step-done")
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ # The two halves of a person's name, derived from the one string they typed.
5
+ #
6
+ # WHY THIS IS A PRIMITIVE AND NOT THREE LINES IN A CONTROLLER. Every Studio
7
+ # host derives `first_name`/`last_name` from `name` in a `before_save`
8
+ # (`set_name_parts` — byte-identical in mcritchie-studio and turf-monster), so
9
+ # any writer that steps around callbacks owes the SAME derivation or the row
10
+ # lands split differently depending on which door it came through. The engine
11
+ # has exactly such a writer: Studio::OnboardingController writes with
12
+ # `update_columns`, and it does so for a reason worth keeping (see there).
13
+ # Parity is the whole requirement, so the rule lives in one named, tested
14
+ # place instead of being retyped at each door.
15
+ #
16
+ # PURE ON PURPOSE — no record, no columns, no ActiveRecord. That is what lets
17
+ # the engine's pure-Ruby unit lane exercise it directly, and what lets a host
18
+ # callback delegate to it later without inheriting anything.
19
+ #
20
+ # `last_name` is OMITTED, not nil, for a one-word name. That is parity, not
21
+ # tidiness: the host callback has always left an existing last name standing
22
+ # (`self.last_name = parts.last if parts.size > 1`), so a hash that nulled it
23
+ # would make the callback-free writer disagree with the callback in the
24
+ # opposite direction — someone with a last name on file would lose it by
25
+ # answering a first-name prompt. Whether that carry-over is itself the right
26
+ # rule is a separate question from this one; matching it is this file's job.
27
+ #
28
+ # Mirrors mcritchie-studio's `User.name_parts`, which the hub extracted for its
29
+ # own callback-free writers (seeds and the identity migrations). The engine
30
+ # cannot call that one — it ships to hosts that do not define it, and
31
+ # mcritchie-industries has no `first_name` column at all — so it carries its
32
+ # own copy of a rule both must agree on. test/lib/studio/name_parts_test.rb
33
+ # pins the parity for every shape of name.
34
+ module NameParts
35
+ module_function
36
+
37
+ # A hash ready for `update_columns` / `assign_attributes`.
38
+ #
39
+ # Studio::NameParts.from("Ada Lovelace") # => { first_name: "Ada", last_name: "Lovelace" }
40
+ # Studio::NameParts.from("Ada") # => { first_name: "Ada" }
41
+ # Studio::NameParts.from("Ada B. Lovelace") # => { first_name: "Ada", last_name: "Lovelace" }
42
+ # Studio::NameParts.from("") # => { first_name: nil }
43
+ def from(name)
44
+ words = name.to_s.strip.split(" ")
45
+ parts = { first_name: words.first }
46
+ parts[:last_name] = words.last if words.size > 1
47
+ parts
48
+ end
49
+ end
50
+ end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.69.3"
2
+ VERSION = "0.69.5"
3
3
  end
data/lib/studio.rb CHANGED
@@ -14,6 +14,7 @@ require "studio/profile_image"
14
14
  require "studio/oauth_identity"
15
15
  require "studio/newsletter"
16
16
  require "studio/username_generator"
17
+ require "studio/name_parts"
17
18
  require "studio/s3"
18
19
  require "studio/image_cache"
19
20
  require "studio/link_token"
@@ -406,17 +407,46 @@ module Studio
406
407
  # later session may ask again. That is the whole reason this is not a column.
407
408
  FIRST_NAME_SKIP_SESSION_KEY = :onboarding_skipped_first_name
408
409
 
409
- # How long a first name may be. ONE constant because users.first_name is
410
- # written from TWO surfaces the onboarding step (seconds after signup) and
411
- # /profile (any time after) and rendered by a third, the profile form's
412
- # maxlength. Two independently-correct caps that disagreed would let onboarding
413
- # accept a name /profile then refused to save, a bug with no obvious owner.
410
+ # How long ONE name field may be users.first_name, and users.last_name where
411
+ # a host carries it. PER FIELD, which is the only thing this number has ever
412
+ # measured: /profile caps each of the two inputs with it, and the profile
413
+ # form's maxlength renders it. Two independently-correct caps that disagreed
414
+ # would let one surface accept a name another then refused to save, a bug with
415
+ # no obvious owner.
414
416
  #
415
417
  # Keeping it here rather than on either controller also keeps the VIEW off a
416
418
  # controller constant: the form needs the number, and a view reaching into
417
419
  # Studio::ProfilesController to get it would couple the two for no reason.
418
420
  FIRST_NAME_MAX_LENGTH = 40
419
421
 
422
+ # How long a WHOLE typed answer may be — one string holding a full name, which
423
+ # the onboarding step stores as users.name and SPLITS into the two halves
424
+ # above (Studio::NameParts).
425
+ #
426
+ # A SECOND CONSTANT BECAUSE THIS IS A SECOND QUESTION. Onboarding asked the
427
+ # per-field number for a whole-answer bound, and the two readings were both
428
+ # defensible at their own call site: "Bartholomew Fitzwilliam
429
+ # Montgomery-Smythe" is 41 characters, so it stored 40 and handed the account
430
+ # back its own surname misspelled — "Montgomery-Smyth". Nothing was wrong in
431
+ # either controller alone; the number was answering two questions with one
432
+ # value. Giving the whole-answer bound its own name makes that borrow
433
+ # unwriteable rather than merely discouraged.
434
+ #
435
+ # DERIVED, NOT PICKED. It is the longest answer whose two halves BOTH still
436
+ # fit the per-field cap — first(40) + a space + last(40) — so onboarding can
437
+ # never accept a name /profile would later shorten, which is the drift
438
+ # FIRST_NAME_MAX_LENGTH exists to prevent. Raising the per-field cap moves
439
+ # this one with it, by construction.
440
+ #
441
+ # NOT BOUNDED BY THE COLUMN. Measured 2026-09-06 across all six consumer
442
+ # databases (mcritchie-studio, turf-monster and mcritchie-industries, prod and
443
+ # QA): every one of `name`, `first_name` and `last_name` is an unbounded
444
+ # `character varying` — character_maximum_length NULL, no CHECK constraint, no
445
+ # model length validation. So this cap is a product decision start to finish,
446
+ # not a column ceiling, and raising it cannot trade a truncation for a
447
+ # PG::StringDataRightTruncation.
448
+ FULL_NAME_MAX_LENGTH = (FIRST_NAME_MAX_LENGTH * 2) + 1
449
+
420
450
  # The shared rule for "does this account still owe us a first name?" — the one
421
451
  # piece of onboarding logic every app agrees on. Hosts compose it into their own
422
452
  # flow rather than re-deriving it (turf's OnboardingFlow calls straight through).
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.69.3
4
+ version: 0.69.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -576,6 +576,7 @@ files:
576
576
  - lib/studio/link_token.rb
577
577
  - lib/studio/log_rotation.rb
578
578
  - lib/studio/mail_transport.rb
579
+ - lib/studio/name_parts.rb
579
580
  - lib/studio/newsletter.rb
580
581
  - lib/studio/oauth_identity.rb
581
582
  - lib/studio/profile_image.rb