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,46 @@
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
+ # The candidate files git ignores, which this tool names but never reads. A folder git ignores can hold many
8
+ # copies of instruction files written there by specs or builds, and listing each buries a file ignored on
9
+ # its own, such as personal settings (#406). So a file under a folder git ignores is counted under the
10
+ # shallowest such folder, and only the rest are listed.
11
+ class IgnoredCandidates
12
+ # paths are the ignored candidates; folders are every folder above one that git ignores, each ending in "/".
13
+ #: (paths: Array[String], folders: Array[String]) -> void
14
+ def initialize(paths:, folders:)
15
+ @paths = paths #: Array[String]
16
+ @folders = folders #: Array[String]
17
+ freeze
18
+ end
19
+
20
+ # The inventory's ignored files and ignored folders.
21
+ #: () -> Hash[String, untyped]
22
+ def listing = { "ignored" => on_their_own, "ignored_folders" => by_folder }
23
+
24
+ # Each ignored candidate with no folder above it that git ignores.
25
+ #: () -> Array[String]
26
+ def on_their_own
27
+ @paths.reject { |path| shallowest_folder(path) }.sort
28
+ end
29
+
30
+ # Each shallowest folder git ignores, and how many ignored candidates it holds.
31
+ #: () -> Array[Hash[String, untyped]]
32
+ def by_folder
33
+ @paths.filter_map { |path| shallowest_folder(path) }.tally.sort.map do |folder, count|
34
+ { "folder" => folder, "candidates" => count }
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ #: (String) -> String?
41
+ def shallowest_folder(path)
42
+ @folders.select { |folder| path.start_with?(folder) }.min_by(&:length)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # The digest of what a JSON value says: its objects' keys in sorted order at every depth and its lists in their
11
+ # own, so a formatter or a hand edit that only reorders a file's keys leaves it unchanged (#656). A map's
12
+ # version and an audit record's revision are each one (D51).
13
+ class JsonDigest
14
+ #: (untyped) -> void
15
+ def initialize(value)
16
+ @text = JSON.generate(in_key_order(value)) #: String
17
+ freeze
18
+ end
19
+
20
+ #: () -> String
21
+ def to_s = Digest::SHA256.hexdigest(@text)
22
+
23
+ private
24
+
25
+ #: (untyped) -> untyped
26
+ def in_key_order(value)
27
+ case value
28
+ when Hash then value.sort.to_h { |key, inner| [key, in_key_order(inner)] }
29
+ when Array then value.map { |inner| in_key_order(inner) }
30
+ else value
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,131 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "strscan"
5
+ require_relative "../error"
6
+ require_relative "json_path_string"
7
+ require_relative "json_path_selectors"
8
+
9
+ module Principles
10
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # A query in this tool's own profile of RFC 9535 JSONPath (D32): from $, name selectors in dot or bracket
13
+ # form, wildcards, and filters that keep the children whose relative paths equal strings, joined by &&.
14
+ # A query outside the profile is refused whole, never half-read, so an id always holds standard JSONPath
15
+ # and a later full implementation changes no map. It only evaluates over values already parsed as JSON.
16
+ class JsonPathQuery
17
+ # A member-name shorthand, as RFC 9535 defines it for ASCII.
18
+ SHORTHAND = /[A-Za-z_][A-Za-z0-9_]*/ #: Regexp
19
+
20
+ # The repair this tool gives when it has no exact rewrite to offer.
21
+ GRAMMAR = "Use $ followed by .name, ['name'], .* or [*], and [?@.field=='text' && ...] filters." #: String
22
+
23
+ #: (String) -> void
24
+ def initialize(query)
25
+ @query = query
26
+ @segments = parsed(StringScanner.new(query)) #: Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
27
+ freeze
28
+ end
29
+
30
+ # The nodes this query selects from value, in document order.
31
+ #: (untyped) -> Array[untyped]
32
+ def select(value)
33
+ @segments.reduce([value]) { |nodes, selector| nodes.flat_map { |node| selector.select(node) } }
34
+ end
35
+
36
+ # The name of the one top-level member this query selects, or nil when it selects anything else.
37
+ #: () -> String?
38
+ def top_level_name
39
+ only = @segments.first
40
+ only.name if @segments.length == 1 && only.is_a?(JsonPathName)
41
+ end
42
+
43
+ private
44
+
45
+ #: (StringScanner) -> Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
46
+ def parsed(scanner)
47
+ refuse("it does not start with $") unless scanner.scan("$")
48
+ segments = [] #: Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
49
+ segments << segment(scanner) until scanner.eos?
50
+ segments
51
+ end
52
+
53
+ #: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
54
+ def segment(scanner)
55
+ refuse("descendant segments (..) are outside the profile") if scanner.check("..")
56
+ return dotted(scanner) if scanner.scan(".")
57
+ return bracketed(scanner) if scanner.scan("[")
58
+
59
+ refuse("it has #{scanner.rest.inspect} where a segment should start")
60
+ end
61
+
62
+ #: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
63
+ def dotted(scanner)
64
+ return JsonPathWildcard.new if scanner.scan("*")
65
+
66
+ start = scanner.pos - 1
67
+ name = scanner.scan(SHORTHAND) or refuse("a . must be followed by a name or *")
68
+ refuse_hyphenated(scanner, start, name) if scanner.check(/-/)
69
+ JsonPathName.new(name)
70
+ end
71
+
72
+ # A name with a hyphen cannot follow a dot in RFC 9535, so the repair gives the query with it bracketed.
73
+ #: (StringScanner, Integer, String) -> bot
74
+ def refuse_hyphenated(scanner, start, name)
75
+ name += scanner.scan(/[A-Za-z0-9_-]+/).to_s
76
+ rewritten = "#{@query[0...start]}['#{name}']#{scanner.rest}"
77
+ refuse("a name with a hyphen cannot follow a dot", "Put the name in brackets: write it #{rewritten}.")
78
+ end
79
+
80
+ #: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
81
+ def bracketed(scanner)
82
+ selector = if scanner.scan("*") then JsonPathWildcard.new
83
+ elsif scanner.scan("?") then JsonPathFilter.new(conditions(scanner))
84
+ elsif scanner.check(/['"]/) then JsonPathName.new(JsonPathString.new(@query).read(scanner))
85
+ else refuse("this selector is outside the profile, which takes names, * and ?filters")
86
+ end
87
+ refuse("this selector is outside the profile, or not closed") unless scanner.scan("]")
88
+ selector
89
+ end
90
+
91
+ # The filter's conditions: @-relative paths each equal to a string, joined by &&.
92
+ #: (StringScanner) -> Array[[Array[String], String]]
93
+ def conditions(scanner)
94
+ found = [condition(scanner)]
95
+ found << condition(scanner) while scanner.scan(/\s*&&\s*/)
96
+ scanner.skip(/\s*/)
97
+ found
98
+ end
99
+
100
+ #: (StringScanner) -> [Array[String], String]
101
+ def condition(scanner)
102
+ scanner.skip(/\s*/)
103
+ refuse("a filter must compare an @ path") unless scanner.scan("@")
104
+ path = [] #: Array[String]
105
+ path << relative_name(scanner) while scanner.check(/\.|\[/)
106
+ refuse("a filter compares with == only") unless scanner.scan(/\s*==\s*/)
107
+ refuse("a filter compares with a quoted string only") unless scanner.check(/['"]/)
108
+ [path, JsonPathString.new(@query).read(scanner)]
109
+ end
110
+
111
+ #: (StringScanner) -> String
112
+ def relative_name(scanner)
113
+ if scanner.scan(".")
114
+ scanner.scan(SHORTHAND) or refuse("a . in a filter path must be followed by a name")
115
+ else
116
+ scanner.scan("[")
117
+ refuse("a [ in a filter path must hold a quoted name") unless scanner.check(/['"]/)
118
+ name = JsonPathString.new(@query).read(scanner)
119
+ refuse("a [ in a filter path is not closed") unless scanner.scan("]")
120
+ name
121
+ end
122
+ end
123
+
124
+ #: (String, ?String) -> bot
125
+ def refuse(why, repair = GRAMMAR)
126
+ raise Error.new("unsupported_query", "#{@query.inspect} is outside the supported JSONPath profile: #{why}",
127
+ repair)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,64 @@
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
+ # The selector * in a JSONPath query: every member value of an object, or every element of an array.
8
+ class JsonPathWildcard
9
+ #: () -> void
10
+ def initialize
11
+ freeze
12
+ end
13
+
14
+ #: (untyped) -> Array[untyped]
15
+ def select(node)
16
+ return node.values if node.is_a?(Hash)
17
+
18
+ node.is_a?(Array) ? node : []
19
+ end
20
+ end
21
+
22
+ # A name selector in a JSONPath query: the member of an object with that name, if it has one.
23
+ class JsonPathName
24
+ #: String
25
+ attr_reader :name
26
+
27
+ #: (String) -> void
28
+ def initialize(name)
29
+ @name = name
30
+ freeze
31
+ end
32
+
33
+ #: (untyped) -> Array[untyped]
34
+ def select(node)
35
+ node.is_a?(Hash) && node.key?(@name) ? [node.fetch(@name)] : []
36
+ end
37
+ end
38
+
39
+ # A filter selector in this tool's profile: the children for which every condition holds, each condition an
40
+ # @-relative path of names leading to a string equal to the condition's string.
41
+ class JsonPathFilter
42
+ #: (Array[[Array[String], String]]) -> void
43
+ def initialize(conditions)
44
+ @conditions = conditions
45
+ freeze
46
+ end
47
+
48
+ #: (untyped) -> Array[untyped]
49
+ def select(node)
50
+ JsonPathWildcard.new.select(node).select { |child| holds_for?(child) }
51
+ end
52
+
53
+ private
54
+
55
+ #: (untyped) -> bool
56
+ def holds_for?(child)
57
+ @conditions.all? do |path, expected|
58
+ found = path.reduce(child) { |value, name| value.is_a?(Hash) ? value[name] : nil }
59
+ found.is_a?(String) && found == expected
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,76 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "strscan"
5
+ require_relative "../error"
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
+ # One quoted name or string in a JSONPath query, read as RFC 9535 writes it: single or double quotes, the
11
+ # escapes it defines, and no raw control character. Anything else refuses the whole query.
12
+ class JsonPathString
13
+ # The escapes a quoted name or string may hold, and what each stands for.
14
+ ESCAPES = { "b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t", "/" => "/", "\\" => "\\",
15
+ "'" => "'", "\"" => "\"" }.freeze #: Hash[String, String]
16
+
17
+ # query is the whole query, which a refusal names.
18
+ #: (String) -> void
19
+ def initialize(query)
20
+ @query = query
21
+ freeze
22
+ end
23
+
24
+ # The string that starts at scanner, its quotes and escapes read; scanner is left after its closing quote.
25
+ #: (StringScanner) -> String
26
+ def read(scanner)
27
+ string(scanner)
28
+ end
29
+
30
+ private
31
+
32
+ # A quoted string, single or double, with its escapes read.
33
+ #: (StringScanner) -> String
34
+ def string(scanner)
35
+ quote = scanner.getch.to_s
36
+ text = +""
37
+ until scanner.scan(quote)
38
+ refuse("a quoted string is not closed") if scanner.eos?
39
+ text << (scanner.scan("\\") ? escaped(scanner, quote) : unescaped(scanner))
40
+ end
41
+ text
42
+ end
43
+
44
+ # One escape after a backslash. A quote may be escaped only inside quotes of its own kind, and a \u escape
45
+ # naming half of a surrogate pair is outside the profile.
46
+ #: (StringScanner, String) -> String
47
+ def escaped(scanner, quote)
48
+ if scanner.scan(/u\h{4}/)
49
+ point = scanner.matched.to_s[1, 4].to_s.hex
50
+ refuse("a \\u escape for a surrogate is outside the profile") if point.between?(0xD800, 0xDFFF)
51
+ return [point].pack("U")
52
+ end
53
+ char = scanner.getch.to_s
54
+ other_quote = quote == "'" ? '"' : "'"
55
+ refuse("a string holds an escape JSONPath does not define") if char == other_quote
56
+
57
+ ESCAPES.fetch(char) { refuse("a string holds an escape JSONPath does not define") }
58
+ end
59
+
60
+ # One character as written; a control character must be escaped.
61
+ #: (StringScanner) -> String
62
+ def unescaped(scanner)
63
+ char = scanner.getch.to_s
64
+ refuse("a string holds an unescaped control character") if char.ord < 0x20
65
+
66
+ char
67
+ end
68
+
69
+ #: (String) -> bot
70
+ def refuse(why)
71
+ raise Error.new("unsupported_query", "#{@query.inspect} is outside the supported JSONPath profile: #{why}",
72
+ "Quote names with ' or \", escaping only what RFC 9535 defines.")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,70 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "first_raised_claim"
5
+ require_relative "map_members"
6
+ require_relative "open_finding"
7
+ require_relative "waits_on"
8
+
9
+ module Principles
10
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # A prior finding a still_holds decision kept (D35): the finding as the next record keeps it, and what the decision
13
+ # dropped or replaced of it, which the completed result names so a change not meant can be seen and put back.
14
+ class KeptPriorFinding
15
+ # decision is the checked still_holds decision on finding, the prior finding as its record left it open; members
16
+ # are the map's members now, under whose ids it is recorded.
17
+ #: (Hash[String, untyped], OpenFinding, MapMembers) -> void
18
+ def initialize(decision, finding, members)
19
+ @decision = decision
20
+ @finding = finding
21
+ @record = record(members) #: Hash[String, untyped]
22
+ freeze
23
+ end
24
+
25
+ # The finding as the record keeps it, with any problem, action or pointer its decision gave, and any earlier
26
+ # first_raised_at it claimed with the commit it cited (D53); a null pointer drops the one it had. Its evidence is
27
+ # as OpenFinding#evidence_after says, from its own state rather than as the request printed it (D64, D70).
28
+ #: () -> Hash[String, untyped]
29
+ def to_h = @record
30
+
31
+ # The pointer it had that its decision dropped with a null recorded_at, or nil when it dropped none (#374).
32
+ #: () -> Hash[String, untyped]?
33
+ def dropped_write_up
34
+ had = @finding.recorded_at
35
+ return unless had && @decision.key?("recorded_at") && @decision["recorded_at"].nil?
36
+
37
+ { "finding_id" => @finding.id, "recorded_at" => had.to_h }
38
+ end
39
+
40
+ # What it waited on that its decision dropped, with null or by accepting it, or nil when it dropped nothing (D52).
41
+ #: () -> Hash[String, untyped]?
42
+ def waits_on_dropped
43
+ return if @finding.without_waits_on? || @record.key?("waits_on")
44
+
45
+ { "finding_id" => @finding.id, **@finding.waiting }
46
+ end
47
+
48
+ # Each kept evidence item its decision gave a new place, as it was and is now (D70).
49
+ #: () -> Array[Hash[String, untyped]]
50
+ def swaps = @finding.replacements(@decision["problem_evidence"]).swaps(@finding.id)
51
+
52
+ private
53
+
54
+ #: (MapMembers) -> Hash[String, untyped]
55
+ def record(members)
56
+ kept = @finding.kept_against(members)
57
+ .merge(@decision.slice("problem", "action", "recorded_at", *FirstRaisedClaim::FIELDS),
58
+ { "evidence" => @finding.evidence_after(@decision["problem_evidence"]) }).compact
59
+ kept.merge(waiting_after(kept))
60
+ end
61
+
62
+ # What it waits on, and since when, after its decision (D52).
63
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
64
+ def waiting_after(kept)
65
+ WaitsOn.new(@decision.fetch("waits_on", @finding.waits_on), action: kept.fetch("action"))
66
+ .kept_over(@finding.waiting)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,70 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "audit_record"
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 revision of a principle's last completed audit record when a request was printed, or nil when there was
11
+ # none. An assessment's basis and a correction's carry it, so either answer is refused once that record changed,
12
+ # by another audit completing or a member's acknowledgement, rather than replacing it and what it left open
13
+ # (#646, #653).
14
+ class LastCompletedRevision
15
+ #: (String?) -> void
16
+ def initialize(revision)
17
+ @revision = revision
18
+ freeze
19
+ end
20
+
21
+ # Refuses an answer of principle_id that would write changes, when record, the record now, changed since the
22
+ # request was printed. An answer that writes nothing, as an identical retry does, is accepted (D24).
23
+ #: (String, AuditRecord?, Array[untyped]) -> void
24
+ def confirm_writable(principle_id, record, changes)
25
+ confirm_unchanged(principle_id, record) unless changes.empty?
26
+ end
27
+
28
+ # What the block gives, judging an answer of principle_id; but when the block refuses the answer while record,
29
+ # the record now, changed since the request was printed, the refusal is that the request is stale, so the agent
30
+ # does not repair an answer it must start again (#669). An answer the block refuses is not D24's identical
31
+ # retry, which passed every check, so the stale refusal always wins over it.
32
+ #: [T] (String, AuditRecord?) { () -> T } -> T
33
+ def refusing_stale_first(principle_id, record, &)
34
+ yield
35
+ rescue Error
36
+ confirm_unchanged(principle_id, record)
37
+ raise
38
+ end
39
+
40
+ # Refuses any answer of principle_id when record, the record now, changed since the request was printed.
41
+ #: (String, AuditRecord?) -> void
42
+ def confirm_unchanged(principle_id, record)
43
+ last = record&.last_completed
44
+ return if @revision == last&.revision
45
+
46
+ raise Error.new("audit_completed_since_request",
47
+ "The record of #{principle_id}'s last audit changed after this request was printed: " \
48
+ "#{what_changed(last)}, and answering it would replace that record and what it left open",
49
+ "To see what the record now leaves open: " \
50
+ "#{Error::EXECUTABLE} list-open-findings #{principle_id}\n" \
51
+ "Then start the audit again and answer the new request: " \
52
+ "#{Error::EXECUTABLE} start-audit #{principle_id}")
53
+ end
54
+
55
+ private
56
+
57
+ # What changed the record last: its removal, the newest acknowledgement since its audit, or else the audit.
58
+ # Acknowledgements are dropped when a full audit completes, so any kept is newer than the audit (D33).
59
+ #: (AuditRecord?) -> String
60
+ def what_changed(last)
61
+ return "the record was removed" unless last
62
+
63
+ id, at = last.members.latest_acknowledgement
64
+ return "#{id} was acknowledged at #{at}" if id
65
+
66
+ "an audit completed at #{last.to_h.fetch("audited_at")}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
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
+ # The lines a member spans in its file, first to last, as a LocatedText gives them, and whether the member is
8
+ # its whole file, so two members' spans are compared in one place.
9
+ class LineSpan
10
+ #: Integer
11
+ attr_reader :first_line, :last_line
12
+
13
+ #: (Integer, Integer, ?whole_file: bool) -> void
14
+ def initialize(first_line, last_line, whole_file: false)
15
+ @first_line = first_line
16
+ @last_line = last_line
17
+ @whole_file = whole_file
18
+ freeze
19
+ end
20
+
21
+ # Whether the two spans share a line.
22
+ #: (LineSpan) -> bool
23
+ def shares_a_line_with?(other)
24
+ first_line <= other.last_line && other.first_line <= last_line
25
+ end
26
+
27
+ # Whether every line of other is one of this span's lines. Lines are not text: where the two share a first or
28
+ # last line, a passage can start or end partway through it, so this span may not hold all of other's text.
29
+ #: (LineSpan) -> bool
30
+ def holds?(other)
31
+ first_line <= other.first_line && other.last_line <= last_line
32
+ end
33
+
34
+ # Whether this span surely holds all of other's text: it holds other's lines and is a whole file, which holds
35
+ # every line's text, where a passage may start or end partway through a line (#756).
36
+ #: (LineSpan) -> bool
37
+ def holds_all_text_of?(other)
38
+ @whole_file && holds?(other)
39
+ end
40
+
41
+ # Whether the two spans begin and end on the same lines. Equal lines are not equal text: a passage can end
42
+ # within its last line.
43
+ #: (LineSpan) -> bool
44
+ def same_lines_as?(other)
45
+ first_line == other.first_line && last_line == other.last_line
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_loads"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # The files a principle's members load, each once however many of its members it holds, since every member's
10
+ # loads gives its whole file's size and summing members counts a shared file once per member (second alpha
11
+ # user on #481, #483). Only session start loads its files together, so only those are totalled; a file that
12
+ # loads on files, on invocation or on delegation loads apart from the rest. A file that cannot be read has no
13
+ # size and is not totalled.
14
+ class LoadedFiles
15
+ # The files members load, each read whole once, as files hold it.
16
+ #: (Data::ProjectFiles, MapMembers, hooks: HookCommands) -> LoadedFiles
17
+ def self.read(files, members, hooks:)
18
+ new(members.paths.to_h { |path| [path, MemberLoads.of_file(files, path, hooks: hooks)] })
19
+ end
20
+
21
+ # loads holds each file's whole-file MemberLoads by path.
22
+ #: (Hash[String, MemberLoads]) -> void
23
+ def initialize(loads)
24
+ @loads = loads.select { |_path, file| file.carried? } #: Hash[String, MemberLoads]
25
+ freeze
26
+ end
27
+
28
+ # files_loaded and session_start_total, each left out when it would be empty.
29
+ #: () -> Hash[String, untyped]
30
+ def to_h
31
+ return {} if @loads.empty?
32
+
33
+ total = session_start_total
34
+ { "files_loaded" => @loads.map { |path, file| { "path" => path }.merge(file.claude_code) },
35
+ **(total ? { "session_start_total" => total } : {}) }
36
+ end
37
+
38
+ private
39
+
40
+ # The files that load whole at session start and their summed size, beside the descriptions that load at start
41
+ # while their bodies wait; each left out when there is none, and nil when there is neither.
42
+ #: () -> Hash[String, untyped]?
43
+ def session_start_total
44
+ files = summed(@loads.values.select(&:at_session_start?).map(&:size), "files", MemberLoads::FILE_SIZE_FIELDS)
45
+ descriptions = summed(@loads.values.map(&:description_at_start), "count", MemberLoads::DESCRIPTION_SIZE_FIELDS)
46
+ total = files.merge(descriptions.empty? ? {} : { "descriptions" => descriptions })
47
+ total unless total.empty?
48
+ end
49
+
50
+ # How many of sizes were measured, under counted, and their sum under each of fields; empty when none was.
51
+ #: (Array[Hash[String, Integer]], String, [String, String]) -> Hash[String, Integer]
52
+ def summed(sizes, counted, fields)
53
+ measured = sizes.reject(&:empty?)
54
+ return {} if measured.empty?
55
+
56
+ { counted => measured.size }.merge(fields.to_h { |field| [field, measured.sum { it.fetch(field) }] })
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "answer_text"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # One decision in an assessment's loading_decisions on a member the audit listed to check against the
11
+ # activation (#355). Whether the member should load as it does is the agent's judgement; this tool checks the
12
+ # decision's shape.
13
+ class LoadingDecision
14
+ # decision is as the answer gave it; index is its place in loading_decisions.
15
+ #: (untyped, Integer) -> void
16
+ def initialize(decision, index)
17
+ @decision = decision.is_a?(Hash) ? decision : {} #: Hash[String, untyped]
18
+ @index = index
19
+ freeze
20
+ end
21
+
22
+ # The member this decides, or "" when the decision names none.
23
+ #: () -> String
24
+ def member_id = @decision["member_id"].to_s
25
+
26
+ # What the record keeps of this decision once checked: its reason and the loads it accepted, from its
27
+ # member's row in the listing.
28
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
29
+ def kept(row) = { "reason" => @decision["reason"], "loads" => row.fetch("loads") }
30
+
31
+ # Refuses the decision unless it is accepted with a one-line reason.
32
+ #: () -> void
33
+ def check
34
+ return if @decision["decision"] == "accepted" && AnswerText.new(@decision["reason"]).written?
35
+
36
+ raise Error.new("invalid_loading_decision",
37
+ "loading_decisions[#{@index}] for #{@decision["member_id"]} needs decision accepted with a " \
38
+ "reason",
39
+ "Give decision accepted with a one-line reason why the member loads as it does, or, if the " \
40
+ "activation is what is wrong, change it with change-activation and run start-audit again, " \
41
+ "as the request's loads_to_check_instruction says; a finding decision is not built yet.")
42
+ end
43
+ end
44
+ end
45
+ end