principles 0.0.1

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 (278) hide show
  1. checksums.yaml +7 -0
  2. data/exe/principles +10 -0
  3. data/lib/principles/after_commit_hook_options.rb +44 -0
  4. data/lib/principles/cli.rb +149 -0
  5. data/lib/principles/command_line.rb +188 -0
  6. data/lib/principles/command_routes.rb +60 -0
  7. data/lib/principles/controllers/acknowledgement.rb +106 -0
  8. data/lib/principles/controllers/answer.rb +172 -0
  9. data/lib/principles/controllers/audit.rb +203 -0
  10. data/lib/principles/controllers/audit_completion.rb +113 -0
  11. data/lib/principles/controllers/command.rb +201 -0
  12. data/lib/principles/controllers/correction.rb +99 -0
  13. data/lib/principles/controllers/help.rb +107 -0
  14. data/lib/principles/controllers/inventory.rb +181 -0
  15. data/lib/principles/controllers/mapping.rb +56 -0
  16. data/lib/principles/controllers/member_entries.rb +149 -0
  17. data/lib/principles/controllers/members_a_commit_changed.rb +43 -0
  18. data/lib/principles/controllers/members_to_assess.rb +160 -0
  19. data/lib/principles/controllers/meta_principle_setup.rb +92 -0
  20. data/lib/principles/controllers/repoint.rb +68 -0
  21. data/lib/principles/controllers/running_tool.rb +41 -0
  22. data/lib/principles/data/audit_files.rb +96 -0
  23. data/lib/principles/data/bounded_file.rb +45 -0
  24. data/lib/principles/data/candidate_files_found.rb +105 -0
  25. data/lib/principles/data/command-steps.json +305 -0
  26. data/lib/principles/data/command_steps_file.rb +50 -0
  27. data/lib/principles/data/expected_type.rb +71 -0
  28. data/lib/principles/data/ignored_by_git.rb +54 -0
  29. data/lib/principles/data/json_text.rb +57 -0
  30. data/lib/principles/data/known_fields.rb +42 -0
  31. data/lib/principles/data/linked_path.rb +102 -0
  32. data/lib/principles/data/map_files.rb +104 -0
  33. data/lib/principles/data/principle_id.rb +32 -0
  34. data/lib/principles/data/principles_subfolder.rb +93 -0
  35. data/lib/principles/data/project_files.rb +109 -0
  36. data/lib/principles/data/repository.rb +165 -0
  37. data/lib/principles/data/running_version.rb +44 -0
  38. data/lib/principles/data/schema_check.rb +139 -0
  39. data/lib/principles/data/schemas/acknowledgement-answer.schema.json +25 -0
  40. data/lib/principles/data/schemas/answer-guide.schema.json +99 -0
  41. data/lib/principles/data/schemas/assessment-answer.schema.json +332 -0
  42. data/lib/principles/data/schemas/audit-record.schema.json +277 -0
  43. data/lib/principles/data/schemas/command-steps.schema.json +83 -0
  44. data/lib/principles/data/schemas/correction-answer-guide.schema.json +68 -0
  45. data/lib/principles/data/schemas/correction-answer.schema.json +306 -0
  46. data/lib/principles/data/schemas/member-map.schema.json +39 -0
  47. data/lib/principles/data/schemas/repointing-answer.schema.json +44 -0
  48. data/lib/principles/error.rb +102 -0
  49. data/lib/principles/invocation_options.rb +77 -0
  50. data/lib/principles/models/accepted_loading.rb +36 -0
  51. data/lib/principles/models/acknowledgement.rb +203 -0
  52. data/lib/principles/models/activation.rb +64 -0
  53. data/lib/principles/models/agent_loads.rb +64 -0
  54. data/lib/principles/models/answer_fields.rb +67 -0
  55. data/lib/principles/models/answer_place.rb +53 -0
  56. data/lib/principles/models/answer_text.rb +37 -0
  57. data/lib/principles/models/answered_time.rb +65 -0
  58. data/lib/principles/models/assessment_answer.rb +131 -0
  59. data/lib/principles/models/assessment_basis.rb +47 -0
  60. data/lib/principles/models/assessment_report.rb +62 -0
  61. data/lib/principles/models/audit.rb +200 -0
  62. data/lib/principles/models/audit_record.rb +249 -0
  63. data/lib/principles/models/audit_records.rb +103 -0
  64. data/lib/principles/models/audit_start.rb +96 -0
  65. data/lib/principles/models/audited_member_lines.rb +45 -0
  66. data/lib/principles/models/candidate_files.rb +66 -0
  67. data/lib/principles/models/candidate_inventory.rb +67 -0
  68. data/lib/principles/models/candidate_listing.rb +32 -0
  69. data/lib/principles/models/carried_assessments.rb +76 -0
  70. data/lib/principles/models/check_since_last_audit.rb +61 -0
  71. data/lib/principles/models/cited_files.rb +39 -0
  72. data/lib/principles/models/command_steps.rb +270 -0
  73. data/lib/principles/models/commits_brought_in.rb +27 -0
  74. data/lib/principles/models/commits_since_base.rb +32 -0
  75. data/lib/principles/models/correction_answer.rb +66 -0
  76. data/lib/principles/models/correction_basis.rb +70 -0
  77. data/lib/principles/models/dated_line.rb +29 -0
  78. data/lib/principles/models/decided_prior_findings.rb +106 -0
  79. data/lib/principles/models/declared_map_changes.rb +156 -0
  80. data/lib/principles/models/disposition.rb +233 -0
  81. data/lib/principles/models/entry_file.rb +65 -0
  82. data/lib/principles/models/entry_files.rb +46 -0
  83. data/lib/principles/models/evidence_item.rb +48 -0
  84. data/lib/principles/models/evidence_lines_since.rb +57 -0
  85. data/lib/principles/models/evidence_replacements.rb +99 -0
  86. data/lib/principles/models/finding_write_ups.rb +108 -0
  87. data/lib/principles/models/findings_left_open.rb +34 -0
  88. data/lib/principles/models/first_raised_claim.rb +128 -0
  89. data/lib/principles/models/frontmatter_fields.rb +128 -0
  90. data/lib/principles/models/hook_commands.rb +100 -0
  91. data/lib/principles/models/ignored_candidates.rb +46 -0
  92. data/lib/principles/models/json_digest.rb +35 -0
  93. data/lib/principles/models/json_path_query.rb +131 -0
  94. data/lib/principles/models/json_path_selectors.rb +64 -0
  95. data/lib/principles/models/json_path_string.rb +76 -0
  96. data/lib/principles/models/kept_prior_finding.rb +70 -0
  97. data/lib/principles/models/last_completed_revision.rb +70 -0
  98. data/lib/principles/models/line_span.rb +49 -0
  99. data/lib/principles/models/loaded_files.rb +60 -0
  100. data/lib/principles/models/loading_decision.rb +45 -0
  101. data/lib/principles/models/loading_facts.rb +45 -0
  102. data/lib/principles/models/loads_to_check.rb +93 -0
  103. data/lib/principles/models/locate_refusal.rb +23 -0
  104. data/lib/principles/models/located_text.rb +43 -0
  105. data/lib/principles/models/locator.rb +52 -0
  106. data/lib/principles/models/map_members.rb +171 -0
  107. data/lib/principles/models/markdown_file.rb +92 -0
  108. data/lib/principles/models/member.rb +171 -0
  109. data/lib/principles/models/member_assessments.rb +144 -0
  110. data/lib/principles/models/member_elsewhere.rb +74 -0
  111. data/lib/principles/models/member_file.rb +156 -0
  112. data/lib/principles/models/member_grounds.rb +32 -0
  113. data/lib/principles/models/member_history.rb +68 -0
  114. data/lib/principles/models/member_lines.rb +95 -0
  115. data/lib/principles/models/member_loads.rb +197 -0
  116. data/lib/principles/models/member_map.rb +219 -0
  117. data/lib/principles/models/member_map_history.rb +143 -0
  118. data/lib/principles/models/member_maps.rb +185 -0
  119. data/lib/principles/models/member_moves.rb +95 -0
  120. data/lib/principles/models/member_problems.rb +253 -0
  121. data/lib/principles/models/member_sighting.rb +176 -0
  122. data/lib/principles/models/member_snapshots.rb +36 -0
  123. data/lib/principles/models/member_state.rb +35 -0
  124. data/lib/principles/models/members_elsewhere.rb +46 -0
  125. data/lib/principles/models/members_sharing_lines.rb +116 -0
  126. data/lib/principles/models/meta_principle.rb +51 -0
  127. data/lib/principles/models/missing_files.rb +85 -0
  128. data/lib/principles/models/misspelt_member_id.rb +45 -0
  129. data/lib/principles/models/open_finding.rb +221 -0
  130. data/lib/principles/models/open_findings_elsewhere.rb +45 -0
  131. data/lib/principles/models/other_principles.rb +47 -0
  132. data/lib/principles/models/placement.rb +42 -0
  133. data/lib/principles/models/plain_text_file.rb +32 -0
  134. data/lib/principles/models/principles_folder_move.rb +59 -0
  135. data/lib/principles/models/principles_package.rb +53 -0
  136. data/lib/principles/models/prior_finding_decision.rb +162 -0
  137. data/lib/principles/models/prior_finding_outcomes.rb +60 -0
  138. data/lib/principles/models/prior_findings.rb +109 -0
  139. data/lib/principles/models/problem_report.rb +110 -0
  140. data/lib/principles/models/project.rb +214 -0
  141. data/lib/principles/models/project_history.rb +203 -0
  142. data/lib/principles/models/record_judged_against.rb +42 -0
  143. data/lib/principles/models/recorded_entry.rb +93 -0
  144. data/lib/principles/models/recorded_finding.rb +97 -0
  145. data/lib/principles/models/recorded_findings.rb +58 -0
  146. data/lib/principles/models/recorded_members.rb +149 -0
  147. data/lib/principles/models/repoint.rb +112 -0
  148. data/lib/principles/models/reported_check.rb +130 -0
  149. data/lib/principles/models/request_id.rb +70 -0
  150. data/lib/principles/models/runs_from.rb +36 -0
  151. data/lib/principles/models/skill_loads.rb +123 -0
  152. data/lib/principles/models/still_holds_restatement.rb +87 -0
  153. data/lib/principles/models/text_size.rb +26 -0
  154. data/lib/principles/models/uncommitted_outside_the_map.rb +30 -0
  155. data/lib/principles/models/waits_on.rb +58 -0
  156. data/lib/principles/models/write_up_opening.rb +93 -0
  157. data/lib/principles/models/write_up_pointer.rb +50 -0
  158. data/lib/principles/models/write_up_repoint.rb +39 -0
  159. data/lib/principles/prose_list.rb +23 -0
  160. data/lib/principles/supported_ruby.rb +93 -0
  161. data/lib/principles/version.rb +11 -0
  162. data/lib/principles/views/acknowledged_since_last_audit.rb +34 -0
  163. data/lib/principles/views/acknowledgement_result.rb +44 -0
  164. data/lib/principles/views/activation_in_hand.rb +47 -0
  165. data/lib/principles/views/assessment_request.rb +131 -0
  166. data/lib/principles/views/audit_changes.rb +79 -0
  167. data/lib/principles/views/audit_commit.rb +55 -0
  168. data/lib/principles/views/audit_result.rb +147 -0
  169. data/lib/principles/views/candidate_files_result.rb +95 -0
  170. data/lib/principles/views/check_result_template.rb +75 -0
  171. data/lib/principles/views/diff_since_last_audit.rb +29 -0
  172. data/lib/principles/views/findings_result.rb +45 -0
  173. data/lib/principles/views/help.rb +119 -0
  174. data/lib/principles/views/invocation.rb +66 -0
  175. data/lib/principles/views/landed_since_base.rb +27 -0
  176. data/lib/principles/views/lines_shared_with_member.rb +111 -0
  177. data/lib/principles/views/loads_to_check_result.rb +45 -0
  178. data/lib/principles/views/map_result.rb +169 -0
  179. data/lib/principles/views/member_change_since_baseline.rb +58 -0
  180. data/lib/principles/views/member_repairs.rb +82 -0
  181. data/lib/principles/views/members_a_commit_changed.rb +66 -0
  182. data/lib/principles/views/members_with_no_baseline.rb +56 -0
  183. data/lib/principles/views/meta_principle_passage.rb +23 -0
  184. data/lib/principles/views/meta_principle_result.rb +170 -0
  185. data/lib/principles/views/open_findings_elsewhere_result.rb +32 -0
  186. data/lib/principles/views/open_findings_line.rb +125 -0
  187. data/lib/principles/views/other_principles_unjudged_result.rb +32 -0
  188. data/lib/principles/views/outside_the_map_result.rb +31 -0
  189. data/lib/principles/views/package_since_last_audit_result.rb +34 -0
  190. data/lib/principles/views/principle_read_result.rb +86 -0
  191. data/lib/principles/views/principle_with_changed_members.rb +26 -0
  192. data/lib/principles/views/principles_with_no_completed_audit.rb +30 -0
  193. data/lib/principles/views/printed_principle.rb +45 -0
  194. data/lib/principles/views/prior_finding_decisions_result.rb +35 -0
  195. data/lib/principles/views/prior_findings_result.rb +80 -0
  196. data/lib/principles/views/problem_lines.rb +84 -0
  197. data/lib/principles/views/problem_result.rb +107 -0
  198. data/lib/principles/views/prompts/acknowledge-a-change/say-whether-the-change-keeps-the-meaning.md +12 -0
  199. data/lib/principles/views/prompts/answer-any-judgement-request/what-a-self-audit-cannot-show.md +6 -0
  200. data/lib/principles/views/prompts/assess-every-member/answer-guide.json +63 -0
  201. data/lib/principles/views/prompts/assess-every-member/assess-each-member-and-the-set.md +12 -0
  202. data/lib/principles/views/prompts/assess-every-member/check-roles-other-principles-give-these-lines.md +7 -0
  203. data/lib/principles/views/prompts/assess-every-member/decide-each-member-loading.md +17 -0
  204. data/lib/principles/views/prompts/assess-every-member/decide-each-prior-finding.md +38 -0
  205. data/lib/principles/views/prompts/assess-every-member/give-problem-evidence-at-still-holds.md +2 -0
  206. data/lib/principles/views/prompts/assess-every-member/give-the-evidence-a-prior-finding-lacks.md +7 -0
  207. data/lib/principles/views/prompts/assess-every-member/have-a-changed-check-reviewed-before-running-it.md +4 -0
  208. data/lib/principles/views/prompts/assess-every-member/have-a-new-check-reviewed-before-running-it.md +3 -0
  209. data/lib/principles/views/prompts/assess-every-member/have-hidden-characters-in-a-check-explained.md +4 -0
  210. data/lib/principles/views/prompts/assess-every-member/judge-acknowledged-edits-together.md +6 -0
  211. data/lib/principles/views/prompts/assess-every-member/judge-the-keeping-against-the-other-principles.md +7 -0
  212. data/lib/principles/views/prompts/assess-every-member/judge-the-keeping-under-the-release-since-the-last-audit.md +10 -0
  213. data/lib/principles/views/prompts/assess-every-member/judge-the-set-with-carried-assessments.md +10 -0
  214. data/lib/principles/views/prompts/assess-every-member/keep-the-last-review-basis-unless-you-give-a-new-one.md +1 -0
  215. data/lib/principles/views/prompts/assess-every-member/name-another-principles-open-finding-rather-than-raise-it.md +9 -0
  216. data/lib/principles/views/prompts/assess-every-member/read-whether-each-prior-finding-s-evidence-changed.md +12 -0
  217. data/lib/principles/views/prompts/assess-every-member/run-a-named-check-at-every-full-audit.md +2 -0
  218. data/lib/principles/views/prompts/assess-every-member/run-the-check-then-report-it.md +2 -0
  219. data/lib/principles/views/prompts/assess-every-member/run-the-reviewed-check-and-say-what-its-review-rested-on.md +6 -0
  220. data/lib/principles/views/prompts/assess-every-member/uncommitted-files-outside-the-map.md +4 -0
  221. data/lib/principles/views/prompts/assess-every-member/why-each-member-is-judged-again.md +24 -0
  222. data/lib/principles/views/prompts/dispose-of-each-finding/answer-guide.json +34 -0
  223. data/lib/principles/views/prompts/dispose-of-each-finding/fix-or-record-each-finding.md +24 -0
  224. data/lib/principles/views/prompts/find-a-principle-to-map/ask-the-owner-before-mapping.md +1 -0
  225. data/lib/principles/views/prompts/find-a-principle-to-map/choose-one-principle.md +5 -0
  226. data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-ignored-folders.md +2 -0
  227. data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-ignored.md +3 -0
  228. data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-possibly-test-data.md +2 -0
  229. data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-untracked.md +2 -0
  230. data/lib/principles/views/prompts/find-a-principle-to-map/what-each-command-writes.md +4 -0
  231. data/lib/principles/views/prompts/find-a-principle-to-map/where-the-inventory-looks.md +10 -0
  232. data/lib/principles/views/prompts/learn-what-a-command-does/add-member.md +45 -0
  233. data/lib/principles/views/prompts/learn-what-a-command-does/change-activation.md +21 -0
  234. data/lib/principles/views/prompts/learn-what-a-command-does/change-description.md +2 -0
  235. data/lib/principles/views/prompts/learn-what-a-command-does/change-member-check.md +17 -0
  236. data/lib/principles/views/prompts/learn-what-a-command-does/change-member-role.md +9 -0
  237. data/lib/principles/views/prompts/learn-what-a-command-does/create-principle.md +6 -0
  238. data/lib/principles/views/prompts/learn-what-a-command-does/every-command-and-how-they-chain.md +28 -0
  239. data/lib/principles/views/prompts/learn-what-a-command-does/list-candidate-files.md +17 -0
  240. data/lib/principles/views/prompts/learn-what-a-command-does/list-member-problems.md +73 -0
  241. data/lib/principles/views/prompts/learn-what-a-command-does/list-open-findings.md +71 -0
  242. data/lib/principles/views/prompts/learn-what-a-command-does/list-principles.md +7 -0
  243. data/lib/principles/views/prompts/learn-what-a-command-does/move-member.md +11 -0
  244. data/lib/principles/views/prompts/learn-what-a-command-does/remove-member-check.md +10 -0
  245. data/lib/principles/views/prompts/learn-what-a-command-does/remove-member.md +6 -0
  246. data/lib/principles/views/prompts/learn-what-a-command-does/show-answer-check.md +16 -0
  247. data/lib/principles/views/prompts/learn-what-a-command-does/show-audit.md +20 -0
  248. data/lib/principles/views/prompts/learn-what-a-command-does/show-meta-principle-passage.md +52 -0
  249. data/lib/principles/views/prompts/learn-what-a-command-does/show-principle.md +86 -0
  250. data/lib/principles/views/prompts/learn-what-a-command-does/start-audit.md +68 -0
  251. data/lib/principles/views/prompts/learn-what-a-command-does/submit-answer.md +56 -0
  252. data/lib/principles/views/prompts/repoint-write-ups/point-each-moved-write-up-at-its-new-place.md +10 -0
  253. data/lib/principles/views/prompts/report-after-a-commit/it-refused-after-the-commits-touched-the-maps.md +2 -0
  254. data/lib/principles/views/prompts/report-after-a-commit/others-are-still-listed.md +1 -0
  255. data/lib/principles/views/prompts/report-after-a-commit/the-commits-changed-these-members.md +2 -0
  256. data/lib/principles/views/prompts/report-after-a-commit/this-command-shows-each-one.md +1 -0
  257. data/lib/principles/views/prompts/report-the-completed-audit/commit-only-what-the-audit-changed.md +2 -0
  258. data/lib/principles/views/prompts/report-the-completed-audit/replace-the-default-commit-message.md +4 -0
  259. data/lib/principles/views/prompts/report-the-completed-audit/what-completion-means.md +2 -0
  260. data/lib/principles/views/prompts/set-principles-up/entry-file-passage.md +3 -0
  261. data/lib/principles/views/prompts/set-principles-up/name-the-step-each-member-lacks.md +15 -0
  262. data/lib/principles/views/prompts/set-principles-up/place-the-passage-with-the-user.md +31 -0
  263. data/lib/principles/views/prompts/set-principles-up/set-up-first.md +2 -0
  264. data/lib/principles/views/prompts/set-principles-up/what-the-passage-presumes.md +4 -0
  265. data/lib/principles/views/prompts/start-a-session/act-on-the-open-findings.md +5 -0
  266. data/lib/principles/views/prompts/start-a-session/it-returns-after-each-clear-or-compaction-too.md +1 -0
  267. data/lib/principles/views/prompts/start-a-session/it-returns-at-each-session-start.md +1 -0
  268. data/lib/principles/views/prompts/start-a-session/raise-them-at-the-next-pause.md +1 -0
  269. data/lib/principles/views/prompts/start-a-session/raise-them-before-the-first-task.md +1 -0
  270. data/lib/principles/views/prompts.rb +71 -0
  271. data/lib/principles/views/repoint_result.rb +80 -0
  272. data/lib/principles/views/request_part.rb +26 -0
  273. data/lib/principles/views/request_parts.rb +26 -0
  274. data/lib/principles/views/shell_word.rb +29 -0
  275. data/lib/principles/views/visible_text.rb +26 -0
  276. data/lib/principles/views/workflow_diagram.rb +77 -0
  277. data/lib/principles.rb +5 -0
  278. metadata +326 -0
