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,149 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+
6
+ module Principles
7
+ # Each runs one command: reads models, applies their rules and prints through a view.
8
+ # controllers/CLAUDE.md owns the boundary.
9
+ module Controllers
10
+ # The writes to one member's entry in the committed member map: where the member is, the part it plays
11
+ # and the check that enforces it. This tool asks, the agent answers, and this tool writes the map and
12
+ # nothing else.
13
+ class MemberEntries < Command
14
+ # Each command and the method that runs it.
15
+ COMMANDS = {
16
+ "add-member" => :add_member, "move-member" => :move_member,
17
+ "change-member-role" => :change_member_role, "change-member-check" => :change_member_check,
18
+ "remove-member-check" => :remove_member_check, "remove-member" => :remove_member
19
+ }.freeze #: Hash[String, Symbol]
20
+
21
+ # Each location option and the name the agent typed it as.
22
+ LOCATION_OPTIONS = { "section" => "--section", "starting_at" => "--starting-at",
23
+ "ending_at" => "--ending-at", "query" => "--query" }.freeze #: Hash[String, String]
24
+
25
+ #: (String, String?, Hash[String, untyped]) -> untyped
26
+ def call(command, id, options)
27
+ naming_the_executable(rerun: ->(member) { rerun(command, id.to_s, member, options) }) do
28
+ method = COMMANDS[command] or unknown(command)
29
+ send(method, required_id(id), options)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # command as the agent ran it on principle id, with member in place of the member id it gave, and its options
36
+ # as given, each quoted for a shell and named as the agent typed it: a location by LOCATION_OPTIONS, any other,
37
+ # such as --path or --role, by its own one-word name (D56).
38
+ #: (String, String, String, Hash[String, untyped]) -> String
39
+ def rerun(command, id, member, options)
40
+ flags = options.except("project", "member").flat_map do |name, value|
41
+ [LOCATION_OPTIONS.fetch(name) { "--#{name}" }, value.to_s]
42
+ end
43
+ invocation.in_project(command, id, member, options: flags)
44
+ end
45
+
46
+ #: (String, Hash[String, untyped]) -> untyped
47
+ def add_member(id, options)
48
+ started = !@member_maps.mapped?(id)
49
+ map = @member_maps.addable(id)
50
+ member = located(options, { "path" => text(options["path"], "PATH") }).merge(played(options))
51
+ write_map(map.adding(member)) do |printed|
52
+ added = member_added(printed, map, member)
53
+ started ? added.merge(map_result.map_created(printed)) : added
54
+ end
55
+ end
56
+
57
+ # What add-member reports of the member it added to map: its id, lines and check, and the members, in
58
+ # this map or another, that share a line with it, so the agent sees an edit there shows on each (#383).
59
+ #: (Views::PrintedPrinciple, Models::MemberMap, Hash[String, untyped]) -> Hash[String, untyped]
60
+ def member_added(printed, map, member)
61
+ added = Models::Member.new(member)
62
+ map_result.member_added(printed, added: span(member).merge(member.slice("check")),
63
+ shared: lines_shared(added, map))
64
+ end
65
+
66
+ # The part a new member plays, and the check that enforces it when one is named (D30).
67
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
68
+ def played(options)
69
+ role = { "role" => text(options["role"], "--role") }
70
+ options["check"] ? role.merge("check" => text(options["check"], "--check")) : role
71
+ end
72
+
73
+ # The member's id and the lines it spans in its file now, so the agent sees what it located.
74
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
75
+ def span(member)
76
+ { "id" => Models::Member.new(member).id }
77
+ .merge(snapshot(Models::Member.new(member).locator).slice("first_line", "last_line"))
78
+ end
79
+
80
+ # Points an existing member at a new place (a narrower passage, a renamed heading, an anchor that was
81
+ # lost, or with --path a file that was renamed) without taking it out of the map, so it keeps its key and
82
+ # its audit (#268).
83
+ #: (String, Hash[String, untyped]) -> untyped
84
+ def move_member(id, options)
85
+ map = @member_maps.find(id)
86
+ old = map.member(text(options["member"], "MEMBER"))
87
+ member = located(options, { "path" => new_path(options, old) })
88
+ moved = span(member)
89
+ write_map(map.moving(Models::Member.new(old).id, member)) do |printed|
90
+ map_result.member_moved(printed, from: Models::Member.new(old).id, moved: moved)
91
+ end
92
+ end
93
+
94
+ # The path --path names, or else the member's own.
95
+ #: (Hash[String, untyped], Hash[String, untyped]) -> String
96
+ def new_path(options, old)
97
+ options["path"] ? text(options["path"], "--path") : old.fetch("path")
98
+ end
99
+
100
+ #: (String, Hash[String, untyped]) -> untyped
101
+ def change_member_role(id, options)
102
+ map = @member_maps.find(id)
103
+ target = Models::Member.new(map.member(text(options["member"], "MEMBER"))).id
104
+ write_map(map.changing_role(target, text(options["role"], "--role"))) do |printed|
105
+ map_result.member_replaced(printed, member: target, done: "Changed the role of #{target}.")
106
+ end
107
+ end
108
+
109
+ # Names the command that enforces a member, which the audit asks the agent to run (D30).
110
+ #: (String, Hash[String, untyped]) -> untyped
111
+ def change_member_check(id, options)
112
+ map = @member_maps.find(id)
113
+ target = Models::Member.new(map.member(text(options["member"], "MEMBER"))).id
114
+ check = text(options["check"], "--check")
115
+ changed = map.changing_check(target, check)
116
+ hidden = Models::Member.new(changed.member(target)).check_hidden_characters
117
+ write_map(changed) { |printed| map_result.check_changed(printed, member: target, hidden: hidden) }
118
+ end
119
+
120
+ #: (String, Hash[String, untyped]) -> untyped
121
+ def remove_member_check(id, options)
122
+ map = @member_maps.find(id)
123
+ target = Models::Member.new(map.member(text(options["member"], "MEMBER"))).id
124
+ write_map(map.removing_check(target)) do |printed|
125
+ map_result.member_replaced(printed, member: target,
126
+ done: "Removed the check of #{target}; the audit no longer asks for " \
127
+ "its result.")
128
+ end
129
+ end
130
+
131
+ #: (String, Hash[String, untyped]) -> untyped
132
+ def remove_member(id, options)
133
+ map = @member_maps.find(id)
134
+ target = text(options["member"], "MEMBER")
135
+ write_map(map.removing(target)) { |printed| map_result.member_removed(printed, removed: target) }
136
+ end
137
+
138
+ # The member's place in its file from --section, --starting-at and --ending-at, checked to resolve.
139
+ #: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
140
+ def located(options, member)
141
+ LOCATION_OPTIONS.each { |key, flag| member[key] = text(options[key], flag) if options[key] }
142
+ place = Models::Member.new(member)
143
+ place.check_location
144
+ snapshot(place.locator)
145
+ member
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "../views/members_a_commit_changed"
6
+
7
+ module Principles
8
+ # Each runs one command: reads models, applies their rules and prints through a view.
9
+ # controllers/CLAUDE.md owns the boundary.
10
+ module Controllers
11
+ # list-member-problems --commits-after COMMIT --hook-event EVENT, which the plugin's after-commit hook runs once a
12
+ # Bash call moved HEAD (D73): the members the commits after COMMIT changed, as the hook's JSON, or nothing. It
13
+ # exits 0 whatever it finds, since the host shows the user an error for any other status, and reports a refusal
14
+ # only when those commits could have caused it, so a broken setup does not repeat at every commit (review R8).
15
+ class MembersACommitChanged < Command
16
+ # report gives list-member-problems' result for every map, as Inventory checks it, or raises its refusal.
17
+ #: (Models::Project, tool: RunningTool, report: ^() -> Hash[String, untyped]) -> void
18
+ def initialize(project, tool:, report:)
19
+ super(project, tool: tool)
20
+ @report = report
21
+ end
22
+
23
+ # The hook's output for the commits after `after`, run for event.
24
+ #: (after: String, event: String) -> Views::Lines
25
+ def printed(after:, event:)
26
+ members = @report.call.fetch("members")
27
+ changed = @history.commits_brought_in(after).changed_by(members)
28
+ return Views::MembersACommitChanged::NOTHING if changed.empty?
29
+
30
+ view.changed(event, changed, others: members.size - changed.size)
31
+ rescue Error => e
32
+ return Views::MembersACommitChanged::NOTHING unless @history.touched_since?(after, @member_maps.readable_paths)
33
+
34
+ view.refused(event, e.payload.fetch("code"))
35
+ end
36
+
37
+ private
38
+
39
+ #: () -> Views::MembersACommitChanged
40
+ def view = Views::MembersACommitChanged.new(invocation)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,160 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "../models/check_since_last_audit"
6
+ require_relative "../models/open_findings_elsewhere"
7
+ require_relative "../views/acknowledged_since_last_audit"
8
+ require_relative "../views/diff_since_last_audit"
9
+
10
+ module Principles
11
+ # Each runs one command: reads models, applies their rules and prints through a view.
12
+ # controllers/CLAUDE.md owns the boundary.
13
+ module Controllers
14
+ # The members a full audit's request lists, which Audit hands over for start-audit: each as the agent assesses
15
+ # it, beside what the principle's audit record says of it since the last full audit.
16
+ class MembersToAssess < Command
17
+ # The members a full audit's request lists, each as the agent assesses it, and the findings other principles
18
+ # leave open on members sharing their lines, each once, which each member's mapped_elsewhere names by id (D62).
19
+ class Listing
20
+ #: Array[Hash[String, untyped]]
21
+ attr_reader :members
22
+
23
+ #: Array[Hash[String, untyped]]
24
+ attr_reader :open_findings_elsewhere
25
+
26
+ #: (members: Array[Hash[String, untyped]], open_findings_elsewhere: Array[Hash[String, untyped]]) -> void
27
+ def initialize(members:, open_findings_elsewhere:)
28
+ @members = members
29
+ @open_findings_elsewhere = open_findings_elsewhere
30
+ freeze
31
+ end
32
+ end
33
+
34
+ # principle_id names the principle whose members are listed, against its audit record now.
35
+ #: (Models::Project, tool: RunningTool, principle_id: String) -> void
36
+ def initialize(project, tool:, principle_id:)
37
+ super(project, tool: tool)
38
+ @principle_id = principle_id
39
+ @record = @audit_records.find(principle_id) #: Models::AuditRecord?
40
+ @completed = @record&.last_completed #: Models::AuditRecord?
41
+ @checks = Models::CheckSinceLastAudit.new(id: principle_id, history: @history, member_maps: @member_maps,
42
+ record: @record) #: Models::CheckSinceLastAudit
43
+ end
44
+
45
+ # Each member of audit's map as the assessment request shows it, located as current has it: id, role, lines,
46
+ # the evidence that locates it and the check that enforces it, if any, with whether the last audit already had
47
+ # it run, the edits to it acknowledged since that audit, its last assessment carried forward by audit, or why
48
+ # it is judged again (D49) with the command that shows its file's edits since that audit (#771), and the other
49
+ # principles' members sharing its lines (#719), beside the findings their principles leave open on them (D62).
50
+ #: (Models::Audit, Models::MemberSnapshots) -> Listing
51
+ def listed(audit, current)
52
+ map = audit.map
53
+ carried = carried_assessments(audit)
54
+ elsewhere = elsewhere(map, current)
55
+ members = map.members_shown.map do |fields|
56
+ member = Models::Member.new(fields)
57
+ judged = carried(member, carried)
58
+ located(member, current).merge(checked(member), acknowledged(fields), judged,
59
+ changed_since(fields, judged), shared_lines(member, elsewhere))
60
+ end
61
+ Listing.new(members: members, open_findings_elsewhere: elsewhere_listed(elsewhere, current))
62
+ end
63
+
64
+ private
65
+
66
+ # Each finding elsewhere names, once, with each evidence item's lines beside this audit's members, where current
67
+ # locates them; the project locates each item now (D64).
68
+ #: (Models::OpenFindingsElsewhere, Models::MemberSnapshots) -> Array[Hash[String, untyped]]
69
+ def elsewhere_listed(elsewhere, current)
70
+ elsewhere.listed(current.audited_lines) { |locator| @project.located_text(locator) }
71
+ end
72
+
73
+ # The other principles' members sharing a line with each member of map where current locates it, with the
74
+ # findings their records leave open on them, each record read once (#772, D62). The request id hashes this
75
+ # principle's members, not other maps or their records: another principle changing after the request is its
76
+ # own audit to judge, and does not make this one stale.
77
+ #: (Models::MemberMap, Models::MemberSnapshots) -> Models::OpenFindingsElsewhere
78
+ def elsewhere(map, current)
79
+ sharing = map.members.ids.to_h do |id|
80
+ [id, Models::MembersSharingLines.new(@project, current.of(id), id: id).elsewhere(except: @principle_id)]
81
+ end
82
+ Models::OpenFindingsElsewhere.of(sharing, @audit_records)
83
+ end
84
+
85
+ # The other principles' members sharing a line with member, each with its role there, so a contradiction
86
+ # between the two maps shows in this audit (#719), naming the findings that principle leaves open on it, so one
87
+ # problem is not raised under both (#772, D62); nothing when there are none.
88
+ #: (Models::Member, Models::OpenFindingsElsewhere) -> Hash[String, untyped]
89
+ def shared_lines(member, elsewhere)
90
+ Views::LinesSharedWithMember.new(mapped_elsewhere: elsewhere.beside(member.id)).to_h
91
+ end
92
+
93
+ # The member's last assessment carried forward, with whether any evidence it cited changed since and, in git,
94
+ # whether its file changed since the last full audit's base commit, outside the member, whose own text did not;
95
+ # or why it is judged again. These are facts: the agent may re-judge any carried member (D49).
96
+ #: (Models::Member, Models::CarriedAssessments) -> Hash[String, untyped]
97
+ def carried(member, carried)
98
+ shown = carried.shown(member.id) { |locator, sha| evidence_changed?(locator, sha) }
99
+ kept = shown["carried_forward"]
100
+ base = @completed&.base_commit
101
+ return shown unless kept && base
102
+
103
+ { "carried_forward" => kept.merge("file_changed_outside_member" =>
104
+ @history.commits_since(base, member.path).any?) }
105
+ end
106
+
107
+ # Whether the evidence at locator no longer hashes to sha, or can no longer be found.
108
+ #: (Models::Locator, String) -> bool
109
+ def evidence_changed?(locator, sha)
110
+ snapshot(locator).fetch("sha256") != sha
111
+ rescue Error
112
+ true
113
+ end
114
+
115
+ # The member as the request names and locates it: id, role, evidence and its lines now.
116
+ #: (Models::Member, Models::MemberSnapshots) -> Hash[String, untyped]
117
+ def located(member, current)
118
+ { "id" => member.id, "role" => member.role, "evidence" => [member.reference] }
119
+ .merge(current.of(member.id).lines)
120
+ end
121
+
122
+ # The command that shows the edits to a member judged again to its file since the last full audit's base
123
+ # commit, over its path then and now (#557, #771); nothing for a member carried forward, or with no such
124
+ # commit, as outside git or before any full audit.
125
+ #: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
126
+ def changed_since(fields, judged)
127
+ base = @completed&.base_commit
128
+ return {} unless judged["judge_because"] && base
129
+
130
+ paths = map_history(@principle_id).paths_from(base, Models::Member.new(fields).key, fields)
131
+ { "read_the_change" => Views::DiffSinceLastAudit.new(last_audited_commit: base, paths: paths).to_s }
132
+ end
133
+
134
+ # The edits to member acknowledged since the last full audit, which this audit judges together (D33, #312), with
135
+ # its lines then and now; its read_the_change shows the whole change (#771).
136
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
137
+ def acknowledged(fields)
138
+ last = @completed or return {}
139
+
140
+ member = Models::Member.new(fields)
141
+ made = last.members.acknowledgements_for(member.key)
142
+ return {} if made.empty?
143
+
144
+ base = last.base_commit.to_s
145
+ lines = @project.member_lines_since_last_audit(member.locator, since: base).shown
146
+ since = Views::AcknowledgedSinceLastAudit.new(last_audited_commit: base, member_lines: lines,
147
+ acknowledgements: made)
148
+ { "acknowledged_since_last_audit" => since.to_h }
149
+ end
150
+
151
+ #: (Models::Member) -> Hash[String, untyped]
152
+ def checked(member)
153
+ check = member.check or return {}
154
+ hidden = member.check_hidden_characters
155
+ { "check" => check, "check_since_last_audit" => @checks.of(member, check) }
156
+ .merge(hidden.empty? ? {} : { "check_hidden_characters" => hidden })
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,92 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "inventory"
6
+ require_relative "../models/entry_files"
7
+ require_relative "../models/runs_from"
8
+ require_relative "../models/meta_principle"
9
+ require_relative "../models/text_size"
10
+ require_relative "../views/meta_principle_result"
11
+
12
+ module Principles
13
+ # Each runs one command: reads models, applies their rules and prints through a view.
14
+ # controllers/CLAUDE.md owns the boundary.
15
+ module Controllers
16
+ # Setting principles up: shows the map of the principle that keeps every other one held, and proposes its passage
17
+ # for the agent to place with the user, since this tool writes none of the project's guidance (D67, D68); the
18
+ # add-member it prints writes the map (T74). It takes no id, since the principle is the product's own.
19
+ class MetaPrincipleSetup < Command
20
+ #: (String, String?, Hash[String, untyped]) -> untyped
21
+ def call(command, _id, _options)
22
+ naming_the_executable do
23
+ command == "show-meta-principle-passage" ? show_meta_principle_passage : unknown(command)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Writes nothing: with no map, it shows the one the printed add-member creates with the passage as its first
30
+ # member, so no memberless map waits while placement waits on an owner (T74). A map already there is shown as
31
+ # the project holds it, since the id is its identity, and its members in an entry file are named for the agent
32
+ # to judge whether one is the passage.
33
+ #: () -> untyped
34
+ def show_meta_principle_passage
35
+ map = @member_maps.addable(Models::MetaPrinciple::ID)
36
+ view = meta_principle_result
37
+ view.proposal(printed_principle(map), placement(view), map_in_project(map),
38
+ existing_principles: existing_principles).merge("wrote" => [])
39
+ end
40
+
41
+ # map as the project holds it: whether it was there before this run, its members already in an entry file, and
42
+ # whether principles is set up, its map having a member (D75).
43
+ #: (Models::MemberMap) -> Views::MetaPrincipleResult::MapInProject
44
+ def map_in_project(map)
45
+ Views::MetaPrincipleResult::MapInProject.new(
46
+ already_mapped: @member_maps.mapped?(map.id), members_in_entry_files: map.members_in(Models::EntryFiles::PATHS),
47
+ set_up: @member_maps.set_up?
48
+ )
49
+ end
50
+
51
+ # The project's other principles, as list-principles lists them, which this principle's first audit keeps held.
52
+ #: () -> Array[Hash[String, untyped]]
53
+ def existing_principles = handed(Inventory).rows(@member_maps.ids - [Models::MetaPrinciple::ID])
54
+
55
+ # Where the passage can go: how this run was started and whether the hook runs in this session, the project's
56
+ # entry files, and the passage.
57
+ #: (Views::MetaPrincipleResult) -> Views::MetaPrincipleResult::Placement
58
+ def placement(view)
59
+ runs_from = Models::RunsFrom.new(@tool.executable)
60
+ Views::MetaPrincipleResult::Placement.new(
61
+ runs_from: runs_from.to_s, session_start_hook_in_this_session: runs_from.session_start_hook_in_this_session?,
62
+ entry_files: @project.entry_files.to_a, passage: passage(view)
63
+ )
64
+ end
65
+
66
+ # The view of show-meta-principle-passage's result, with how its printed commands map the passage.
67
+ #: () -> Views::MetaPrincipleResult
68
+ def meta_principle_result
69
+ Views::MetaPrincipleResult.new(invocation, passage_mapping, in_git: @history.inside_git?)
70
+ end
71
+
72
+ # The passage member's heading and role, the role of a passage the project has that carries one of its steps,
73
+ # and the role of any other place that keeps the principles held (D75).
74
+ #: () -> Views::MetaPrincipleResult::PassageMapping
75
+ def passage_mapping
76
+ Views::MetaPrincipleResult::PassageMapping.new(heading: Models::MetaPrinciple::PASSAGE_HEADING,
77
+ role: Models::MetaPrinciple::PASSAGE_ROLE,
78
+ step_role: Models::MetaPrinciple::STEP_ROLE,
79
+ place_role: Models::MetaPrinciple::KEEPING_PLACE_ROLE)
80
+ end
81
+
82
+ # The passage's text, as the agent pastes it, with its size, which loads in every session (F9), and what it
83
+ # presumes of a teammate without principles (D71).
84
+ #: (Views::MetaPrincipleResult) -> Hash[String, untyped]
85
+ def passage(view)
86
+ text = view.passage
87
+ size = Models::TextSize.new(text).under(%w[bytes estimated_tokens])
88
+ { "text" => text, **size, "presumes" => view.presumes(size.fetch("estimated_tokens")) }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require_relative "command"
6
+ require_relative "../models/finding_write_ups"
7
+ require_relative "../models/last_completed_revision"
8
+ require_relative "../models/repoint"
9
+ require_relative "../views/repoint_result"
10
+
11
+ module Principles
12
+ # Each runs one command: reads models, applies their rules and prints through a view.
13
+ # controllers/CLAUDE.md owns the boundary.
14
+ module Controllers
15
+ # A repoint of a principle's open findings' write-ups (D59): start-audit ID --write-ups lists each open finding
16
+ # with where its write-up is now, and submit-answer points the recorded_at the agent gives in the audit record.
17
+ class Repoint < Command
18
+ # The request for principle id's open findings.
19
+ #: (String) -> Hash[String, untyped]
20
+ def start(id)
21
+ naming_the_executable do
22
+ record = @audit_records.find(id)
23
+ repoint = Models::Repoint.new(principle_id: id, record: record, history: @history)
24
+ findings = Models::FindingWriteUps.of(@project, [record].compact).listed
25
+ view.request(request_id: repoint.request_id.to_s, principle_id: id, revision: repoint.revision,
26
+ findings: findings).merge("wrote" => [])
27
+ end
28
+ end
29
+
30
+ # The result of answering request_id with response, and the record it changes, not yet written. It is
31
+ # refused once the last completed record changed after the request was printed, unless it writes nothing.
32
+ #: (Models::RequestId, untyped) -> [Hash[String, untyped], Array[Models::AuditRecord]]
33
+ def answered(request_id, response)
34
+ id = request_id.principle_id
35
+ record = @audit_records.find(id)
36
+ repoint = Models::Repoint.new(principle_id: id, record: record, history: @history)
37
+ repointed = repoint.accepted(response, at: Time.now.utc.iso8601) { |locator| snapshot(locator) }
38
+ changes = writable_changes(record, repointed, response.dig("basis", "revision"))
39
+ [view.repointed(principle_id: id, file: @audit_records.file(id), outcome: outcome(repoint, repointed)), changes]
40
+ end
41
+
42
+ private
43
+
44
+ # What the repoint did, and each other principle's open finding whose write-up cannot be found (T16).
45
+ #: (Models::Repoint, Models::AuditRecord) -> Views::RepointResult::Outcome
46
+ def outcome(repoint, repointed)
47
+ others = @audit_records.of(@member_maps.ids - [repointed.principle_id])
48
+ Views::RepointResult::Outcome.new(repointed: repoint.moved_ids(repointed),
49
+ unchanged: repoint.unchanged_ids(repointed),
50
+ not_found_elsewhere: Models::FindingWriteUps.of(@project, others).not_found)
51
+ end
52
+
53
+ # The records repointed changes from record. Refused once record changed after the request, whose revision the
54
+ # answer quotes back, was printed, unless there are none.
55
+ #: (Models::AuditRecord?, Models::AuditRecord, String?) -> Array[Models::AuditRecord]
56
+ def writable_changes(record, repointed, revision)
57
+ changes = @audit_records.changes(repointed)
58
+ Models::LastCompletedRevision.new(revision).confirm_writable(repointed.principle_id, record, changes)
59
+ changes
60
+ end
61
+
62
+ #: () -> Views::RepointResult
63
+ def view
64
+ Views::RepointResult.new(invocation)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../models/meta_principle"
5
+ require_relative "../models/principles_package"
6
+ require_relative "../views/meta_principle_passage"
7
+
8
+ module Principles
9
+ # Each runs one command: reads models, applies their rules and prints through a view.
10
+ # controllers/CLAUDE.md owns the boundary.
11
+ module Controllers
12
+ # This run of the tool, as its entry point was invoked: the executable every printed command names, and the
13
+ # version running, which only the command line can read; and so the release of principles it comes from.
14
+ class RunningTool
15
+ #: String
16
+ attr_reader :executable
17
+
18
+ # version gives the version running, read only when a command needs it, since in a checkout reading it
19
+ # asks git.
20
+ #: (executable: String, version: ^() -> String) -> void
21
+ def initialize(executable:, version:)
22
+ @executable = executable
23
+ @version = version
24
+ freeze
25
+ end
26
+
27
+ #: () -> String
28
+ def version = @version.call
29
+
30
+ # The release of principles this run comes from, as the audit of map's principle keeps it: the version running
31
+ # and the passage as setup prints it, which only a view holds; nil for a principle whose audit keeps none (D68).
32
+ #: (Models::MemberMap) -> Models::PrinciplesPackage?
33
+ def release_kept_by(map)
34
+ return unless map.keeps_package?
35
+
36
+ passage = Views::MetaPrinciplePassage.new(heading: Models::MetaPrinciple::PASSAGE_HEADING).text
37
+ Models::PrinciplesPackage.of(version: version, passage: passage)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,96 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require_relative "../error"
6
+ require_relative "schema_check"
7
+ require_relative "principles_subfolder"
8
+ require_relative "json_text"
9
+
10
+ module Principles
11
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
12
+ module Data
13
+ # The audit records: one JSON file per principle under .principles/audits/, committed in the audited
14
+ # repository beside its map. A record holds the last audit's base commit, each member's hash and each
15
+ # finding's id, members and action, never the agent's prose (D24, D25).
16
+ class AuditFiles
17
+ # The key a record named its principle under before the rename to principles (#536).
18
+ EARLIER_ID_KEY = "memeplex_id"
19
+
20
+ #: (PrinciplesSubfolder) -> void
21
+ def initialize(directory)
22
+ @directory = directory
23
+ end
24
+
25
+ # Which of the folder and its earlier name hold this subfolder (#536).
26
+ #: () -> Array[String]
27
+ def held_in
28
+ @directory.held_in
29
+ end
30
+
31
+ #: (String) -> bool
32
+ def exist?(id)
33
+ @directory.exist?(id)
34
+ end
35
+
36
+ # The record of id's last audit, or nil when it has none, checked against its schema (D29).
37
+ #: (String) -> Hash[String, untyped]?
38
+ def read(id)
39
+ return unless exist?(id)
40
+
41
+ parse(id, @directory.read(id, shown_as: relative(id)))
42
+ end
43
+
44
+ # The record of id from its text, with an older version's key upgraded, checked against its schema (D29).
45
+ #: (String, String) -> Hash[String, untyped]
46
+ def parse(id, text)
47
+ record = upgraded(JsonText.new(text).parsed)
48
+ problems = SchemaCheck.new("audit-record").problems(record)
49
+ return record if problems.empty?
50
+
51
+ raise Error.new("invalid_audit_record", "#{relative(id)} is not in the shape this tool writes",
52
+ "Repair each field listed, or restore the record from version control.", relative(id),
53
+ fields: problems)
54
+ rescue JSON::ParserError => e
55
+ raise Error.new("unparseable_audit", e.message,
56
+ "Repair this audit record or restore it from version control.", relative(id))
57
+ end
58
+
59
+ #: (String, Hash[String, untyped]) -> void
60
+ def write(id, record)
61
+ @directory.write(id, "#{JSON.pretty_generate(record)}\n")
62
+ end
63
+
64
+ #: (String) -> String
65
+ def relative(id)
66
+ File.join(PrinciplesSubfolder::FOLDER, "audits", "#{id}.json")
67
+ end
68
+
69
+ # Where record id was kept before the folder took its name now, so it can be read at earlier commits (#536).
70
+ #: (String) -> String
71
+ def earlier_relative(id)
72
+ File.join(PrinciplesSubfolder::EARLIER_FOLDER, "audits", "#{id}.json")
73
+ end
74
+
75
+ private
76
+
77
+ # A record an older version wrote names its principle under EARLIER_ID_KEY, in the record and in the last
78
+ # completed record an open one keeps. It is read under principle_id, in the same place, so the next save
79
+ # writes the new key only; when both are present, principle_id is kept.
80
+ #: (untyped) -> untyped
81
+ def upgraded(record)
82
+ return record unless record.is_a?(Hash)
83
+
84
+ renamed = renamed_id(record)
85
+ kept = renamed["last_completed"]
86
+ kept.is_a?(Hash) ? renamed.merge("last_completed" => renamed_id(kept)) : renamed
87
+ end
88
+
89
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
90
+ def renamed_id(record)
91
+ current = record.key?("principle_id") ? record.except(EARLIER_ID_KEY) : record
92
+ current.to_h { |key, value| [key == EARLIER_ID_KEY ? "principle_id" : key, value] }
93
+ end
94
+ end
95
+ end
96
+ end