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,221 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require_relative "audited_member_lines"
6
+ require_relative "evidence_item"
7
+ require_relative "evidence_replacements"
8
+ require_relative "recorded_finding"
9
+ require_relative "recorded_members"
10
+
11
+ module Principles
12
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
13
+ module Models
14
+ # One finding an audit record left open, deferred or accepted, with the principle and the time of that audit:
15
+ # what list-open-findings prints of it, and what the next full audit is asked to decide on it (D35).
16
+ # PriorFindings is the next audit's view of all of them, and checks its decisions. It carries the evidence it
17
+ # was raised with, which list-open-findings and the next full audit's request print, and which that audit keeps
18
+ # when it keeps the finding (D64, #839).
19
+ class OpenFinding
20
+ # members are the members as the audit that left this finding open recorded them, against which the next
21
+ # audit says whether each changed since.
22
+ #: (RecordedFinding, principle_id: String, audited_at: String?, members: RecordedMembers) -> void
23
+ def initialize(finding, principle_id:, audited_at:, members:)
24
+ @finding = finding
25
+ @principle_id = principle_id
26
+ @audited_at = audited_at
27
+ @members = members
28
+ freeze
29
+ end
30
+
31
+ # The finding as list-open-findings prints it, with its principle, when its audit was made and when it was
32
+ # first raised, so how long it has waited shows (#629), what it says it waits on, since when (D52), and the
33
+ # evidence its record keeps, or that it keeps none (D64, #839).
34
+ #: () -> Hash[String, untyped]
35
+ def listed
36
+ { "principle" => @principle_id, "finding_id" => id, "member_ids" => @finding.member_ids, **problem,
37
+ "action" => @finding.action, "recorded_at" => recorded_at&.to_h, "audited_at" => @audited_at,
38
+ "first_raised_at" => first_raised_at, **raised_in, **waiting, **kept_evidence }
39
+ end
40
+
41
+ # The finding as the next full audit's request lists it, with when it was first raised, and what it waits on,
42
+ # since when, for the agent to confirm (D52).
43
+ #: () -> Hash[String, untyped]
44
+ def for_next_audit
45
+ { "finding_id" => id, "member_ids" => @finding.member_ids, **problem,
46
+ **present("recorded_at", recorded_at&.to_h),
47
+ "action" => @finding.action,
48
+ "first_raised_at" => first_raised_at, **raised_in, **waiting }
49
+ end
50
+
51
+ # The finding as the next full audit's request lists it, against the map's members now and each one's hash
52
+ # now, hashes_now: under its members' ids now, with whether each changed since the audit that left it open,
53
+ # what its record did not keep of it, and the evidence it keeps, so the agent deciding it sees what it rests on
54
+ # (#839). Given since, each evidence item also carries what since says of it now (#858, #877).
55
+ #: (MapMembers, Hash[String, String]) ?{ (EvidenceItem) -> Hash[String, bool] }
56
+ #| -> Hash[String, untyped]
57
+ def for_next_audit_against(members_now, hashes_now, &)
58
+ finding = for_next_audit
59
+ ids = ids_now(members_now)
60
+ since = ids.to_h { |id| [id, since_this_audit(members_now, hashes_now, id)] }
61
+ finding.merge("member_ids" => ids, "members_since" => since, **text_not_kept,
62
+ **kept_evidence(evidence_since(&)))
63
+ end
64
+
65
+ # The finding as the next full audit's record keeps it, before that audit's decision on it: under its members'
66
+ # ids now, with its write-up, when it was first raised and the commit an earlier first_raised_at cited (D53).
67
+ #: (MapMembers) -> Hash[String, untyped]
68
+ def kept_against(members_now)
69
+ { "id" => id, "member_ids" => ids_now(members_now), **problem, "action" => @finding.action,
70
+ **present("recorded_at", recorded_at&.to_h), "first_raised_at" => first_raised_at, **raised_in }
71
+ end
72
+
73
+ # The principle whose record left it open.
74
+ #: String
75
+ attr_reader :principle_id
76
+
77
+ # The finding's id in its record.
78
+ #: () -> String
79
+ def id = @finding.id
80
+
81
+ # Where its write-up is, or nil when it was never written up.
82
+ #: () -> WriteUpPointer?
83
+ def recorded_at = @finding.recorded_at
84
+
85
+ # What its audit did about it: deferred or accepted.
86
+ #: () -> String?
87
+ def action = @finding.action
88
+
89
+ # Whether its record keeps what it says is wrong; one that keeps none is marked text_not_kept for the next full
90
+ # audit to supply.
91
+ #: () -> bool
92
+ def states_problem? = !@finding.problem.nil?
93
+
94
+ # The locators of the evidence it was raised with, or nil when its record keeps none (D64).
95
+ #: () -> Array[Hash[String, untyped]]?
96
+ def evidence = @finding.evidence
97
+
98
+ # Whether its record keeps the evidence it was raised with (D64).
99
+ #: () -> bool
100
+ def evidence_kept? = @finding.evidence_kept?
101
+
102
+ # The ids now, as the next full audit's request lists them, of its members no longer in the map, which its
103
+ # members_since calls removed.
104
+ #: (MapMembers) -> Array[String]
105
+ def removed_from(members_now) = ids_now(members_now).reject { |member_id| members_now.with_id(member_id) }
106
+
107
+ # The new places a decision's problem_evidence gives the evidence its record keeps, so the check that an item is
108
+ # no longer found and the swap read the same items (D70); none when its record keeps none, which problem_evidence
109
+ # gives instead (D64).
110
+ #: (Array[Hash[String, untyped]]?) -> EvidenceReplacements
111
+ def replacements(problem_evidence)
112
+ kept = evidence or return EvidenceReplacements.new([], [])
113
+
114
+ EvidenceReplacements.new(kept, problem_evidence || [])
115
+ end
116
+
117
+ # The evidence its next record keeps after a decision gives problem_evidence: the evidence it was raised with,
118
+ # each item the decision gave a new place swapped for it (D70), or, when its record keeps none, the evidence the
119
+ # decision gave, kept as plain evidence since the record does not care which audit gave it (D64).
120
+ #: (Array[Hash[String, untyped]]?) -> Array[Hash[String, untyped]]?
121
+ def evidence_after(problem_evidence) = evidence ? replacements(problem_evidence).evidence : problem_evidence
122
+
123
+ # Whether it names the member known by key, under whichever id its audit recorded that member, so a member
124
+ # renamed since is still found (#772).
125
+ #: (String) -> bool
126
+ def on?(key) = @finding.member_ids.any? { |member_id| @members.key_of(member_id) == key }
127
+
128
+ # The finding as another principle's full audit lists it, once, under open_findings_elsewhere, so the same
129
+ # problem is not raised again there (#772, D62), with each item of its evidence and what its lines are beside
130
+ # lines, that audit's members, where locate finds the item now, or that its record keeps none (D64).
131
+ #: (AuditedMemberLines) { (Locator) -> LocatedText } -> Hash[String, untyped]
132
+ def listed_elsewhere(lines, &)
133
+ located = items&.map { |item| lines.facts_of(item, &) }
134
+ { "principle_id" => @principle_id, "finding_id" => id, **problem, **present("action", @finding.action),
135
+ **kept_evidence(located) }
136
+ end
137
+
138
+ # When the finding was first raised, which orders the findings by how long each has waited.
139
+ #: () -> Time
140
+ def first_raised_time = Time.iso8601(first_raised_at.to_s)
141
+
142
+ # The finding as the session-start line names it: its principle and id, the whole days it has waited since it
143
+ # was first raised until now, and what it says it waits on, when it says (D47, D52).
144
+ #: (Time) -> Hash[String, untyped]
145
+ def waited_at(now)
146
+ { "principle" => @principle_id, "finding_id" => id,
147
+ "days_open" => ((now - first_raised_time) / 86_400).floor, **present("waits_on", @finding.waits_on) }
148
+ end
149
+
150
+ # Whether it records no waits_on, so the session-start line can count those whose line it cannot show (#736).
151
+ #: () -> bool
152
+ def without_waits_on? = !@finding.waits_on
153
+
154
+ # The one line it says it waits on, or nil when it says it waits on nothing (D52).
155
+ #: () -> String?
156
+ def waits_on = @finding.waits_on
157
+
158
+ # What the finding says it waits on, and since when: its own waits_on_since when an earlier audit asked it, or
159
+ # else this audit's time; nothing when it says it waits on nothing (D52).
160
+ #: () -> Hash[String, untyped]
161
+ def waiting
162
+ line = @finding.waits_on or return {}
163
+ { "waits_on" => line, "waits_on_since" => @finding.waits_on_since || @audited_at }
164
+ end
165
+
166
+ # Its own first_raised_at when an earlier audit kept it, or else this audit's time.
167
+ #: () -> String?
168
+ def first_raised_at = @finding.first_raised_at || @audited_at
169
+
170
+ private
171
+
172
+ # Its members' ids now, each member found by key so a member renamed since keeps its new id (#772).
173
+ #: (MapMembers) -> Array[String]
174
+ def ids_now(members_now) = @finding.member_ids.map { |id| id_now(members_now, @members.key_of(id)) || id }
175
+
176
+ #: (MapMembers, String) -> String?
177
+ def id_now(members_now, key) = members_now.with_key(key)&.id
178
+
179
+ #: (MapMembers, Hash[String, String], String) -> String
180
+ def since_this_audit(members_now, hashes_now, id)
181
+ member = members_now.with_id(id) or return "removed"
182
+ @members.freshness(member.key, hashes_now.fetch(id))
183
+ end
184
+
185
+ # The commit whose record an audit cited for an earlier first_raised_at, when one did (D53).
186
+ #: () -> Hash[String, untyped]
187
+ def raised_in = present("raised_in", @finding.raised_in)
188
+
189
+ # Its evidence items, each with what since says of it now, or as kept without it (#858, #877).
190
+ #: () ?{ (EvidenceItem) -> Hash[String, bool] } -> Array[Hash[String, untyped]]?
191
+ def evidence_since(&since)
192
+ return @finding.evidence unless since
193
+
194
+ items&.map { |item| item.with_facts(since.call(item)) }
195
+ end
196
+
197
+ # The evidence it was raised with, as items, or nil when its record keeps none (D64).
198
+ #: () -> Array[EvidenceItem]?
199
+ def items = @finding.evidence&.map { |fields| EvidenceItem.new(fields) }
200
+
201
+ # A mark when its record keeps no problem statement, for the next full audit to supply.
202
+ #: () -> Hash[String, untyped]
203
+ def text_not_kept = states_problem? ? {} : { "text_not_kept" => true }
204
+
205
+ # The evidence its record keeps, as items, which another principle's audit gives with each item's lines, or the
206
+ # mark that it keeps none, as a record written before this tool kept evidence does (D64, #839).
207
+ #: (?Array[Hash[String, untyped]]?) -> Hash[String, untyped]
208
+ def kept_evidence(listed = @finding.evidence)
209
+ evidence_kept? ? { "evidence" => listed } : { "evidence_not_kept" => true }
210
+ end
211
+
212
+ # What it says is wrong, under its key, or nothing when the record kept no text for it.
213
+ #: () -> Hash[String, untyped]
214
+ def problem = present("problem", @finding.problem)
215
+
216
+ # value under name, or nothing when there is none, as the record leaves out a field it has no value for.
217
+ #: (String, untyped) -> Hash[String, untyped]
218
+ def present(name, value) = value.nil? ? {} : { name => value }
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "members_elsewhere"
6
+ require_relative "open_finding"
7
+
8
+ module Principles
9
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
10
+ module Models
11
+ # The findings other principles' records leave open on members sharing lines with a full audit's members, read
12
+ # once per principle: each member's mapped_elsewhere names them by id, and the request lists each once, since a
13
+ # whole file's findings printed in full beside every passage within it (#772, #786, D62).
14
+ class OpenFindingsElsewhere
15
+ # sharing gives, under each audited member's id, the other principles' members sharing its lines.
16
+ #: (Hash[String, MembersElsewhere], AuditRecords) -> OpenFindingsElsewhere
17
+ def self.of(sharing, records)
18
+ ids = sharing.values.flat_map(&:principle_ids).uniq
19
+ new(sharing, ids.to_h { |id| [id, records.left_open_or_none(id)] })
20
+ end
21
+
22
+ #: (Hash[String, MembersElsewhere], Hash[String, Array[OpenFinding]]) -> void
23
+ def initialize(sharing, left_open_by_principle)
24
+ @sharing = sharing
25
+ @left_open_by_principle = left_open_by_principle
26
+ freeze
27
+ end
28
+
29
+ # The other principles' members sharing the lines of the audited member member_id, each naming by id the
30
+ # findings left open on it.
31
+ #: (String) -> Array[Hash[String, untyped]]
32
+ def beside(member_id) = @sharing.fetch(member_id).naming(@left_open_by_principle)
33
+
34
+ # Each finding left open on any of them, once, with its principle, and what its evidence's lines are beside
35
+ # lines, this audit's members, where locate finds each item now (D64): a finding is the same one when its
36
+ # principle and id are.
37
+ #: (AuditedMemberLines) { (Locator) -> LocatedText } -> Array[Hash[String, untyped]]
38
+ def listed(lines, &)
39
+ @sharing.values.flat_map { |members| members.open_among(@left_open_by_principle) }
40
+ .uniq { |finding| [finding.principle_id, finding.id] }
41
+ .map { |finding| finding.listed_elsewhere(lines, &) }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_problems"
5
+ require_relative "member_state"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # Every principle of the project but one, each as list-member-problems reports its members, which an audit of
11
+ # that one names when their members need an audit of their own: a completed audit for those its fixes changed
12
+ # (#410, #823), and the meta-principle's full audit for those no audit has judged as they are now (D71).
13
+ class OtherPrinciples
14
+ # Each principle of project but the one id names, its members as MemberProblems gives them; a map this tool
15
+ # cannot read is among them, as list-member-problems lists it.
16
+ #: (Project, but: String) -> OtherPrinciples
17
+ def self.of(project, but:)
18
+ new((project.member_maps.ids - [but]).to_h { [it, MemberProblems.for(project, it, listing_all: true)] })
19
+ end
20
+
21
+ # problems maps each principle's id to its members' problems.
22
+ #: (Hash[String, MemberProblems]) -> void
23
+ def initialize(problems)
24
+ @problems = problems.freeze #: Hash[String, MemberProblems]
25
+ freeze
26
+ end
27
+
28
+ # Each principle with a member in paths whose own text changed or whose reference broke, by id, with those
29
+ # members; a principle with none is left out.
30
+ #: (Array[String]) -> Hash[String, Array[MemberState]]
31
+ def changed_or_broken_in(paths) = listed { it.changed_or_broken_in(paths) }
32
+
33
+ # Each principle with members no audit has judged as they are now, by id, with those members by id and state;
34
+ # a principle with none is left out.
35
+ #: () -> Hash[String, Array[Hash[String, untyped]]]
36
+ def unjudged = listed(&:unjudged)
37
+
38
+ private
39
+
40
+ # What the block gives for each principle's problems, by id, where it gives any.
41
+ #: [T] () { (MemberProblems) -> Array[T] } -> Hash[String, Array[T]]
42
+ def listed(&)
43
+ @problems.transform_values(&).reject { |_, members| members.empty? }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
6
+ module Models
7
+ # Where a member may be read at a commit, and the hash of its text there, nil where it cannot be found. A
8
+ # one-member audit takes as the member's baseline the oldest placement that holds the text the audit record
9
+ # vouches for (#550).
10
+ class Placement
11
+ #: String
12
+ attr_reader :commit
13
+
14
+ #: Hash[String, untyped]
15
+ attr_reader :reference
16
+
17
+ #: String?
18
+ attr_reader :sha256
19
+
20
+ # Where the member is at commit, as the Locator MemberFile reads.
21
+ #: () -> Locator
22
+ def locator = Locator.new(reference)
23
+
24
+ #: (commit: String, reference: Hash[String, untyped], sha256: String?) -> void
25
+ def initialize(commit:, reference:, sha256:)
26
+ @commit = commit
27
+ @reference = reference
28
+ @sha256 = sha256
29
+ freeze
30
+ end
31
+
32
+ # Whether the member's text here is the text whose hash is recorded.
33
+ #: (String?) -> bool
34
+ def holds?(recorded) = !sha256.nil? && sha256 == recorded
35
+
36
+ # The paths a change from here to the member at now_path spans, this one first, so a diff over them follows
37
+ # a move (#551).
38
+ #: (String) -> Array[String]
39
+ def paths_to(now_path) = [reference.fetch("path"), now_path].uniq
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_file"
5
+
6
+ module Principles
7
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # A file that is not Markdown, such as a script or a config file. It has no sections, so a member is the
10
+ # whole file or a passage of it, and a # line is never read as a heading.
11
+ class PlainTextFile < MemberFile
12
+ private
13
+
14
+ # The whole text; a heading is refused, since a # line here is a comment, not a heading.
15
+ #: (Array[String], Locator) -> [Integer, Integer]
16
+ def region(lines, locator)
17
+ if locator.heading
18
+ raise Error.new("section_needs_markdown", "#{@path} is not Markdown, so it has no sections",
19
+ "Name the member by a passage with --starting-at TEXT [--ending-at TEXT], or map the " \
20
+ "whole file.", @path)
21
+ end
22
+
23
+ if locator.query
24
+ raise Error.new("query_needs_frontmatter", "#{@path} is not Markdown, so it has no frontmatter to query",
25
+ "Name the member by a passage with --starting-at TEXT, or map the whole file.", @path)
26
+ end
27
+
28
+ [0, lines.length]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "../data/principles_subfolder"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # Whether a project's maps and records have moved from .memeplex, the folder's earlier name, to .principles.
11
+ # Reading the new folder alone would show an unmoved project as unmapped, so every command that reads either
12
+ # refuses until the folder moves, or until one of two folders holding them is removed. A file an older version
13
+ # left in .memeplex beside no maps or records does not count (#536, rename brief findings 1 and 9).
14
+ class PrinciplesFolderMove
15
+ FOLDER = Data::PrinciplesSubfolder::FOLDER #: String
16
+ EARLIER = Data::PrinciplesSubfolder::EARLIER_FOLDER #: String
17
+
18
+ # held names the folders that hold maps/ or audits/.
19
+ #: (Array[String]) -> void
20
+ def initialize(held)
21
+ @held = held
22
+ freeze
23
+ end
24
+
25
+ # Refuses while the earlier folder holds maps or records.
26
+ #: () -> void
27
+ def refuse_while_pending
28
+ return unless @held.include?(EARLIER)
29
+
30
+ raise both_folders if @held.include?(FOLDER)
31
+
32
+ raise not_moved
33
+ end
34
+
35
+ private
36
+
37
+ #: () -> Error
38
+ def both_folders
39
+ Error.new("both_principles_folders",
40
+ "Both #{EARLIER} and #{FOLDER} hold maps or audit records; this version reads #{FOLDER} only",
41
+ "Removing either folder changes the user's project, so ask the user first. Compare the two " \
42
+ "before removing either: move into #{FOLDER} each map or record that only " \
43
+ "#{EARLIER} holds, and where both hold one, compare them and keep the current one; in git, " \
44
+ "git log shows which changed last. Then remove #{EARLIER} and commit.", EARLIER)
45
+ end
46
+
47
+ #: () -> Error
48
+ def not_moved
49
+ Error.new("principles_folder_not_moved",
50
+ "This project's maps or audit records are in #{EARLIER}, the folder's earlier name; this version " \
51
+ "reads #{FOLDER}",
52
+ "Moving the folder changes the user's project, so ask the user before moving it.\n" \
53
+ "To move it in git: git mv #{EARLIER} #{FOLDER}\nOutside git: mv #{EARLIER} #{FOLDER}\n" \
54
+ "Then commit the move. If anything untracked is left in #{EARLIER}, such as state.json and " \
55
+ "lock, which an older version wrote, ask the user before deleting it.", EARLIER)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "digest"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # The release of principles an audit of the meta-principle ran under: its version, and a digest of the passage
10
+ # it proposes for setup, as printed. An upgrade can change the session-start hook, the skill and the passage,
11
+ # none of which a consuming project maps, so the meta-principle's record keeps this and its next audit states
12
+ # what changed since (D68, synthesis F3 and F12).
13
+ class PrinciplesPackage
14
+ # The key the passage's digest is kept under. The record keeps its digests by key, as releases before D71 kept
15
+ # one for each of two passage forms, so their records stay valid; neither form's key is this one, so a record
16
+ # one of them wrote reads as a passage change once, at the next audit (D71).
17
+ PASSAGE = "entry_file_passage"
18
+
19
+ # The release running now, from its version and the passage's text.
20
+ #: (version: String, passage: String) -> PrinciplesPackage
21
+ def self.of(version:, passage:)
22
+ new("version" => version, "passage_sha256" => { PASSAGE => Digest::SHA256.hexdigest(passage) })
23
+ end
24
+
25
+ # The release a record kept, or nil when it kept none, as a record of another principle or one an older
26
+ # version wrote does not.
27
+ #: (Hash[String, untyped]?) -> PrinciplesPackage?
28
+ def self.kept(fields) = fields ? new(fields) : nil
29
+
30
+ #: (Hash[String, untyped]) -> void
31
+ def initialize(fields)
32
+ @fields = fields.freeze #: Hash[String, untyped]
33
+ freeze
34
+ end
35
+
36
+ #: () -> String
37
+ def version = @fields.fetch("version")
38
+
39
+ # Whether the passage's text differs from the one earlier proposed, or earlier kept no digest of it.
40
+ #: (PrinciplesPackage) -> bool
41
+ def passage_changed_since?(earlier) = earlier.passage_sha256 != passage_sha256
42
+
43
+ # As a record keeps it.
44
+ #: () -> Hash[String, untyped]
45
+ def to_h = @fields
46
+
47
+ protected
48
+
49
+ #: () -> String?
50
+ def passage_sha256 = @fields.fetch("passage_sha256")[PASSAGE]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,162 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "answer_fields"
6
+ require_relative "answer_place"
7
+ require_relative "answer_text"
8
+ require_relative "first_raised_claim"
9
+ require_relative "kept_prior_finding"
10
+ require_relative "open_finding"
11
+ require_relative "still_holds_restatement"
12
+ require_relative "waits_on"
13
+
14
+ module Principles
15
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
16
+ module Models
17
+ # One decision on a prior finding the request listed (D35), checked against that finding and this audit's
18
+ # new findings. Whether the finding still holds is the agent's judgement; this tool checks the decision's shape.
19
+ class PriorFindingDecision
20
+ KINDS = %w[still_holds resolved withdrawn raised_again].freeze
21
+ CLOSING = %w[resolved withdrawn].freeze
22
+ # What the completed result shows of each closed finding.
23
+ CLOSED_FIELDS = %w[finding_id decision reason evidence].freeze
24
+ REPAIR = "Decide it still_holds, raised_again with as, or resolved or withdrawn with a one-line reason."
25
+
26
+ # finding is the prior finding as its record left it open; at is the decision's place in the answer, such as
27
+ # /prior_finding_decisions/0, which a refusal about its pointers names; members are the map's members now, which
28
+ # a kept finding's members must still be among.
29
+ #: (Hash[String, untyped], OpenFinding, Array[String], at: String, members: MapMembers) -> void
30
+ def initialize(decision, finding, new_ids, at:, members:)
31
+ @decision = decision.reject { |key, value| key == "problem_evidence" && value == [] } #: Hash[String, untyped]
32
+ @finding = finding
33
+ @new_ids = new_ids
34
+ @members = members
35
+ @id = finding.id #: String
36
+ @place = AnswerPlace.new(at, "Prior finding #{@id}") #: AnswerPlace
37
+ freeze
38
+ end
39
+
40
+ # Refuses the decision unless its kind, reason, as, any evidence of a fix and any restated problem or pointer
41
+ # are sound. locate checks that each evidence pointer and a new recorded_at resolve, as a deferral's is checked.
42
+ #: (^(Locator) -> void) -> void
43
+ def check(locate)
44
+ kind = @decision["decision"]
45
+ invalid("#{kind.inspect} is not a decision") unless KINDS.include?(kind)
46
+ kind == "still_holds" ? holds : closed_or_raised(kind)
47
+ fix_shown(locate)
48
+ new_action
49
+ StillHoldsRestatement.new(@decision, @finding, @place, refuse: refusal).check(locate)
50
+ WaitsOn.new(@decision["waits_on"], action: kept_action).check(code: "invalid_decision",
51
+ subject: "Prior finding #{@id}")
52
+ end
53
+
54
+ # The prior finding it decides, as its record left it open.
55
+ #: OpenFinding
56
+ attr_reader :finding
57
+
58
+ # What it decides: still_holds, resolved, withdrawn or raised_again.
59
+ #: () -> String
60
+ def kind = @decision.fetch("decision")
61
+
62
+ # The id of the new finding a raised_again decision raises it as.
63
+ #: () -> String
64
+ def raised_as = @decision.fetch("as")
65
+
66
+ # The decision as the completed result shows a closed finding.
67
+ #: () -> Hash[String, untyped]
68
+ def closed = @decision.slice(*CLOSED_FIELDS)
69
+
70
+ # The finding a still_holds decision keeps, against the map's members now. Its problem_evidence is read with an
71
+ # empty list as none given: the template starts a marked finding's decision with one, which an agent leaves in
72
+ # whatever it decides (#838).
73
+ #: () -> KeptPriorFinding
74
+ def kept = KeptPriorFinding.new(@decision, @finding, @members)
75
+
76
+ # The earlier first_raised_at this decision claims, if any, to check against the record it cites (D53).
77
+ #: () -> FirstRaisedClaim
78
+ def first_raised_claim = FirstRaisedClaim.new(@decision, @finding)
79
+
80
+ private
81
+
82
+ # A still_holds decision may make its finding deferred or accepted, with a one-line reason, and a deferral
83
+ # says where it is written up, if only as null, as a correction's does (D35 brief, finding 14).
84
+ #: () -> void
85
+ def new_action
86
+ action = @decision["action"] or return
87
+ unless @decision["decision"] == "still_holds"
88
+ invalid("only still_holds may give action", "Drop action from #{@id}'s decision, or decide it still_holds.")
89
+ end
90
+ invalid("still_holds may make it only deferred or accepted") unless %w[deferred accepted].include?(action)
91
+ changed_action(action)
92
+ end
93
+
94
+ #: (String) -> void
95
+ def changed_action(action)
96
+ if action == @finding.action
97
+ invalid("it is already #{action}", "Drop action from #{@id}'s decision; it is already #{action}.")
98
+ end
99
+ invalid("a new action needs a one-line reason") unless one_line_reason?
100
+ unrecorded_deferral if action == "deferred" && !@decision.key?("recorded_at")
101
+ end
102
+
103
+ #: () -> bot
104
+ def unrecorded_deferral
105
+ invalid("deferring it needs recorded_at, or null when it is written up nowhere",
106
+ "Set recorded_at to where #{@id} is written up, as {\"path\": ..., \"heading\": ...}, or to null.")
107
+ end
108
+
109
+ # The action the finding keeps once decided, or nil when the decision closes it or raises it again (D52).
110
+ #: () -> String?
111
+ def kept_action
112
+ return nil unless @decision["decision"] == "still_holds"
113
+
114
+ @decision["action"] || @finding.action
115
+ end
116
+
117
+ #: () -> void
118
+ def holds
119
+ gone unless @finding.removed_from(@members).empty?
120
+ end
121
+
122
+ #: (String) -> void
123
+ def closed_or_raised(kind)
124
+ invalid("#{kind} needs a one-line reason") if CLOSING.include?(kind) && !one_line_reason?
125
+ invalid("raised_again needs as: the id of a new finding") if kind == "raised_again" && !raised_as_new?
126
+ end
127
+
128
+ #: () -> bool
129
+ def one_line_reason? = AnswerText.new(@decision["reason"]).one_line?
130
+
131
+ #: () -> bool
132
+ def raised_as_new? = @new_ids.include?(@decision["as"])
133
+
134
+ # A resolved decision may point at where its fix shows (D35 brief, finding 10).
135
+ #: (^(Locator) -> void) -> void
136
+ def fix_shown(locate)
137
+ return unless @decision.key?("evidence")
138
+
139
+ unless @decision["decision"] == "resolved"
140
+ invalid("only resolved may give evidence", "Drop evidence from #{@id}'s decision, or decide it resolved.")
141
+ end
142
+ AnswerFields.new(locate).evidence(@decision["evidence"], @place.child("evidence"))
143
+ end
144
+
145
+ # The decision's refusal, for a part that checks it apart: a reason and a repair, or REPAIR for nil.
146
+ #: () -> ^(String, String?) -> void
147
+ def refusal = ->(why, repair) { invalid(why, repair || REPAIR) }
148
+
149
+ #: (String, ?String) -> bot
150
+ def invalid(why, repair = REPAIR)
151
+ raise Error.new("invalid_decision", "Prior finding #{@id}: #{why}", repair)
152
+ end
153
+
154
+ #: () -> bot
155
+ def gone
156
+ removed = @finding.removed_from(@members)
157
+ raise Error.new("unknown_member", "Prior finding #{@id} names members no longer mapped: #{removed.join(", ")}",
158
+ "Decide it resolved or withdrawn with a reason, or raise it again as a new finding.")
159
+ end
160
+ end
161
+ end
162
+ end