settings_hub 0.1.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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +10 -0
  5. data/app/assets/stylesheets/settings_hub/application.css +15 -0
  6. data/app/controllers/settings_hub/api/sections_controller.rb +41 -0
  7. data/app/controllers/settings_hub/application_controller.rb +23 -0
  8. data/app/controllers/settings_hub/sections_controller.rb +78 -0
  9. data/app/controllers/settings_hub/settings_controller.rb +7 -0
  10. data/app/helpers/settings_hub/app_routes_helper.rb +12 -0
  11. data/app/helpers/settings_hub/application_helper.rb +4 -0
  12. data/app/jobs/settings_hub/application_job.rb +4 -0
  13. data/app/mailers/settings_hub/application_mailer.rb +6 -0
  14. data/app/models/settings_hub/application_record.rb +5 -0
  15. data/app/models/settings_hub/change_name.rb +15 -0
  16. data/app/views/settings_hub/sections/_profile.html.erb +6 -0
  17. data/app/views/settings_hub/settings/show.html.erb +26 -0
  18. data/config/routes.rb +11 -0
  19. data/lib/generators/settings_hub/section/section_generator.rb +43 -0
  20. data/lib/generators/settings_hub/section/templates/action.rb.tt +11 -0
  21. data/lib/generators/settings_hub/section/templates/action_test.rb.tt +9 -0
  22. data/lib/generators/settings_hub/section/templates/partial.html.erb.tt +6 -0
  23. data/lib/settings_hub/bad_registration.rb +4 -0
  24. data/lib/settings_hub/engine.rb +9 -0
  25. data/lib/settings_hub/reference/guide.md +216 -0
  26. data/lib/settings_hub/registry.rb +58 -0
  27. data/lib/settings_hub/result.rb +22 -0
  28. data/lib/settings_hub/section.rb +30 -0
  29. data/lib/settings_hub/settings_account.rb +10 -0
  30. data/lib/settings_hub/version.rb +3 -0
  31. data/lib/settings_hub.rb +36 -0
  32. data/lib/tasks/settings_hub_tasks.rake +4 -0
  33. data/the_local/agents/settings_hub-develop.md +281 -0
  34. data/the_local/agents/settings_hub-info.md +92 -0
  35. data/the_local/agents/settings_hub-install.md +145 -0
  36. data/the_local/interface.yml +57 -0
  37. metadata +110 -0