@@ -0,0 +1,176 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "time"
6
+ require_relative "../error"
7
+ require_relative "answer_place"
8
+ require_relative "answer_text"
9
+ require_relative "answered_time"
10
+ require_relative "carried_assessments"
11
+
12
+ module Principles
13
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
14
+ module Models
15
+ # One member assessment's sighting: whether its delivery was observed, what was seen, and when, as the last full
16
+ # audit's sighting of the member bears on it (#680, #716). Its delivery picks its kind: Observed, or NotObserved.
17
+ class MemberSighting
18
+ # The sighting of assessment, one of the answer's member_assessments at the place at, of the kind its
19
+ # delivery names; carried knows the last full audit's sightings, and answered_at is when the answer came. A
20
+ # delivery other than observed or not_observed is refused here, before the evidence, as the refusals have always
21
+ # come.
22
+ #: (Hash[String, untyped], carried: CarriedAssessments?, answered_at: String, at: AnswerPlace) -> MemberSighting
23
+ def self.for(assessment, carried:, answered_at:, at:)
24
+ case assessment["delivery"]
25
+ when "observed" then Observed.new(assessment, carried: carried, answered_at: answered_at, at: at)
26
+ when "not_observed" then NotObserved.new(assessment, at: at)
27
+ else raise Error.new("invalid_delivery", "Use observed or not_observed",
28
+ "Record unavailable delivery as not_observed.")
29
+ end
30
+ end
31
+
32
+ #: (Hash[String, untyped], at: AnswerPlace) -> void
33
+ def initialize(assessment, at:)
34
+ @assessment = assessment
35
+ @at = at
36
+ end
37
+
38
+ # Refuses the sighting unless it holds to its kind's rules.
39
+ #: () -> void
40
+ def check = raise(NotImplementedError, "#{self.class} has no rules of its own")
41
+
42
+ private
43
+
44
+ #: () -> untyped
45
+ def id = @assessment["member_id"]
46
+
47
+ # The JSON pointer to field of this assessment.
48
+ #: (String) -> String
49
+ def pointer(field) = @at.child(field).pointer
50
+
51
+ # A delivery not observed has no sighting, so it gives no observed_at (#680).
52
+ class NotObserved < MemberSighting
53
+ #: (Hash[String, untyped], at: AnswerPlace) -> void
54
+ def initialize(assessment, at:)
55
+ super
56
+ freeze
57
+ end
58
+
59
+ #: () -> void
60
+ def check
61
+ return unless @assessment.key?("observed_at")
62
+
63
+ raise Error.new("invalid_answer", "Member #{id} is not observed but gives observed_at",
64
+ "Drop observed_at, or record delivery as observed with what you saw in observation.",
65
+ fields: [{ "pointer" => pointer("observed_at"),
66
+ "problem" => "only an observed delivery has a sighting to date" }])
67
+ end
68
+ end
69
+
70
+ # An observed delivery says what was seen, and when, as a time the record can compare, and no word-for-word
71
+ # repeat of the last full audit's but as that sighting or one in this audit.
72
+ class Observed < MemberSighting
73
+ SHAPE_EXAMPLE = "2026-11-01T09:30:00Z" #: String
74
+ # A record that gives only its date gives it at midnight UTC, the observation saying the time is unknown (#785).
75
+ DATE_ALONE_EXAMPLE = "2026-11-01T00:00:00Z" #: String
76
+ OR_NULL_FOR_THIS_AUDIT = "or null for a sighting in this audit." #: String
77
+ # The repair for an observed_at naming no UTC time exactly, which shows the form to follow.
78
+ FORM_REPAIR = %(Give "observed_at" like "#{SHAPE_EXAMPLE}", in UTC, #{OR_NULL_FOR_THIS_AUDIT} If the ) +
79
+ "record gives only a date, give it at 00:00:00Z, such as #{DATE_ALONE_EXAMPLE}, and say in " \
80
+ "observation that the time is unknown." #: String
81
+
82
+ # answered_at is when the answer reporting it came, a UTC time like audited_at, no later than the audit it
83
+ # completes is dated.
84
+ #: (Hash[String, untyped], carried: CarriedAssessments?, answered_at: String, at: AnswerPlace) -> void
85
+ def initialize(assessment, carried:, answered_at:, at:)
86
+ super(assessment, at: at)
87
+ @carried = carried
88
+ @answered_at = answered_at
89
+ freeze
90
+ end
91
+
92
+ #: () -> void
93
+ def check
94
+ refuse_missing_observation
95
+ refuse_unshaped_observed_at
96
+ refuse_observed_after_answer
97
+ refuse_repeated_observation
98
+ end
99
+
100
+ private
101
+
102
+ # An observed delivery needs what was seen (D29: a rule across fields stays in the model).
103
+ #: () -> void
104
+ def refuse_missing_observation
105
+ return if AnswerText.new(@assessment["observation"]).written?
106
+
107
+ raise Error.new("invalid_answer", "Member #{id} is observed but has no observation",
108
+ "Give observation: what you saw the member do, where you saw it, and what you did not see; " \
109
+ "if you did not see it act, record delivery as not_observed.",
110
+ fields: [{ "pointer" => pointer("observation"), "problem" => "must be a nonempty string" }])
111
+ end
112
+
113
+ # An observed_at is a UTC time in audited_at's form, as the record dates every other sighting, to compare; a
114
+ # date alone, an offset or fractional seconds cannot, nor a time no calendar has (#699). Null or none is this
115
+ # audit's.
116
+ #: () -> void
117
+ def refuse_unshaped_observed_at
118
+ observed_at = @assessment["observed_at"]
119
+ return if observed_at.nil? || AnsweredTime.new(observed_at).utc?
120
+
121
+ raise Error.new("invalid_answer",
122
+ "Member #{id}'s observed_at #{observed_at.to_json} is not a UTC time like audited_at",
123
+ shape_repair(AnsweredTime.new(observed_at)),
124
+ fields: [{ "pointer" => pointer("observed_at"),
125
+ "problem" => "must be a UTC time like #{SHAPE_EXAMPLE}" }])
126
+ end
127
+
128
+ # The repair for an observed_at not in audited_at's form: the value it names exactly, to copy, when that is no
129
+ # later than the answer, which would refuse it in turn; otherwise the form, and how to date a date alone (#801).
130
+ #: (AnsweredTime) -> String
131
+ def shape_repair(given)
132
+ exact = given.exact_utc
133
+ return FORM_REPAIR if exact.nil? || AnsweredTime.new(exact).after?(@answered_at)
134
+ unless given.date_alone?
135
+ return %(Give "observed_at" as "#{exact}", the same time in UTC, #{OR_NULL_FOR_THIS_AUDIT})
136
+ end
137
+
138
+ %(Give "observed_at" as "#{exact}", that date at 00:00:00Z, and say in observation that the time is ) +
139
+ "unknown, #{OR_NULL_FOR_THIS_AUDIT}"
140
+ end
141
+
142
+ # A sighting is made before the answer that reports it, so its observed_at is no later than answered_at
143
+ # (#699). Checked once its form is, so the two compare as times.
144
+ #: () -> void
145
+ def refuse_observed_after_answer
146
+ observed_at = @assessment["observed_at"]
147
+ return if observed_at.nil? || !AnsweredTime.new(observed_at).after?(@answered_at)
148
+
149
+ raise Error.new("invalid_answer", "Member #{id}'s observed_at, #{observed_at}, is after this answer",
150
+ %(Give "observed_at" as when you saw it, no later than #{@answered_at}, ) +
151
+ OR_NULL_FOR_THIS_AUDIT,
152
+ fields: [{ "pointer" => pointer("observed_at"),
153
+ "problem" => "must not be after this answer, #{@answered_at}" }])
154
+ end
155
+
156
+ # A fresh observation that repeats the last full audit's word for word describes that audit's sighting or one
157
+ # made in this audit; only the agent knows which, so it gives the recorded date or null, copied from the repair
158
+ # so it never types a clock time (#680, #716). Any other date is another sighting, described in its own words.
159
+ #: () -> void
160
+ def refuse_repeated_observation
161
+ seen = @carried&.repeated_sighting(id, @assessment["observation"]) or return
162
+ return if @assessment.key?("observed_at") && [seen, nil].include?(@assessment["observed_at"])
163
+
164
+ raise Error.new("observation_repeats_last_audit",
165
+ "#{id}'s observation repeats the last full audit's word for word, a sighting dated #{seen}",
166
+ %(Give "observed_at": "#{seen}" if you kept that sighting. If you saw the member again, ) +
167
+ "describe what you saw this time in your own words, with \"observed_at\": null for a " \
168
+ "sighting in this audit, or the date of another, such as a dated record.",
169
+ fields: [{ "pointer" => pointer("observed_at"),
170
+ "problem" => "must be the last full audit's date or null when observation " \
171
+ "repeats its text" }])
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "audited_member_lines"
5
+ require_relative "located_text"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # Each of a map's members by id, where it is in its file now and the hash of what it says there, as
11
+ # Project#snapshots located them all.
12
+ class MemberSnapshots
13
+ #: (Hash[String, Hash[String, untyped]]) -> void
14
+ def initialize(by_id)
15
+ @by_id = by_id.freeze #: Hash[String, Hash[String, untyped]]
16
+ freeze
17
+ end
18
+
19
+ # Each member's snapshot by id.
20
+ #: () -> Hash[String, Hash[String, untyped]]
21
+ def to_h = @by_id
22
+
23
+ # Where the member id is in its file now, and the hash of what it says there.
24
+ #: (String) -> LocatedText
25
+ def of(id) = LocatedText.of(@by_id.fetch(id))
26
+
27
+ # Each member's id and the hash of its text now.
28
+ #: () -> Hash[String, String]
29
+ def hashes = @by_id.transform_values { |snapshot| snapshot.fetch("sha256") }
30
+
31
+ # The lines the members span now, against which another principle's evidence is said to share them (D64).
32
+ #: () -> AuditedMemberLines
33
+ def audited_lines = AuditedMemberLines.new(@by_id.transform_values { LocatedText.of(it) })
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
6
+ module Models
7
+ # One member by its id, with its state now as list-member-problems reports it, which a completed audit names for
8
+ # another principle sharing a file its fixes changed (#410, #823).
9
+ class MemberState
10
+ # The states that mean a member's reference is broken, so an audit would refuse it. unreadable_map is a map's
11
+ # state, which MemberProblems#broken reads over its entries; no MemberState holds it, since a map that cannot be
12
+ # read has no members to name.
13
+ BROKEN = %w[locator_lost unavailable unreadable_map].freeze #: Array[String]
14
+
15
+ #: (String, String) -> void
16
+ def initialize(id, state)
17
+ @id = id
18
+ @state = state
19
+ freeze
20
+ end
21
+
22
+ # Whether its own text changed or its reference broke, so the principle it belongs to needs auditing again.
23
+ #: () -> bool
24
+ def changed_or_broken? = @state == "changed" || broken?
25
+
26
+ # Whether its reference is broken, so an audit would refuse it.
27
+ #: () -> bool
28
+ def broken? = BROKEN.include?(@state)
29
+
30
+ # The member as printed: its id and state.
31
+ #: () -> Hash[String, String]
32
+ def to_h = { "id" => @id, "state" => @state }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_elsewhere"
5
+ require_relative "open_finding"
6
+
7
+ module Principles
8
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # The members of other principles' maps sharing a line with one located member, as MembersSharingLines finds
11
+ # them: what each result prints of them under mapped_elsewhere, and whether the agent should check the text
12
+ # where their lines meet (#756, #776).
13
+ class MembersElsewhere
14
+ #: (Array[MemberElsewhere]) -> void
15
+ def initialize(members)
16
+ @members = members
17
+ freeze
18
+ end
19
+
20
+ # Each as add-member, show-principle and a full audit's request print it under mapped_elsewhere.
21
+ #: () -> Array[Hash[String, untyped]]
22
+ def shown = @members.map(&:to_h)
23
+
24
+ # Whether one of them holds the located member's lines, or lies within them, without either being a whole file.
25
+ #: () -> bool
26
+ def text_check_needed? = @members.any?(&:text_check_needed?)
27
+
28
+ # The principles whose maps hold them, each once.
29
+ #: () -> Array[String]
30
+ def principle_ids = @members.map(&:principle_id).uniq
31
+
32
+ # Each as a full audit's request prints it, naming the findings left_open_by_principle gives its principle that
33
+ # name it (#772, D62).
34
+ #: (Hash[String, Array[OpenFinding]]) -> Array[Hash[String, untyped]]
35
+ def naming(left_open_by_principle)
36
+ @members.map { |member| member.naming(left_open_by_principle.fetch(member.principle_id, [])) }
37
+ end
38
+
39
+ # The findings left_open_by_principle gives their principles that name any of them.
40
+ #: (Hash[String, Array[OpenFinding]]) -> Array[OpenFinding]
41
+ def open_among(left_open_by_principle)
42
+ @members.flat_map { |member| member.open_among(left_open_by_principle.fetch(member.principle_id, [])) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "line_span"
6
+ require_relative "located_text"
7
+ require_relative "member"
8
+ require_relative "member_elsewhere"
9
+ require_relative "members_elsewhere"
10
+
11
+ module Principles
12
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
13
+ module Models
14
+ # The members whose text shares a line with one member located in its file, so one edit there shows on
15
+ # each. The project locates the others; a member this tool cannot find now is left out, since show-principle
16
+ # reports it.
17
+ class MembersSharingLines
18
+ # The members sharing a line with member, located where it is now; refuses as Project#locate does when it
19
+ # cannot be found.
20
+ #: (Project, Member) -> MembersSharingLines
21
+ def self.of(project, member)
22
+ new(project, project.located_text(member.locator), id: member.id, whole_file: member.whole_file?)
23
+ end
24
+
25
+ # located is the text found for one member, its path and lines, id is that member's id, and whole_file whether
26
+ # it is its whole file.
27
+ #: (Project, LocatedText, id: String, ?whole_file: bool) -> void
28
+ def initialize(project, located, id:, whole_file: false)
29
+ @project = project
30
+ @located = located
31
+ @span = located.span(whole_file:) #: LineSpan
32
+ @id = id
33
+ freeze
34
+ end
35
+
36
+ # The ids of members of its own map whose text shares a line with the located member, which is not among
37
+ # them. A member of another map may have the same id and is another member, so elsewhere keeps it.
38
+ #: (Array[Member]) -> Array[String]
39
+ def ids_in(members)
40
+ sharing(others(members)).map { |member, _found| member.id }
41
+ end
42
+
43
+ # The ids of members whose text spans exactly the located member's first and last lines, so the agent sees
44
+ # it may be mapping the same lines twice (#442). Equal lines are not equal text: a passage can end within
45
+ # its last line.
46
+ #: (Array[Member]) -> Array[String]
47
+ def same_lines_ids_in(members)
48
+ sharing(others(members)).filter_map { |member, found| member.id if same_lines?(found) }
49
+ end
50
+
51
+ # The members sharing a line with the located one, beside map, its own: those in map, those of them on exactly
52
+ # its lines, and those in the other maps, as plain values, with whether the agent should check the text where
53
+ # one of those holds the other's lines without being a whole file (#756).
54
+ #: (MemberMap)
55
+ #| -> { overlaps: Array[String], same_lines_as: Array[String], mapped_elsewhere: Array[Hash[String, untyped]],
56
+ #| text_check: bool }
57
+ def facts_beside(map)
58
+ others = elsewhere(except: map.id)
59
+ members = map.members.to_a
60
+ { overlaps: ids_in(members), same_lines_as: same_lines_ids_in(members),
61
+ mapped_elsewhere: others.shown, text_check: others.text_check_needed? }
62
+ end
63
+
64
+ # Each member of a map other than except whose text shares a line with the located member, with its map, its
65
+ # role there and its lines, so the agent sees that one edit changes both, checks the roles fit (#383) and how
66
+ # much of the located member it holds, which no other part of the page shows (#610): same_lines when it spans
67
+ # exactly the located member's lines (#442), spans_these_lines when its lines include all of them, and
68
+ # within_these_lines when they include all of its, so the agent can tell which member an edit reaches (#718). A
69
+ # partial overlap is marked neither way. A map that cannot be read is left out; list-member-problems
70
+ # reports it.
71
+ #: (except: String) -> MembersElsewhere
72
+ def elsewhere(except:)
73
+ maps = @project.member_maps
74
+ MembersElsewhere.new((maps.ids - [except]).flat_map { |id| sharing_in(maps, id) })
75
+ end
76
+
77
+ private
78
+
79
+ # The members of map id sharing a line with the located member, or none when that map cannot be read.
80
+ #: (MemberMaps, String) -> Array[MemberElsewhere]
81
+ def sharing_in(maps, id)
82
+ sharing(maps.find(id).members.to_a).map do |member, found|
83
+ MemberElsewhere.new(principle_id: id, member:, span: found.span(whole_file: member.whole_file?),
84
+ located: @span)
85
+ end
86
+ rescue Error
87
+ []
88
+ end
89
+
90
+ # Each member that shares a line with the located member, with where it is found now: in the same file, by
91
+ # its own path or through a symbolic link (#695).
92
+ #: (Array[Member]) -> Array[[Member, LocatedText]]
93
+ def sharing(members)
94
+ members.filter_map do |member|
95
+ next unless @project.same_file?(member.path, @located.path)
96
+
97
+ found = @project.located_text(member.locator)
98
+ [member, found] if found.span.shares_a_line_with?(@span)
99
+ rescue Error
100
+ nil
101
+ end
102
+ end
103
+
104
+ # The members of its own map but the located one.
105
+ #: (Array[Member]) -> Array[Member]
106
+ def others(members)
107
+ members.reject { |member| member.id == @id }
108
+ end
109
+
110
+ #: (LocatedText) -> bool
111
+ def same_lines?(found)
112
+ found.span.same_lines_as?(@span)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "activation"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # The principle that keeps every other principle held, which setting principles up maps first (D67, D68). It is
11
+ # this product's own principle, not an example, so what its map starts with ships with the tool; once mapped, the
12
+ # project owns the map like any other.
13
+ module MetaPrinciple
14
+ ID = "sticking-to-your-principles"
15
+
16
+ DESCRIPTION = "Every other principle stays held: each place that carries one is mapped, a change to any of " \
17
+ "them is judged against the rest before work goes on, and the agent loads only the guidance its " \
18
+ "task needs, so harness drift and debt stay visible and an agent can be trusted with longer, " \
19
+ "wider unsupervised work."
20
+
21
+ # Its work is in hand in every session, since it brings the other principles back into attention.
22
+ ACTIVATION = Activation.new({ "when" => "always" }) #: Activation
23
+
24
+ # The heading of the passage this tool proposes, which the add-member command it prints names as the section.
25
+ PASSAGE_HEADING = "Stick to your principles"
26
+
27
+ # The part the passage plays as the principle's first member. It carries no check: a command stored in the
28
+ # committed map runs on every machine that reads it, where principles may not be installed or may run from
29
+ # another path (D71).
30
+ PASSAGE_ROLE = "States the practice of keeping every principle held, in every session"
31
+
32
+ # The part a passage the project already has plays when it carries one of the passage's steps in its place,
33
+ # so the step is said once (#895, #911).
34
+ STEP_ROLE = "Says, for this project, one of the keeping steps"
35
+
36
+ # The part any other place that keeps the principles held plays as a member, such as a timer or the committed
37
+ # setting that enables the plugin: the agent replaces STEP with the keeping step it runs and TRIGGER with what
38
+ # starts it, a hook, a timer or a reader (D75).
39
+ KEEPING_PLACE_ROLE = "Runs STEP, started by TRIGGER"
40
+
41
+ # The repair line that sets this principle up, for a refusal of its id before its map exists: setup writes no
42
+ # map, and the add-member it prints creates the map with its first member (T74).
43
+ SET_UP = "To set it up: #{Error::EXECUTABLE} show-meta-principle-passage".freeze #: String
44
+
45
+ # The fields this principle's map starts with as setup starts it, with no members; the add-member setup prints
46
+ # writes the map with its first member (T74).
47
+ #: () -> Hash[String, untyped]
48
+ def self.starting_fields = { "description" => DESCRIPTION, "members" => [], "activation" => ACTIVATION.to_h }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,85 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "project_history"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # The files that broken members name but that cannot be read, each with the members it held, so one rename
11
+ # reads as one new path to find (#273). Each member still takes its own move: whether every one of them
12
+ # landed in the same place is the agent's judgement. Beside a path, git_detected_rename gives any rename git
13
+ # detects for it, which the agent checks before using (#276).
14
+ class MissingFiles
15
+ # How to read a git_detected_rename, wherever this tool gives one beside a missing file.
16
+ RENAME_MATCH = "check before you use it: its similarity is git's percentage of the old file found in the " \
17
+ "new one, and 50 is git's default cut-off for calling it a rename."
18
+ # What a broken_members repair says of missing_files, so each refusal says it the same way.
19
+ REPAIR = "missing_files groups the members of each file that cannot be read: a renamed file usually takes " \
20
+ "one new path for all of them. git_detected_rename, when given, is git's match for that path, to " \
21
+ "#{RENAME_MATCH}".freeze #: String
22
+ # broken holds each broken member's entry: its "id", any "principle", and its refusal's "code" and "path".
23
+ #: (Array[Hash[String, untyped]], ?principle: String?, ?history: ProjectHistory?) -> void
24
+ def initialize(broken, principle: nil, history: nil)
25
+ @broken = broken
26
+ @principle = principle
27
+ @history = history
28
+ freeze
29
+ end
30
+
31
+ # The broken members, each whose file is missing with a repair that points to its missing_files entry, so
32
+ # a reader of the member alone sees any rename git detected (#285).
33
+ #: () -> Array[Hash[String, untyped]]
34
+ def members
35
+ @broken.map do |entry|
36
+ next entry unless missing?(entry)
37
+
38
+ entry.merge("repair" => "Its file, #{entry.fetch("path")}, is missing: see that path in missing_files, " \
39
+ "with any rename git detected, then run a command from repair_with.")
40
+ end
41
+ end
42
+
43
+ # The broken_members refusal for these members, saying how to repair each. note comes before what
44
+ # missing_files says, and rerun names what to run once they are repaired.
45
+ # rerun is the command to run once the members are repaired, or nil when it is the one the agent ran.
46
+ #: (String, ?rerun: String?, ?note: String) -> Error
47
+ def refusal(message, rerun: nil, note: "")
48
+ Error.new("broken_members", message,
49
+ "For each member listed, say whether it moved or is gone, and run the matching command in its " \
50
+ "repair_with: the move-member, with its NEW_PATH, HEADING or QUERY filled in, if it moved, or the " \
51
+ "remove-member if it is gone. #{note}#{REPAIR}" \
52
+ "#{rerun ? "\nThen run it again: #{rerun}" : " Then run the command again."}")
53
+ .including(**({ "principle" => @principle } if @principle), "members" => members, "missing_files" => to_a)
54
+ end
55
+
56
+ # Each missing path, in the order first named, with the members that name it.
57
+ #: () -> Array[Hash[String, untyped]]
58
+ def to_a
59
+ missing = @broken.select { |entry| missing?(entry) }
60
+ missing.group_by { |entry| entry.fetch("path") }.map do |path, entries|
61
+ { "path" => path, "members" => entries.map { |entry| named(entry) } }.merge(renamed(path))
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ #: (Hash[String, untyped]) -> bool
68
+ def missing?(entry)
69
+ entry["code"] == "unreadable_file" && !entry["path"].nil?
70
+ end
71
+
72
+ # git_detected_rename for path, when git detects one.
73
+ #: (String) -> Hash[String, untyped]
74
+ def renamed(path)
75
+ rename = @history&.rename_of(path)
76
+ rename ? { "git_detected_rename" => rename } : {}
77
+ end
78
+
79
+ #: (Hash[String, untyped]) -> Hash[String, String]
80
+ def named(entry)
81
+ { "principle" => entry["principle"] || @principle, "id" => entry["id"] }.compact
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
6
+ module Models
7
+ # A member id an answer named that the map does not hold. A member id copies its heading exactly, so
8
+ # an agent typing it may use a straight apostrophe for a curly one, a hyphen for a dash, or other
9
+ # case and spacing (alpha 011 friction 12). This finds the mapped id it differs from only so, to name
10
+ # in the refusal; this tool never accepts the misspelt id itself.
11
+ class MisspeltMemberId
12
+ APOSTROPHES = /[‘’‚‛′ʼ`´]/
13
+ QUOTES = /[“”„‟″]/
14
+ DASHES = /[‐-―−]/
15
+
16
+ #: (String) -> void
17
+ def initialize(id)
18
+ @id = id
19
+ freeze
20
+ end
21
+
22
+ # The one mapped id spelled like this one apart from punctuation, case and spacing, or nil.
23
+ #: (Array[String]) -> String?
24
+ def closest(mapped)
25
+ matches = mapped.select { |each| loose(each) == loose(@id) }
26
+ matches.first if matches.one?
27
+ end
28
+
29
+ # What a refusal says of this id: that it is not mapped, and the mapped id it is spelled like; nil when none.
30
+ #: (Array[String]) -> String?
31
+ def hint(mapped)
32
+ closest = closest(mapped) or return nil
33
+ "#{@id} is not mapped; did you mean #{closest}?"
34
+ end
35
+
36
+ private
37
+
38
+ #: (String) -> String
39
+ def loose(text)
40
+ text.unicode_normalize(:nfkc).gsub(APOSTROPHES, "'").gsub(QUOTES, '"').gsub(DASHES, "-")
41
+ .downcase.split.join(" ")
42
+ end
43
+ end
44
+ end
45
+ end