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,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "decided_prior_findings"
6
+ require_relative "kept_prior_finding"
7
+ require_relative "prior_finding_decision"
8
+
9
+ module Principles
10
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # What the next full audit's decisions, once PriorFindings has checked them, do to each finding the last audit
13
+ # left open (D35): the findings kept, as the record keeps them, the ones closed, and, for each new finding that
14
+ # raises a prior one again, when that one was first raised.
15
+ class PriorFindingOutcomes
16
+ # decisions are the checked decisions, each on the finding it decides.
17
+ #: (Array[PriorFindingDecision]) -> void
18
+ def initialize(decisions)
19
+ @decisions = decisions
20
+ freeze
21
+ end
22
+
23
+ # What the decisions do to each finding, which each kept finding answers for itself.
24
+ #: () -> DecidedPriorFindings
25
+ def decided
26
+ holding = of(["still_holds"]).map(&:kept)
27
+ DecidedPriorFindings.new(
28
+ { "kept" => holding.map(&:to_h), "closed" => of(PriorFindingDecision::CLOSING).map(&:closed),
29
+ "inherited" => first_raised(of(["raised_again"])), "dropped" => holding.filter_map(&:dropped_write_up),
30
+ "waits_on_dropped" => holding.filter_map(&:waits_on_dropped),
31
+ "evidence_replaced" => holding.flat_map(&:swaps) }
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ #: (Array[String]) -> Array[PriorFindingDecision]
38
+ def of(kinds) = @decisions.select { |decision| kinds.include?(decision.kind) }
39
+
40
+ # When each finding raised again was first raised, by the id of the new finding it became. Two raised as one
41
+ # new finding are refused, since only one first_raised_at can pass on.
42
+ #: (Array[PriorFindingDecision]) -> Hash[String, String?]
43
+ def first_raised(raised)
44
+ raised.group_by(&:raised_as).to_h do |as, same|
45
+ raised_once(as, same.map { |decision| decision.finding.id })
46
+ [as, same.fetch(0).finding.first_raised_at]
47
+ end
48
+ end
49
+
50
+ #: (String, Array[String]) -> void
51
+ def raised_once(as, ids)
52
+ return if ids.one?
53
+
54
+ raise Error.new("invalid_decision", "#{ids.join(", ")} are raised again as one new finding, #{as}",
55
+ "Raise one again as #{as} and close the others as withdrawn, with a reason naming #{as}, " \
56
+ "or raise each as its own new finding.")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,109 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "audit_record"
6
+ require_relative "decided_prior_findings"
7
+ require_relative "prior_finding_outcomes"
8
+ require_relative "prior_finding_decision"
9
+ require_relative "member"
10
+
11
+ module Principles
12
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
13
+ module Models
14
+ # The findings the last completed audit deferred or accepted, which the next full audit must decide one by
15
+ # one (D35): each still holds, was resolved, was withdrawn, or is raised again as a new finding. This tool
16
+ # lists them and checks the decisions; whether a finding still holds is the agent's judgement.
17
+ class PriorFindings
18
+ # record is the last audit's record, or nil; members are the map's members now, and now each one's hash.
19
+ #: (record: AuditRecord?, members: MapMembers, now: Hash[String, String]) -> void
20
+ def initialize(record:, members:, now:)
21
+ @record = record
22
+ @left_open = record&.left_open || [] #: Array[OpenFinding]
23
+ @members = members
24
+ @now = now
25
+ freeze
26
+ end
27
+
28
+ # Each prior finding as the next audit's request lists it, under its members' ids now, with whether each
29
+ # member changed since that audit.
30
+ #: () -> Array[Hash[String, untyped]]
31
+ def listed
32
+ @left_open.map { |finding| finding.for_next_audit_against(@members, @now) }
33
+ end
34
+
35
+ # Each prior finding as the request shows it: as listed, and with a last_audited_commit, each evidence item with
36
+ # what since says of it now against that commit (#858, #877). The request id digests listed, so these facts never
37
+ # make an answer stale.
38
+ #: () { (EvidenceItem, String) -> Hash[String, bool] } -> Array[Hash[String, untyped]]
39
+ def shown(&since)
40
+ base = last_audited_commit or return listed
41
+
42
+ @left_open.map { |finding| finding.for_next_audit_against(@members, @now) { |item| since.call(item, base) } }
43
+ end
44
+
45
+ # The commit the audit that left these findings open, raising or keeping each, was based on: what each evidence
46
+ # item's text is compared at (#858). nil outside git, or with no record. The record AuditRecord#left_open reads
47
+ # them from gives it, so the two cannot disagree.
48
+ #: () -> String?
49
+ def last_audited_commit = @record&.last_completed&.base_commit
50
+
51
+ # What the request id digests, so an answer to a request made before the record changed is stale.
52
+ #: () -> Array[Hash[String, untyped]]
53
+ def digested
54
+ listed.map { |finding| finding.slice("finding_id", "member_ids", "problem", "action", "first_raised_at") }
55
+ end
56
+
57
+ # The decisions, checked against the listed findings and this audit's new findings. locate checks that a
58
+ # new recorded_at resolves, as a deferral's is checked; records read the record a claim of an earlier
59
+ # first_raised_at cites (D53).
60
+ #: (untyped, Array[untyped], ?records: AuditRecords?) { (Locator) -> void } -> DecidedPriorFindings
61
+ def decided(decisions, findings, records: nil, &locate)
62
+ new_ids = findings.map { |finding| finding.fetch("id") }
63
+ decisions = checked(complete(decisions, left_open_by_id.keys), new_ids, locate, records)
64
+ PriorFindingOutcomes.new(decisions).decided.tap { it.distinct_from(new_ids) }
65
+ end
66
+
67
+ private
68
+
69
+ # The decisions, each on the finding it decides, once checked against it and this audit's new findings.
70
+ #: (Array[Hash[String, untyped]], Array[String], ^(Locator) -> void, AuditRecords?)
71
+ #| -> Array[PriorFindingDecision]
72
+ def checked(decisions, new_ids, locate, records)
73
+ open = left_open_by_id
74
+ decisions.each_with_index.map do |each, index|
75
+ decision = PriorFindingDecision.new(each, open.fetch(each["finding_id"]), new_ids,
76
+ at: "/prior_finding_decisions/#{index}", members: @members)
77
+ check(decision, locate, records)
78
+ decision
79
+ end
80
+ end
81
+
82
+ # Refuses decision unless it is sound for the finding it decides, given this audit's new findings, and any
83
+ # earlier first_raised_at it claims is borne out by the record it cites, which records read (D53).
84
+ #: (PriorFindingDecision, ^(Locator) -> void, AuditRecords?) -> void
85
+ def check(decision, locate, records)
86
+ decision.check(locate)
87
+ decision.first_raised_claim.check(records, principle_id)
88
+ end
89
+
90
+ # The principle whose record left the prior findings open; none is decided without one.
91
+ #: () -> String
92
+ def principle_id = @record ? @record.principle_id : ""
93
+
94
+ #: () -> Hash[String, OpenFinding]
95
+ def left_open_by_id = @left_open.to_h { |finding| [finding.id, finding] }
96
+
97
+ #: (untyped, Array[String]) -> Array[Hash[String, untyped]]
98
+ def complete(decisions, expected)
99
+ decisions = [] unless decisions.is_a?(Array)
100
+ decided = decisions.map { |decision| decision.is_a?(Hash) ? decision["finding_id"] : nil }
101
+ return decisions if decided.sort == expected.sort
102
+
103
+ raise Error.new("incomplete_prior_findings", "Decide each prior finding exactly once: #{expected.join(", ")}",
104
+ "Give prior_finding_decisions one {finding_id, decision} for each prior finding the request " \
105
+ "lists, with decision still_holds, resolved, withdrawn or raised_again.")
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member_problems"
6
+ require_relative "missing_files"
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
+ # What list-member-problems reports across the maps it reads: refused while any member's reference is
12
+ # broken, so a gate can stop on its exit status alone, and otherwise every changed member and a count of
13
+ # each state.
14
+ class ProblemReport
15
+ # history, when given, lets each missing file carry the rename git detects for it (#276).
16
+ #: (Array[MemberProblems], ?history: ProjectHistory?) -> void
17
+ def initialize(maps, history: nil)
18
+ @maps = maps
19
+ @history = history
20
+ freeze
21
+ end
22
+
23
+ # The changed members and the count of each state, refused when any member is broken. Given a block, each
24
+ # changed member gains the history the block makes of its map id, member, baseline hash and hash now.
25
+ #: () ?{ (String, Member, String, String) -> Hash[String, untyped] } -> Hash[String, untyped]
26
+ def checked(&)
27
+ broken = @maps.flat_map(&:broken)
28
+ refuse(broken) unless broken.empty?
29
+
30
+ { "members" => changed(&),
31
+ "counts" => @maps.flat_map(&:entries).map { |each| each.fetch("state") }.tally.sort.to_h,
32
+ "compared_with" => compared_with, **no_baseline_because, **awaiting_first_audit_field }
33
+ end
34
+
35
+ # The principles that await their first full audit, in map order, which list-member-problems names in one
36
+ # line (D58, #731).
37
+ #: () -> Array[String]
38
+ def awaiting_first_audit = @maps.select(&:awaits_first_audit?).map(&:principle_id)
39
+
40
+ # Whether any map's member changed since an audit or acknowledgement assessed it, not only since it was
41
+ # mapped, which --short exits 3 for rather than 4 (D44).
42
+ #: () -> bool
43
+ def assessed_change? = @maps.any?(&:assessed_change?)
44
+
45
+ # For each map with a changed member a one-member audit may judge, the ids of those members, which
46
+ # list-member-problems offers that audit for, one command each (#626, D56).
47
+ #: () -> Hash[String, Array[String]]
48
+ def acknowledgeable
49
+ @maps.to_h { |map| [map.principle_id, map.acknowledgeable_member_ids] }.reject { |_, ids| ids.empty? }
50
+ end
51
+
52
+ # For each map with members no baseline holds, its map file and the files those members are in, and in git
53
+ # only those of them that differ from HEAD or are untracked, since committing an unchanged file does nothing
54
+ # (#710 X3). In git the list is empty when every one is committed already.
55
+ #: (MemberMaps) -> Hash[String, Array[String]]
56
+ def files_to_commit(member_maps)
57
+ no_baseline_paths.to_h do |id, paths|
58
+ files = [member_maps.file(id), *paths]
59
+ [id, @history&.inside_git? ? @history.uncommitted(files) : files]
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Each map's files holding a member with no baseline, by map id; a map with none is left out.
66
+ #: () -> Hash[String, Array[String]]
67
+ def no_baseline_paths
68
+ @maps.to_h { |map| [map.principle_id, map.no_baseline_paths] }.reject { |_, paths| paths.empty? }
69
+ end
70
+
71
+ #: () ?{ (String, Member, String, String) -> Hash[String, untyped] } -> Array[Hash[String, untyped]]
72
+ def changed(&history)
73
+ @maps.flat_map do |map|
74
+ next map.changed unless history
75
+
76
+ map.changed { |member, baseline, now| history.call(map.principle_id, member, baseline, now) }
77
+ end
78
+ end
79
+
80
+ # Each map's count of members by the baseline they were compared with; a map with none is left out.
81
+ #: () -> Hash[String, Hash[String, Integer]]
82
+ def compared_with
83
+ @maps.to_h { |map| [map.principle_id, map.compared_with] }.reject { |_, counts| counts.empty? }
84
+ end
85
+
86
+ # Why each map's members with no baseline have none, under no_baseline_because, or nothing when all have one.
87
+ #: () -> Hash[String, Hash[String, String]]
88
+ def no_baseline_because
89
+ because = @maps.to_h { |map| [map.principle_id, map.no_baseline_because] }.compact
90
+ because.empty? ? {} : { "no_baseline_because" => because }
91
+ end
92
+
93
+ # awaiting_first_audit under its own key, or nothing when no principle awaits one.
94
+ #: () -> Hash[String, Array[String]]
95
+ def awaiting_first_audit_field
96
+ awaiting = awaiting_first_audit
97
+ awaiting.empty? ? {} : { "awaiting_first_audit" => awaiting }
98
+ end
99
+
100
+ #: (Array[Hash[String, untyped]]) -> bot
101
+ def refuse(broken)
102
+ named = broken.map { |each| [each["principle"], each["id"]].compact.join(" ") }
103
+ missing = MissingFiles.new(broken, history: @history)
104
+ raise missing.refusal("#{broken.length} members cannot be found or read: #{named.join(", ")}",
105
+ rerun: "#{Error::EXECUTABLE} list-member-problems",
106
+ note: "A map listed as unreadable_map needs its own refusal's repair. ")
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,214 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../data/project_files"
5
+ require_relative "../data/schema_check"
6
+ require_relative "../data/map_files"
7
+ require_relative "../data/audit_files"
8
+ require_relative "../data/principles_subfolder"
9
+ require_relative "../data/repository"
10
+ require_relative "candidate_inventory"
11
+ require_relative "entry_files"
12
+ require_relative "member"
13
+ require_relative "member_file"
14
+ require_relative "hook_commands"
15
+ require_relative "loaded_files"
16
+ require_relative "located_text"
17
+ require_relative "member_loads"
18
+ require_relative "member_lines"
19
+ require_relative "member_maps"
20
+ require_relative "member_snapshots"
21
+ require_relative "missing_files"
22
+ require_relative "locate_refusal"
23
+ require_relative "audit_records"
24
+ require_relative "project_history"
25
+ require_relative "principles_folder_move"
26
+
27
+ module Principles
28
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
29
+ module Models
30
+ # The user's project as this tool reads it: its instruction files, each member's text, the answers an
31
+ # agent hands in, and the maps, audit records and git history kept beside them. Controllers reach the
32
+ # project only through this model. It is handed its four data adapters, so a spec can hand it stand-ins
33
+ # that meet the same contracts (#337).
34
+ class Project
35
+ # The project at root, with its maps, audit records and history.
36
+ #: (String) -> Project
37
+ def self.at(root)
38
+ files = Data::ProjectFiles.new(root)
39
+ new(files: files, maps: Data::MapFiles.new(Data::PrinciplesSubfolder.new(files.root, "maps")),
40
+ audits: Data::AuditFiles.new(Data::PrinciplesSubfolder.new(files.root, "audits")),
41
+ repository: Data::Repository.new(files.root))
42
+ end
43
+
44
+ #: (files: Data::ProjectFiles, maps: Data::MapFiles, audits: Data::AuditFiles,
45
+ #| repository: Data::Repository) -> void
46
+ def initialize(files:, maps:, audits:, repository:)
47
+ @files = files
48
+ @maps = maps
49
+ @audits = audits
50
+ @repository = repository
51
+ end
52
+
53
+ # The project's root, resolved.
54
+ #: () -> String
55
+ def root
56
+ @files.root
57
+ end
58
+
59
+ #: () -> MemberMaps
60
+ def member_maps
61
+ refuse_unmoved_folder
62
+ MemberMaps.new(@maps)
63
+ end
64
+
65
+ #: () -> AuditRecords
66
+ def audit_records
67
+ refuse_unmoved_folder
68
+ AuditRecords.new(@audits, history)
69
+ end
70
+
71
+ #: () -> ProjectHistory
72
+ def history
73
+ ProjectHistory.new(@repository, @files)
74
+ end
75
+
76
+ # Whether two paths name one file, as their symbolic links lead them, so an edit through either is an edit
77
+ # to both (#695).
78
+ #: (String, String) -> bool
79
+ def same_file?(path, other) = @files.leads_to(path) == @files.leads_to(other)
80
+
81
+ # Where a member is in its file now, and the hash of what it says there.
82
+ #: (Locator) -> Hash[String, untyped]
83
+ def locate(locator)
84
+ member_file(locator.path).locate(locator)
85
+ end
86
+
87
+ # The text locator finds now, as one value: its path, its lines and the hash of what it says (#909).
88
+ #: (Locator) -> LocatedText
89
+ def located_text(locator) = LocatedText.of(locate(locator))
90
+
91
+ # The member at locator, its lines in its file at commit beside its lines now, which an audit shows the
92
+ # agent so it judges the member's own change without the rest of its file's (#379); nil when commit lacks the
93
+ # file or its locator finds nothing there, as after a move, which each caller says in its own terms (#459).
94
+ # then_locator places it at commit where it was elsewhere then, as before a move (#547).
95
+ #: (Locator, since: String, ?then_locator: Locator) -> MemberLines?
96
+ def member_lines(locator, since:, then_locator: locator)
97
+ before = history.lines_at(since, then_locator) or return nil
98
+
99
+ MemberLines.new(at_baseline: before, now: member_file(locator.path).lines_of(locator))
100
+ end
101
+
102
+ # As member_lines, since the last full audit's commit, or that it cannot be found there, so a full audit still
103
+ # starts (#457).
104
+ #: (Locator, since: String) -> (MemberLines | MemberLinesNotFoundAtLastAudit)
105
+ def member_lines_since_last_audit(locator, since:)
106
+ member_lines(locator, since: since) || MemberLinesNotFoundAtLastAudit.new
107
+ end
108
+
109
+ # The project file at path as the type that finds a member in it, refused when it cannot be read.
110
+ #: (String) -> MemberFile
111
+ def member_file(path)
112
+ MemberFile.for(path: path, text: @files.read_text(path))
113
+ end
114
+
115
+ # Each of map's members by id, where it is in its file now and the hash of what it says there. It is
116
+ # refused once, listing every member that cannot be found or read, so the agent repairs them together.
117
+ #: (MemberMap) -> MemberSnapshots
118
+ def snapshots(map)
119
+ broken = [] #: Array[Hash[String, untyped]]
120
+ found = map.members.to_h do |member|
121
+ [member.id, locate(member.locator)]
122
+ rescue Error => e
123
+ broken << { "id" => member.id }.merge(e.payload.except("status"))
124
+ [member.id, {}]
125
+ end
126
+ broken.empty? ? MemberSnapshots.new(found) : refuse_broken(map.id, broken)
127
+ end
128
+
129
+ # The superseded, git-ignored file where an older version kept audit runs; this version never reads it.
130
+ OLDER_STATE = ".memeplex/state.json" #: String
131
+
132
+ # The files beside the maps that an older version wrote and this one does not read, so an audit kept
133
+ # only there is no baseline.
134
+ #: () -> Array[String]
135
+ def unread_older_files
136
+ @files.file?(OLDER_STATE) ? [OLDER_STATE] : []
137
+ end
138
+
139
+ # Each member's hash now by key, or the refusal that says why its text cannot be found or read.
140
+ #: (MapMembers) -> Hash[String, untyped]
141
+ def current_by_key(members)
142
+ members.to_h do |member|
143
+ [member.key, locate(member.locator).fetch("sha256")]
144
+ rescue Error => e
145
+ [member.key, e.payload]
146
+ end
147
+ end
148
+
149
+ #: (String, Array[Hash[String, untyped]]) -> bot
150
+ def refuse_broken(map_id, broken)
151
+ ids = broken.map { |member| member.fetch("id") }.join(", ")
152
+ missing = MissingFiles.new(broken, principle: map_id, history: history)
153
+ raise missing.refusal("#{broken.length} of #{map_id}'s members cannot be found or read: #{ids}")
154
+ end
155
+
156
+ # A member's text now and how fresh it is, which the block says from its hash. A member whose place
157
+ # in its file is gone is locator_lost; one whose file cannot be read is unavailable. Both keep the
158
+ # refusal.
159
+ #: (Locator) { (String) -> String } -> Hash[String, untyped]
160
+ def freshness(locator, &)
161
+ now = locate(locator)
162
+ { "snapshot" => now, "freshness" => yield(now.fetch("sha256")) }
163
+ rescue Error => e
164
+ { "freshness" => LocateRefusal.new(e.payload).member_state, "error" => e.payload }
165
+ end
166
+
167
+ # Each member's MemberLoads by id: when each host loads it into context, with the size of what loads, and
168
+ # the project hooks whose commands name its file, read once for them all unless the caller already read them.
169
+ #: (MapMembers, ?HookCommands) -> Hash[String, MemberLoads]
170
+ def member_loads(members, hooks = hook_commands)
171
+ members.to_h { |member| [member.id, MemberLoads.read(@files, member.locator, hooks: hooks)] }
172
+ end
173
+
174
+ # The files members load, each read whole once, with the project's hooks as hooks read them (#483).
175
+ #: (MapMembers, HookCommands) -> LoadedFiles
176
+ def loaded_files(members, hooks) = LoadedFiles.read(@files, members, hooks: hooks)
177
+
178
+ #: () -> HookCommands
179
+ def hook_commands
180
+ HookCommands.read(@files)
181
+ end
182
+
183
+ # The root entry files every session of a host loads, whether one links the other and which git does not
184
+ # track.
185
+ #: () -> EntryFiles
186
+ def entry_files = EntryFiles.read(@files, history)
187
+
188
+ ANSWER_REPAIR = "Repair each field listed, starting from the request's response_template."
189
+
190
+ # An answer to a phase's request from file, where - means it was read from stdin, checked against the
191
+ # phase's answer schema (D29).
192
+ #: (String, stdin: String?, phase: String) -> untyped
193
+ def answer(file, stdin:, phase:)
194
+ parsed = file == "-" ? @files.parse_json(stdin.to_s, "-") : @files.read_json(file)
195
+ Data::SchemaCheck.new("#{phase}-answer").checked(parsed, code: "invalid_answer", source: file,
196
+ repair: ANSWER_REPAIR)
197
+ end
198
+
199
+ # What list-candidate-files found: the candidate files as it lists them, and which git does not track.
200
+ #: () -> CandidateInventory
201
+ def candidate_files
202
+ CandidateInventory.found(@files.inventory, history)
203
+ end
204
+
205
+ private
206
+
207
+ # Refuses while the maps or records are still in the folder's earlier name (#536).
208
+ #: () -> void
209
+ def refuse_unmoved_folder
210
+ PrinciplesFolderMove.new(@maps.held_in | @audits.held_in).refuse_while_pending
211
+ end
212
+ end
213
+ end
214
+ end