@@ -0,0 +1,58 @@
1
+ module SettingsHub
2
+ class Registry
3
+ def initialize(capabilities: nil)
4
+ @capabilities = capabilities
5
+ @sections = []
6
+ end
7
+
8
+ def add(section)
9
+ raise BadRegistration, "#{section.key} is already a section in the #{section.area} area" if taken_by_another(section)
10
+
11
+ replace(section)
12
+ end
13
+
14
+ def replace(section)
15
+ refuse_objects_the_app_cannot_find(section)
16
+ refuse_capabilities_the_app_does_not_recognise(section)
17
+ @sections.delete(taken_by_another(section))
18
+ @sections << section
19
+ section
20
+ end
21
+
22
+ def in_area(area)
23
+ @sections.select { |section| section.area == area.to_sym }
24
+ end
25
+
26
+ def find(key)
27
+ @sections.find { |section| section.key == key.to_sym }
28
+ end
29
+
30
+ def areas
31
+ @sections.group_by(&:area)
32
+ end
33
+
34
+ private
35
+
36
+ def refuse_objects_the_app_cannot_find(section)
37
+ section.actions
38
+ rescue NameError => missing
39
+ raise BadRegistration, missing.message
40
+ end
41
+
42
+ def refuse_capabilities_the_app_does_not_recognise(section)
43
+ return if section.capability.nil? || recognised_capabilities.nil?
44
+ return if recognised_capabilities.map(&:to_sym).include?(section.capability.to_sym)
45
+
46
+ raise BadRegistration, "#{section.capability} is not a capability this app recognises"
47
+ end
48
+
49
+ def recognised_capabilities
50
+ declared = @capabilities || SettingsHub.capabilities
51
+ declared.respond_to?(:call) ? declared.call : declared
52
+ end
53
+
54
+ def taken_by_another(section)
55
+ @sections.find { |existing| existing.key == section.key && existing.area == section.area }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ module SettingsHub
2
+ class Result
3
+ attr_reader :message
4
+
5
+ def self.ok
6
+ new(ok: true)
7
+ end
8
+
9
+ def self.refused(message)
10
+ new(ok: false, message: message)
11
+ end
12
+
13
+ def initialize(ok:, message: nil)
14
+ @ok = ok
15
+ @message = message
16
+ end
17
+
18
+ def ok?
19
+ @ok
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module SettingsHub
2
+ class Section
3
+ attr_reader :key, :area, :title, :renders, :capability, :runs, :at
4
+
5
+ def initialize(key:, area:, title:, renders: nil, capability: nil, runs: nil, at: nil)
6
+ @key = key.to_sym
7
+ @area = area.to_sym
8
+ @title = title
9
+ @renders = renders
10
+ @capability = capability
11
+ @runs = runs
12
+ @at = at
13
+ end
14
+
15
+ def action
16
+ actions[key]
17
+ end
18
+
19
+ def actions
20
+ return {} if runs.nil?
21
+ return { key => runs.constantize } unless runs.is_a?(Hash)
22
+
23
+ runs.transform_values(&:constantize)
24
+ end
25
+
26
+ def named_actions?
27
+ runs.is_a?(Hash)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ module SettingsHub
2
+ module SettingsAccount
3
+ def self.of(controller)
4
+ return controller.send(:settings_account) if controller.respond_to?(:settings_account, true)
5
+ return controller.send(:current_account) if controller.respond_to?(:current_account, true)
6
+
7
+ nil
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module SettingsHub
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,36 @@
1
+ require "keystone_ui"
2
+
3
+ require "settings_hub/version"
4
+ require "settings_hub/engine"
5
+ require "settings_hub/bad_registration"
6
+ require "settings_hub/registry"
7
+ require "settings_hub/section"
8
+ require "settings_hub/result"
9
+ require "settings_hub/settings_account"
10
+
11
+ module SettingsHub
12
+ class << self
13
+ attr_accessor :capabilities
14
+ end
15
+
16
+ def self.registry
17
+ @registry ||= Registry.new
18
+ end
19
+
20
+ def self.section(key, **details)
21
+ registry.add(Section.new(key: key, **details))
22
+ end
23
+
24
+ def self.replace_section(key, **details)
25
+ registry.replace(Section.new(key: key, **details))
26
+ end
27
+
28
+ def self.prepare!
29
+ @registry = nil
30
+ register_own_sections
31
+ end
32
+
33
+ def self.register_own_sections
34
+ section :profile, area: :user, title: "Profile", renders: "settings_hub/sections/profile", runs: "SettingsHub::ChangeName"
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :settings_hub do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,281 @@
1
+ ---
2
+ name: settings_hub-develop
3
+ description: Use PROACTIVELY for generating a settings section, editing a generated one, registering one by hand, replacing one another gem registered, writing the object a submitted change runs, writing the partial a section draws, and reading or changing settings from a JSON caller — MUST BE USED instead of hand-building a settings page, route or controller.
4
+ tools: Bash, Read, Write, Edit, Grep
5
+ scope: settings — one settings page whose sections are registered by the app and by other gems
6
+ ---
7
+
8
+ This local follows the steps below exactly and invents none. Where a step names a
9
+ decision, put it to the developer and wait for an answer rather than picking one.
10
+
11
+ ## What settings_hub is
12
+
13
+ SettingsHub is a mountable Rails engine serving one settings page whose sections the
14
+ app and other gems register. A section is a registration, not a page: it names
15
+ what it is called, which list it belongs in, what is drawn for it, and what runs
16
+ when a person submits it. SettingsHub owns the page, each section's address and the
17
+ check deciding who may see one, so adding a section changes no route, no
18
+ navigation and no controller. The same registrations answer a caller that speaks
19
+ JSON rather than asking for the page. Fire this local whenever settings are being
20
+ added to or changed in an app that already has settings_hub mounted.
21
+
22
+ ## Interface
23
+
24
+ - `bin/rails generate settings_hub:section <name>` — registers a section and writes the
25
+ object it runs, the partial it draws and a test for that object.
26
+ - `GET /settings/api/sections` — the sections the signed-in person may see, each
27
+ naming the actions it offers, as JSON.
28
+ - `PATCH /settings/api/sections/:key/:action_name` — runs one named action of one
29
+ section for a JSON caller and answers whether it worked.
30
+ - `SettingsHub.section` — registers a section, refusing a key already taken in that
31
+ area.
32
+ - `SettingsHub.replace_section` — registers a section over one already registered
33
+ under the same key and area, instead of refusing it.
34
+ - `SettingsHub.registry` — the sections registered so far, answering `find(key)`,
35
+ `in_area(area)` and `areas`.
36
+ - `SettingsHub::Result.ok` — what an object answers when the change was made.
37
+ - `SettingsHub::Result.refused` — what an object answers when it was not, carrying the
38
+ message shown to the person or given back to the caller.
39
+ - `SettingsHub::BadRegistration` — raised while registering when the key is taken,
40
+ when an object named in `runs:` cannot be found, or when a capability the app
41
+ does not recognise is named.
42
+ - `section.key` — the section's own name as a symbol, and the last part of its
43
+ address.
44
+ - `section.area` — the list it appears in, as a symbol.
45
+ - `section.title` — what that list calls it.
46
+ - `section.renders` — the partial drawn for it, or `nil`.
47
+ - `section.capability` — what a person must hold to see it, or `nil` when
48
+ everyone signed in may.
49
+ - `section.runs` — the class name, or hash of names, a submitted change is handed
50
+ to.
51
+ - `section.at` — the address a person is sent to instead of being drawn a
52
+ partial.
53
+ - `section.action` — the class held under the section's own key, which is the one
54
+ object a section naming a single object runs.
55
+ - `section.actions` — every class behind the section, keyed by the names given in
56
+ `runs:` and by the section's own key when a single object was named, and empty
57
+ for a section that runs nothing.
58
+ - `section.named_actions?` — whether the section named several objects rather
59
+ than one.
60
+ - `person` — the partial's local for who is signed in.
61
+ - `account` — the partial's local for the account settings act on, `nil` when the
62
+ host names none.
63
+ - `selection` — the partial's local for the query string, as a hash with symbol
64
+ keys.
65
+ - `submit_url` — the partial's local for where to submit, given to every section
66
+ that did not name several objects.
67
+ - `submit_urls` — the partial's local for where to submit each named object, as a
68
+ hash keyed by the names given in `runs:`.
69
+
70
+ ## How to use it
71
+
72
+ 1. **Settle what the section is.** Ask the developer three things and do not pick
73
+ any of them yourself: which area it belongs in, whether a capability is needed
74
+ to see it, and whether it offers one thing a person can do or several. The
75
+ area is any symbol and is commonly `:user`, `:team` or `:account`; sections
76
+ sharing one are listed together under it.
77
+
78
+ 2. **Read what is already there before writing anything.** A section may arrive
79
+ already registered, with an object, a partial and a test written for it, in
80
+ which case every step below is an edit to a file that exists rather than a new
81
+ file. Find the registration in `config/initializers/settings_hub.rb` or in the
82
+ registering gem's engine, and follow `renders:` and `runs:` to the partial and
83
+ the object they name.
84
+
85
+ 3. **Generate a new section rather than writing its files by hand.** The
86
+ generator takes the section's name and writes every file a working section
87
+ needs:
88
+
89
+ ```
90
+ bin/rails generate settings_hub:section reminders
91
+ ```
92
+
93
+ It adds the registration to `config/initializers/settings_hub.rb`, creating that
94
+ file with the reload hook already in it when it is missing. It writes
95
+ `app/models/save_reminders.rb`, `app/views/settings/_reminders.html.erb` and
96
+ `test/models/save_reminders_test.rb`. The generated test passes as written and
97
+ the section appears on the settings page with nothing further to wire. The
98
+ registration it writes always uses `area: :user` and names no capability, so
99
+ edit it to match the answers from step 1.
100
+
101
+ 4. **Register it in a reload hook.** An app not using the generator registers in
102
+ `config/initializers/settings_hub.rb`, a gem in its own engine:
103
+
104
+ ```ruby
105
+ Rails.application.config.to_prepare do
106
+ SettingsHub.section :password, area: :user, title: "Password",
107
+ renders: "settings/password", runs: "ChangePassword"
108
+ end
109
+ ```
110
+
111
+ The first argument is the key and appears in the section's address. `area:`
112
+ and `title:` are required; `renders:`, `runs:`, `capability:` and `at:` are
113
+ not. SettingsHub clears every registration on each code reload and the hook runs
114
+ again, so a registration made anywhere but `to_prepare` is made once and then
115
+ lost.
116
+
117
+ 5. **Write the partial `renders:` names.** The string is a partial path in the
118
+ host, so `"settings/password"` is `app/views/settings/_password.html.erb`. It
119
+ is drawn inside the settings page, so it writes no page heading, no frame and
120
+ no layout, and it uses keystone_ui helpers so it matches every other section:
121
+
122
+ ```erb
123
+ <%= ui_panel do %>
124
+ <%= ui_form(action: submit_url, method: :patch) do %>
125
+ <%= ui_form_field(attribute: "name", label: "Name", value: person.name, required: true) %>
126
+ <%= ui_button(label: "Save") %>
127
+ <% end %>
128
+ <% end %>
129
+ ```
130
+
131
+ It is handed `person`, `account`, `selection` and either `submit_url` or
132
+ `submit_urls`, and nothing else — no request, no params, no controller. Submit
133
+ with `method: :patch`.
134
+
135
+ 6. **Write the object `runs:` names.** Name the class as a string. It takes three
136
+ keywords and answers `call` with a result:
137
+
138
+ ```ruby
139
+ class ChangePassword
140
+ def initialize(person:, account:, values:)
141
+ @person = person
142
+ @account = account
143
+ @values = values
144
+ end
145
+
146
+ def call
147
+ return SettingsHub::Result.refused("That password is too short") unless @person.update(password: @values[:password])
148
+
149
+ SettingsHub::Result.ok
150
+ end
151
+ end
152
+ ```
153
+
154
+ `values` is what was submitted, as a hash with symbol keys. Reading a request,
155
+ a session or a params object here is what stops a caller other than the page
156
+ running the same object, so do neither.
157
+
158
+ 7. **Answer with a result, never a boolean or an exception.**
159
+ `SettingsHub::Result.ok` means the change was made and the person is sent back to
160
+ the section. `SettingsHub::Result.refused("why not")` draws the section again with
161
+ that message above it, saves nothing, and does not tell the host a change was
162
+ made.
163
+
164
+ 8. **Give each thing its own name when the section does several.** `runs:` takes
165
+ a hash, and each name gets its own address in `submit_urls`:
166
+
167
+ ```ruby
168
+ SettingsHub.section :team, area: :team, title: "Team", capability: :manage_team,
169
+ renders: "citizen/members/team",
170
+ runs: {
171
+ invite: "Citizen::Invite",
172
+ remove: "Citizen::RemoveMember"
173
+ }
174
+ ```
175
+
176
+ ```erb
177
+ <%= ui_form(action: submit_urls[:invite], method: :patch) do %>
178
+ ```
179
+
180
+ Every object in the hash takes the same three keywords and answers the same
181
+ way as a single one. A section naming several is handed `submit_urls` and not
182
+ `submit_url`, so a partial written against one does not work for the other.
183
+
184
+ 9. **Call the same sections from JSON when the caller is not a browser.** Two
185
+ addresses under wherever the engine is mounted answer a caller signed in as a
186
+ person the same way the page is:
187
+
188
+ ```
189
+ GET /settings/api/sections
190
+ PATCH /settings/api/sections/:key/:action_name
191
+ ```
192
+
193
+ The listing holds only the sections that person may see, each naming the
194
+ actions it offers. A section running one object names that action after the
195
+ section itself:
196
+
197
+ ```json
198
+ [{"key": "profile", "actions": ["profile"]},
199
+ {"key": "team", "actions": ["invite", "remove"]}]
200
+ ```
201
+
202
+ A change names the section and one of those actions, and is answered whether
203
+ it happened and why not:
204
+
205
+ ```json
206
+ {"ok": false, "message": "That name is spoken for"}
207
+ ```
208
+
209
+ The same objects run for both callers, so a section needs nothing added to it
210
+ to answer here. Take an action name from the listing rather than building one.
211
+
212
+ 10. **Hold a choice across a request with `selection`.** A section with no
213
+ controller of its own reads the query string from it — a section listing
214
+ people links each to `?member_id=1` and draws the one `selection[:member_id]`
215
+ names. Keys are symbols and the hash is empty when nothing was asked for.
216
+
217
+ 11. **Name a capability when not everyone may see the section.** `capability:`
218
+ takes the product's own word for it, the section is left out of the page and
219
+ out of the JSON listing for anyone who does not hold it, and both of its
220
+ addresses answer forbidden. A section naming none is shown to everyone the
221
+ app let in.
222
+
223
+ 12. **Send the person elsewhere with `at:` when settings_hub cannot draw the page.** A
224
+ section with `at:` draws no partial and redirects to that address, which is
225
+ how a page another engine owns is listed beside the rest. Give it no
226
+ `renders:` and no `runs:`.
227
+
228
+ 13. **Replace a registration rather than registering over it.** `SettingsHub.section`
229
+ refuses a key already taken in that area, so code meaning to override a
230
+ section another gem registered says so:
231
+
232
+ ```ruby
233
+ SettingsHub.replace_section :profile, area: :user, title: "Profile",
234
+ renders: "my_app/profile", runs: "MyApp::ChangeName"
235
+ ```
236
+
237
+ It takes the same arguments as `SettingsHub.section` and every other rule above
238
+ still applies to it.
239
+
240
+ 14. **Check it worked.** Start the app, sign in and visit the settings path. The
241
+ section appears in its area's list, the partial draws beside it, and
242
+ submitting saves and returns to the section. A refusal shows the message
243
+ above the partial and leaves the data unchanged.
244
+
245
+ ## Conventions
246
+
247
+ **A section that was generated rather than written saves nothing yet.** Its
248
+ object answers `SettingsHub::Result.ok` without touching the person or the account,
249
+ its partial holds one field named after the section, and its test asserts only
250
+ that the object answers ok, so all three are edited before the section does
251
+ anything. Edit the files in place and keep the object's three keywords and its
252
+ result.
253
+
254
+ **A registration that is wrong stops the app rather than the person.** SettingsHub
255
+ raises `SettingsHub::BadRegistration` while registering for a key already taken in
256
+ that area, an object named in `runs:` that the app cannot find, and a capability
257
+ the app does not recognise, and the message names which. Fix the registration;
258
+ never rescue it.
259
+
260
+ **Keep a key unique across the whole page, not just its area.** A registration is
261
+ refused only when the key is taken in the same area, but a section is looked up
262
+ by key alone, so the same key in two areas leaves one of them unreachable.
263
+
264
+ **A section runs nothing unless `runs:` names something.** Submitting from the
265
+ page to a section that named no object, or to a name its hash does not hold, is
266
+ rejected and nothing runs.
267
+
268
+ **A JSON caller submits only to an action the listing gave it.** A section
269
+ running nothing is listed with no actions at all, and the two JSON addresses are
270
+ the whole of what a caller outside the browser gets.
271
+
272
+ **SettingsHub stores nothing.** It owns no table, so every field a section shows and
273
+ every change it makes belongs to whoever registered it.
274
+
275
+ **The shipped Profile section reads and writes `name` on whatever the host
276
+ returns for the signed-in person.** An app whose person record has no writable
277
+ `name` replaces that section with `SettingsHub.replace_section`.
278
+
279
+ **Out of scope.** Adding the gem, mounting the engine, the methods the host's
280
+ `ApplicationController` supplies, and declaring which capabilities the app
281
+ recognises all belong to settings_hub-install.
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: settings_hub-info
3
+ description: Use to learn what settings_hub offers — the shared settings page, sections as registrations, and the vocabulary its other locals assume.
4
+ tools: Read
5
+ scope: settings — one settings page whose sections are registered by the app and by other gems
6
+ ---
7
+
8
+ This local explains what settings_hub is and which of its other locals you want. It
9
+ changes nothing and gives no steps.
10
+
11
+ ## What settings_hub is
12
+
13
+ SettingsHub is the settings page an app mounts instead of building. It is a mountable
14
+ Rails engine that owns the page itself, the list of what appears on it, the
15
+ address each entry is served at, and the check deciding who may see one. An app
16
+ or another gem adds to that page by registering a section, and a section owns
17
+ only its own fields and the things a person can do with them. Adding one changes
18
+ no routing, no navigation and no controller in the host.
19
+
20
+ Reach for it when a product needs one settings page that several parts of the
21
+ codebase contribute to — the app's own settings beside settings that belong to
22
+ gems the app installs. Those same registrations also answer a caller that speaks
23
+ JSON rather than asking for the page, so a client outside the browser reads and
24
+ changes settings without the app building a second interface for them. SettingsHub
25
+ owns no database table and stores nothing; every section's data belongs to
26
+ whoever registered it. Every page is drawn with keystone_ui, so a host that does
27
+ not use keystone_ui gets a settings page that does not match the rest of it.
28
+
29
+ ## Interface
30
+
31
+ SettingsHub's surface is split between its other two locals and this one documents
32
+ none of it.
33
+
34
+ - **settings_hub-install** — putting the gem in, mounting the engine, the methods the
35
+ host's `ApplicationController` supplies for who is signed in and what settings
36
+ act on, and telling settings_hub which capabilities the app recognises.
37
+ - **settings_hub-develop** — writing a new section's starting files, registering a
38
+ section by hand, replacing one another gem registered, the object a section
39
+ hands a submitted change to, the result that object answers with, the locals
40
+ its partial is drawn with, and the addresses a JSON caller reads and submits
41
+ at.
42
+
43
+ ## How to use it
44
+
45
+ - Putting settings_hub into an app for the first time, or an app has it and no section
46
+ is appearing — **settings_hub-install**.
47
+ - Starting a section from nothing, editing one that was generated, registering
48
+ one by hand, changing one that already exists, or calling settings from
49
+ outside the browser — **settings_hub-develop**.
50
+
51
+ Both, in that order, when an app is taking settings_hub and its first section in the
52
+ same pass.
53
+
54
+ ## Conventions
55
+
56
+ **Section** — one registration, not a page and not a controller. It names what
57
+ it is called, which list it belongs in, what is drawn for it, and what runs when
58
+ a person submits it.
59
+
60
+ **Area** — the list a section appears in on the page, named by a symbol. The
61
+ person's own settings, a team's and an account's are the usual three, any symbol
62
+ is allowed, and sections sharing an area are shown together.
63
+
64
+ **Key** — the section's own name, used in its address under wherever the engine
65
+ is mounted. A registration taking a key already held in that area is refused
66
+ when the app starts rather than in front of a person, and a section is found by
67
+ key alone, so the same key in two areas leaves one of them unreachable.
68
+
69
+ **Action** — one thing a person can do in a section, named by a symbol and
70
+ answered by its own object. A section that offers several names each of them; a
71
+ section that offers one has that action named after the section itself, so a
72
+ caller addresses every action the same way whichever kind it is.
73
+
74
+ **Capability** — the product's word for what a person must hold to see a
75
+ section. A section that names none is shown to everyone signed in, and the host
76
+ answers whether the signed-in person holds one. The refusal to register a
77
+ capability the app does not recognise runs only once the app declares which
78
+ capabilities exist, which is what lets settings_hub be installed without the gem that
79
+ would normally supply them.
80
+
81
+ **Registration time** — registrations are made in the reload hook rather than at
82
+ boot, and settings_hub clears what it holds on each reload, so the set of sections is
83
+ rebuilt from scratch every time the code reloads.
84
+
85
+ **Refusal** — a submitted change that did not happen. The object behind the
86
+ section says so with a message, nothing is saved, and the host is not told a
87
+ change was made. A person is drawn the section again with the message above it,
88
+ and a JSON caller is answered that it did not happen and given the same message.
89
+
90
+ **Generated code is a starting point.** A section written from nothing runs and
91
+ its test passes as written, and everything in it is meant to be edited rather
92
+ than kept as it came out.