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,106 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member_moves"
6
+ require_relative "recorded_finding"
7
+
8
+ module Principles
9
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
10
+ module Models
11
+ # What the next full audit decided about each prior finding (D35), once this tool has checked the decisions:
12
+ # the findings that still hold, as the record keeps them; the ones closed, with their reasons; and, for each
13
+ # new finding that raises a prior one again, when that one was first raised. The correction's basis carries
14
+ # these fields, so an audit with findings completes with the same decisions.
15
+ class DecidedPriorFindings
16
+ #: (Hash[String, untyped]) -> void
17
+ def initialize(fields)
18
+ @fields = fields.freeze #: Hash[String, untyped]
19
+ freeze
20
+ end
21
+
22
+ # Whether there was no prior finding to decide.
23
+ #: () -> bool
24
+ def none?
25
+ kept.empty? && closed.empty? && @fields.fetch("inherited").empty?
26
+ end
27
+
28
+ # Each finding that still holds, as the record keeps it.
29
+ #: () -> Array[Hash[String, untyped]]
30
+ def kept
31
+ @fields.fetch("kept")
32
+ end
33
+
34
+ #: () -> Array[String]
35
+ def kept_ids = kept_findings.map(&:id)
36
+
37
+ # Refuses a new finding that reuses the id of a prior finding that still holds, since the record would then hold
38
+ # two findings under one id.
39
+ #: (Array[String]) -> void
40
+ def distinct_from(new_ids)
41
+ return unless kept_ids.intersect?(new_ids)
42
+
43
+ raise Error.new("duplicate_finding", "A new finding reuses the id of a prior finding that still holds",
44
+ "Give new findings ids other than the kept ones: #{kept_ids.join(", ")}.")
45
+ end
46
+
47
+ # Each finding resolved or withdrawn, with its decision and reason.
48
+ #: () -> Array[Hash[String, untyped]]
49
+ def closed
50
+ @fields.fetch("closed")
51
+ end
52
+
53
+ # The ids of the prior findings decided resolved; a withdrawn one was never a gap (#714).
54
+ #: () -> Array[String]
55
+ def resolved_ids = closed.select { |each| each["decision"] == "resolved" }.map { |each| each.fetch("finding_id") }
56
+
57
+ # Each kept finding whose pointer its decision dropped with null, with the pointer it had (#374). A
58
+ # correction basis printed before this was built carries none.
59
+ #: () -> Array[Hash[String, untyped]]
60
+ def dropped
61
+ @fields.fetch("dropped", [])
62
+ end
63
+
64
+ # Each kept finding whose waits_on its decision dropped, with null or by accepting it, with the line and since
65
+ # it had (D52). A correction basis printed before this was built carries none.
66
+ #: () -> Array[Hash[String, untyped]]
67
+ def waits_on_dropped
68
+ @fields.fetch("waits_on_dropped", [])
69
+ end
70
+
71
+ # Each kept evidence item a still_holds decision gave a new place, as it was and is now (D70). A correction
72
+ # basis printed before this was built carries none.
73
+ #: () -> Array[Hash[String, untyped]]
74
+ def evidence_replaced
75
+ @fields.fetch("evidence_replaced", [])
76
+ end
77
+
78
+ # When the prior finding that the new finding id raises again was first raised, or nothing.
79
+ #: (String) -> Hash[String, String]
80
+ def first_raised(id)
81
+ inherited = @fields.fetch("inherited")
82
+ inherited.key?(id) ? { "first_raised_at" => inherited.fetch(id) } : {}
83
+ end
84
+
85
+ # These decisions with each kept finding filed under its members' ids after moves (D28).
86
+ #: (MemberMoves) -> DecidedPriorFindings
87
+ def moved(moves)
88
+ DecidedPriorFindings.new(@fields.merge("kept" => moves.findings(kept)))
89
+ end
90
+
91
+ # The fields as the correction's basis carries them.
92
+ #: () -> Hash[String, untyped]
93
+ def to_h
94
+ @fields
95
+ end
96
+
97
+ NONE = new({ "kept" => [], "closed" => [], "inherited" => {} }) #: DecidedPriorFindings
98
+
99
+ private
100
+
101
+ # Each finding that still holds, read as the record keeps it.
102
+ #: () -> Array[RecordedFinding]
103
+ def kept_findings = kept.map { |finding| RecordedFinding.new(finding) }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,156 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member_map"
6
+ require_relative "member_moves"
7
+
8
+ module Principles
9
+ # The domain: maps, audits and command steps, with every rule a schema cannot express.
10
+ # models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # The changes a correction declares it made to the map after the audit began: the members it moved (D28),
13
+ # and the members whose role or check a fix changed, which each fixed disposition lists in
14
+ # changed_map_entries (D40). This tool checks them against the map at the base commit, which may also hold
15
+ # members added since, needing no declaration, as a fix maps a member a finding said was missing (T69); whether a
16
+ # change fixes its finding is the agent's judgement.
17
+ class DeclaredMapChanges
18
+ # The changes an answer declares: its moved_members and each disposition's changed_map_entries.
19
+ #: (untyped) -> DeclaredMapChanges
20
+ def self.declared_in(response)
21
+ new(MemberMoves.new(response["moved_members"]), response["dispositions"])
22
+ end
23
+
24
+ #: (MemberMoves, untyped) -> void
25
+ def initialize(moves, dispositions)
26
+ @moves = moves
27
+ @entries = claimed(dispositions) #: Array[String]
28
+ freeze
29
+ end
30
+
31
+ #: () -> bool
32
+ def empty?
33
+ @moves.empty? && @entries.empty?
34
+ end
35
+
36
+ # The base commit, to read there the map the audit began from, or nil when nothing is declared and current is
37
+ # still at map_version, the version the audit began from. Outside git, a map that changed is refused, as are
38
+ # declared changes, since there is no base map to check them against.
39
+ #: (String?, MemberMap, map_version: String, map_file: String) -> String?
40
+ def base_to_read(base, current, map_version:, map_file:)
41
+ return check_in_git(base, principle_id: current.id) unless empty?
42
+ return if current.version == map_version
43
+ return base if base
44
+
45
+ raise stale_map(current.id, map_file)
46
+ end
47
+
48
+ # Refuses declared changes outside git, where there is no base map to check them against.
49
+ #: (String?, principle_id: String) -> String
50
+ def check_in_git(base, principle_id:)
51
+ return @moves.check_in_git(base, principle_id: principle_id) unless @moves.empty?
52
+ return base if base
53
+
54
+ raise Error.new("map_entries_need_git",
55
+ "changed_map_entries needs git, to read the map #{principle_id} began from",
56
+ "Outside git, defer the finding, commit nothing, and start a fresh audit: " \
57
+ "#{Error::EXECUTABLE} start-audit #{principle_id}")
58
+ end
59
+
60
+ # Refuses unless base is the map the audit began from and current is base with exactly the declared
61
+ # members moved, the claimed members' role and check changed, and any members added (T69).
62
+ #: (MemberMap, MemberMap, map_version: String, map_file: String) -> void
63
+ def check(base, current, map_version:, map_file:)
64
+ raise unexplained(current.id, map_file) unless base.version == map_version
65
+
66
+ expected = @entries.reduce(@moves.applied(base, current)) { |map, id| claimed_entry(map, current, id) }
67
+ raise unexplained(current.id, map_file) unless with_added(expected, current).version == current.version
68
+ end
69
+
70
+ # Each claimed member's role and check, as was and now, where they differ between the base map with the
71
+ # declared moves applied, which check compares, and current. The block gives [base, current], and is
72
+ # called only when a fix claims an entry, so nothing else reads the base map (#462).
73
+ #: () { () -> [MemberMap, MemberMap] } -> Array[Hash[String, untyped]]
74
+ def entry_changes(&)
75
+ return [] if @entries.empty?
76
+
77
+ base, current = yield
78
+ moved = @moves.applied(base, current)
79
+ @entries.map { |id| changed_fields(id, moved.member(id), current.member(id)) }
80
+ end
81
+
82
+ # The map file, when a fix changed a member's role or check in it or the correction added members to it, so the
83
+ # agent commits it too (T69).
84
+ #: (String, ?added: Array[String]) -> Array[String]
85
+ def files_to_commit(map_file, added: [])
86
+ @entries.empty? && added.empty? ? [] : [map_file]
87
+ end
88
+
89
+ private
90
+
91
+ # expected with each member of current whose id it lacks added, as add-member adds it.
92
+ #: (MemberMap, MemberMap) -> MemberMap
93
+ def with_added(expected, current)
94
+ added = current.members.ids - expected.members.ids
95
+ added.reduce(expected) { |map, id| map.adding(current.member(id)) }
96
+ end
97
+
98
+ #: (untyped) -> Array[String]
99
+ def claimed(dispositions)
100
+ Array(dispositions).flat_map { |each| each.is_a?(Hash) ? Array(each["changed_map_entries"]) : [] }.uniq
101
+ end
102
+
103
+ # The map with the claimed member's role and check as current holds them, refused when they are as
104
+ # they were at the base commit.
105
+ #: (MemberMap, MemberMap, String) -> MemberMap
106
+ def claimed_entry(map, current, id)
107
+ unless current.members.taken?(id)
108
+ raise Error.new("unknown_member", "#{id} is not a member of #{current.id}",
109
+ "Name in changed_map_entries an id that this command lists: " \
110
+ "#{Error::EXECUTABLE} show-principle #{current.id}")
111
+ end
112
+ return replayed(map, id, current.member(id)) unless as_at_base?(map, current, id)
113
+
114
+ raise Error.new("claimed_change_missing", "#{id}'s role and check did not change during this audit",
115
+ "The role and check of #{id} are as they were at the base commit; remove it from " \
116
+ "changed_map_entries.")
117
+ end
118
+
119
+ # The map with the member's role and check changed to now's, as change-member-role, change-member-check
120
+ # and remove-member-check changed them.
121
+ #: (MemberMap, String, Hash[String, untyped]) -> MemberMap
122
+ def replayed(map, id, now)
123
+ roled = map.changing_role(id, now.fetch("role"))
124
+ return roled.changing_check(id, now.fetch("check")) if now.key?("check")
125
+
126
+ roled.member(id).key?("check") ? roled.removing_check(id) : roled
127
+ end
128
+
129
+ #: (String, Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
130
+ def changed_fields(id, was, now)
131
+ changed = %w[role check].reject { |field| was[field] == now[field] }
132
+ { "member_id" => id, **changed.to_h { |field| [field, { "was" => was[field], "now" => now[field] }] } }
133
+ end
134
+
135
+ #: (MemberMap, MemberMap, String) -> bool
136
+ def as_at_base?(map, current, id)
137
+ map.member(id).slice("role", "check") == current.member(id).slice("role", "check")
138
+ end
139
+
140
+ #: (String, String) -> Error
141
+ def unexplained(principle_id, map_file)
142
+ @moves.empty? ? stale_map(principle_id, map_file) : @moves.undeclared(principle_id, map_file)
143
+ end
144
+
145
+ #: (String, String) -> Error
146
+ def stale_map(principle_id, map_file)
147
+ Error.new("stale_map", "The map of #{principle_id} changed during this audit",
148
+ "If you moved a member to fix a finding, declare it in the answer's moved_members as " \
149
+ "{\"from\": OLD_ID, \"to\": NEW_ID}; if a fix changed a member's role or check, list the " \
150
+ "member's id in that fixed disposition's changed_map_entries. In git, a member you added with " \
151
+ "add-member needs no declaration, as long as nothing else changed.\n" \
152
+ "Otherwise, start a fresh audit: #{Error::EXECUTABLE} start-audit #{principle_id}", map_file)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,233 @@
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 "waits_on"
9
+ require_relative "write_up_pointer"
10
+
11
+ module Principles
12
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
13
+ module Models
14
+ # One disposition of a correction answer: the action the agent chose for one finding, why, its evidence and
15
+ # where it is written up, and the members a fix claims it changed. The agent's AnswerFields check each field
16
+ # and locate each piece of evidence in the project. Its readers read what the agent gave, so callers ask them
17
+ # only once CorrectionAnswer has checked every disposition, and found none of its fields unfinished, its action
18
+ # among them (#753, #791, #794).
19
+ class Disposition
20
+ ACTIONS = %w[fixed deferred accepted disagreed].freeze #: Array[String]
21
+
22
+ # A field the agent left unfinished: empty, the template's placeholder, the answer guide's sample (#789), or
23
+ # an action not one of the four. It says where it sits in the answer and what to write there, which one
24
+ # refusal lists beside every other (#791).
25
+ class UnfinishedField
26
+ #: String
27
+ attr_reader :repair
28
+
29
+ #: (AnswerPlace, String, problem: String, repair: String) -> void
30
+ def initialize(place, name, problem:, repair:)
31
+ @place = place
32
+ @name = name
33
+ @problem = problem
34
+ @repair = repair
35
+ freeze
36
+ end
37
+
38
+ # Whose field it is, such as "Finding F1 reason".
39
+ #: () -> String
40
+ def to_s = "#{@place.subject} #{@name}"
41
+
42
+ # Its entry in the refusal's fields, as a malformed answer's are (D29).
43
+ #: () -> Hash[String, String]
44
+ def field = { "pointer" => @place.pointer, "problem" => @problem }
45
+ end
46
+
47
+ # A text field the agent must write: the placeholder the correction template leaves in it, which an answer may
48
+ # not send back unchanged, and what to write instead; reassessment applies only to a fix (#725).
49
+ class TextField
50
+ #: String
51
+ attr_reader :placeholder
52
+
53
+ #: (placeholder: String, repair: String) -> void
54
+ def initialize(placeholder:, repair:)
55
+ @placeholder = placeholder
56
+ @repair = repair
57
+ freeze
58
+ end
59
+
60
+ # The field named name, at place, when value leaves it empty, as the placeholder (#788, #725) or as sample,
61
+ # the answer guide's illustration of this field for the disposition's action, if it gives one (#789).
62
+ #: (untyped, AnswerPlace, String, sample: String?) -> UnfinishedField?
63
+ def left_in(value, place, name, sample:)
64
+ problem = if !AnswerText.new(value).written? then "must be a nonempty string"
65
+ elsif value == @placeholder then "is the template's placeholder, unchanged"
66
+ elsif value == sample then SAMPLE_UNCHANGED
67
+ end
68
+ UnfinishedField.new(place, name, problem:, repair: @repair) if problem
69
+ end
70
+ end
71
+
72
+ # The problem with a field the agent gave back as the answer guide's sample (#789).
73
+ SAMPLE_UNCHANGED = "is the answer guide's sample, unchanged" #: String
74
+
75
+ TEXT_FIELDS = {
76
+ "reason" => TextField.new(placeholder: "Why, and what remains",
77
+ repair: "Write why you chose this action and what remains."),
78
+ "reassessment" => TextField.new(placeholder: "When fixed, how the changed members now carry the principle",
79
+ repair: "Write how the changed members now carry the principle.")
80
+ }.freeze #: Hash[String, TextField]
81
+ # The placeholder in each field, which the template prints.
82
+ PLACEHOLDERS = TEXT_FIELDS.transform_values(&:placeholder).freeze #: Hash[String, String]
83
+
84
+ # at is the disposition's place in the answer, such as /dispositions/0, which a refusal about its pointers names.
85
+ # samples is the answer guide's disposition for each action, whose text illustrates each field (#789).
86
+ #: (untyped, AnswerFields, at: String, samples: Hash[String, untyped]) -> void
87
+ def initialize(disposition, fields, at:, samples:)
88
+ @disposition = disposition
89
+ @fields = fields
90
+ @samples = samples
91
+ @place = AnswerPlace.new(at, "Finding #{disposition["finding_id"]}") #: AnswerPlace
92
+ freeze
93
+ end
94
+
95
+ #: () -> untyped
96
+ def finding_id = @disposition["finding_id"]
97
+
98
+ # The disposition as the agent gave it.
99
+ #: () -> untyped
100
+ def to_h = @disposition
101
+
102
+ #: () -> String
103
+ def action = @disposition.fetch("action")
104
+
105
+ #: () -> bool
106
+ def fixed? = action == "fixed"
107
+
108
+ # The members a fix says it changed.
109
+ #: () -> Array[String]
110
+ def changed_members = @disposition.fetch("changed_members")
111
+
112
+ # The file where the finding is written up, if anywhere.
113
+ #: () -> String?
114
+ def recorded_path = recorded_at&.path
115
+
116
+ # What the audit record keeps of this disposition beside its finding: the action, where the finding is
117
+ # written up and what a deferral waits on, when given (D52).
118
+ #: () -> Hash[String, untyped]
119
+ def recorded_outcome
120
+ { "action" => action, "recorded_at" => recorded_at&.to_h, "waits_on" => @disposition["waits_on"] }.compact
121
+ end
122
+
123
+ # Where the finding is written up, if anywhere, as checked_fields located it.
124
+ #: () -> WriteUpPointer?
125
+ def recorded_at
126
+ fields = @disposition["recorded_at"]
127
+ WriteUpPointer.new(fields) if fields
128
+ end
129
+
130
+ # Each field this disposition leaves for the agent still: an action that is not one of the four, the reason
131
+ # left empty, as the template's placeholder or as the answer guide's sample, and so a fix's reassessment,
132
+ # which applies to no other action, and a waits_on left as the guide's sample (#788, #789, #791, #794).
133
+ #: () -> Array[UnfinishedField]
134
+ def unfinished = [*unchosen_action, *unwritten_text, *waits_on_left_as_sample]
135
+
136
+ # The members this disposition claims it changed, refusing it unless its other fields are sound: only a fix
137
+ # claims any, and each must be among the members that changed. CorrectionAnswer has checked its action and
138
+ # that it wrote each field.
139
+ #: (Array[String]) -> Array[untyped]
140
+ def claimed_members(changed)
141
+ action = @disposition["action"]
142
+ checked_fields
143
+ members = @fields.array(@disposition["changed_members"], "changed_members")
144
+ entries = @disposition.fetch("changed_map_entries", [])
145
+ action == "fixed" ? fixed_members(members, entries, changed) : unclaimed(members, entries)
146
+ end
147
+
148
+ private
149
+
150
+ # The template leaves each action null, since choosing it is the agent's call; a null or unknown one is
151
+ # left for the agent, with the four to choose from (#708).
152
+ #: () -> UnfinishedField?
153
+ def unchosen_action
154
+ action = @disposition["action"]
155
+ return if ACTIONS.include?(action)
156
+
157
+ problem = action.nil? ? "is not chosen" : "is #{action.inspect}, not one of the four"
158
+ UnfinishedField.new(@place.child("action"), "action",
159
+ problem:, repair: "Set its action to one of fixed, deferred, accepted or disagreed.")
160
+ end
161
+
162
+ # The reason, and a fix's reassessment, when left empty, as the template's placeholder (#788, #725) or as the
163
+ # answer guide's sample for this action (#789).
164
+ #: () -> Array[UnfinishedField]
165
+ def unwritten_text
166
+ action = @disposition["action"]
167
+ required = action == "fixed" ? TEXT_FIELDS : TEXT_FIELDS.slice("reason")
168
+ required.filter_map do |name, field|
169
+ field.left_in(@disposition[name], @place.child(name), name, sample: @samples.dig(action, name))
170
+ end
171
+ end
172
+
173
+ # A deferral's waits_on left as the answer guide's sample, which illustrates the line without saying whom the
174
+ # finding waits on. It is one line, so WaitsOn would keep it (#789).
175
+ #: () -> UnfinishedField?
176
+ def waits_on_left_as_sample
177
+ sample = @samples.dig(@disposition["action"], "waits_on")
178
+ return unless sample && @disposition["waits_on"] == sample
179
+
180
+ UnfinishedField.new(@place.child("waits_on"), "waits_on", problem: SAMPLE_UNCHANGED, repair: WaitsOn::REPAIR)
181
+ end
182
+
183
+ # A fix's claimed members, each of which must have changed. A fix may claim no member's text when it
184
+ # claims a map entry instead, which DeclaredMapChanges has checked changed (D40).
185
+ #: (Array[untyped], Array[untyped], Array[String]) -> Array[untyped]
186
+ def fixed_members(members, entries, changed)
187
+ return members unless (members.empty? && entries.empty?) || (members - changed).any?
188
+
189
+ raise Error.new("claimed_change_missing",
190
+ "Finding #{finding_id} claims changes its members do not show: #{members.join(", ")}",
191
+ "Make the authorised change and reassess it, or record a deferral.")
192
+ end
193
+
194
+ # The disposition's evidence and recorded_at, which a deferral must give, if only as null, and the
195
+ # one line a deferral may give saying what it waits on (D52).
196
+ #: () -> void
197
+ def checked_fields
198
+ locate_pointers
199
+ unrecorded_deferral if @disposition["action"] == "deferred" && !@disposition.key?("recorded_at")
200
+ WaitsOn.new(@disposition["waits_on"], action: @disposition["action"])
201
+ .check(code: "invalid_disposition", subject: "Finding #{finding_id}")
202
+ end
203
+
204
+ # Each piece of evidence, and where the finding is written up, located in the project.
205
+ #: () -> void
206
+ def locate_pointers
207
+ @fields.evidence(@disposition["evidence"], @place.child("evidence"))
208
+ @fields.pointer(@disposition["recorded_at"], @place.child("recorded_at")) if @disposition["recorded_at"]
209
+ end
210
+
211
+ # A deferral must say where it is written up, or null, so an answer reused from before recorded_at existed
212
+ # cannot drop the pointer silently (#299).
213
+ #: () -> bot
214
+ def unrecorded_deferral
215
+ raise Error.new("invalid_disposition", "Deferred finding #{finding_id} has no recorded_at",
216
+ "Set recorded_at to where the finding is written up, as {\"path\": ..., \"heading\": ...}, " \
217
+ "or to null when it is written up nowhere.")
218
+ end
219
+
220
+ #: (Array[untyped], Array[untyped]) -> Array[untyped]
221
+ def unclaimed(members, entries)
222
+ unless entries.empty?
223
+ raise Error.new("invalid_disposition", "Only a fixed disposition may list changed_map_entries",
224
+ "Mark it fixed with a reassessment, or omit changed_map_entries.")
225
+ end
226
+ return [] if members.empty?
227
+
228
+ raise Error.new("invalid_disposition", "Only a fixed disposition may claim changed members",
229
+ "Mark it fixed with a reassessment, or omit changed_members.")
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "../data/project_files"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # One root entry file as the project holds it: the path, the file its links lead to, and its text, or the code
11
+ # this tool refused it with when it cannot be read. It says whether it links the other entry file: as a symbolic
12
+ # link to it, or by an import word, @AGENTS.md or @CLAUDE.md, which Claude Code documents as loading the file it
13
+ # names. That is a fact about the string; a plain mention of the other's name is not a link, and an unread text
14
+ # shows no import. It says, too, when git does not track it, since such a file may be the owner's alone (#896).
15
+ class EntryFile
16
+ # The entry file at path as the project holds it, or nil when there is none; tracked says whether git tracks
17
+ # it, and is true outside git, where nothing is checked.
18
+ #: (Data::ProjectFiles, String, tracked: bool) -> EntryFile?
19
+ def self.read(files, path, tracked:)
20
+ return nil unless files.file?(path)
21
+
22
+ leads_to = files.leads_to(path)
23
+ begin
24
+ new(path, leads_to: leads_to, text: files.read_text(path), tracked: tracked)
25
+ rescue Error => e
26
+ new(path, leads_to: leads_to, not_read: e.payload.fetch("code"), tracked: tracked)
27
+ end
28
+ end
29
+
30
+ #: (String, leads_to: String, ?text: String?, ?not_read: String?, ?tracked: bool) -> void
31
+ def initialize(path, leads_to:, text: nil, not_read: nil, tracked: true)
32
+ @path = path
33
+ @leads_to = leads_to
34
+ @text = text
35
+ @not_read = not_read
36
+ @tracked = tracked
37
+ freeze
38
+ end
39
+
40
+ # What setup reports of the file: that it exists, that git does not track it when it does not, why its text
41
+ # could not be read when it could not, and how it links other, when it does.
42
+ #: (String) -> Hash[String, untyped]
43
+ def facts(other)
44
+ untracked = @tracked ? {} : { "tracked" => false }
45
+ unread = @not_read ? { "not_read" => @not_read } : {}
46
+ { "path" => @path, "exists" => true, **untracked, **unread }.merge(link(other))
47
+ end
48
+
49
+ private
50
+
51
+ # How this file links other, or nothing when it does not.
52
+ #: (String) -> Hash[String, String]
53
+ def link(other)
54
+ return { "links_to" => other, "by" => "symbolic_link" } if @leads_to == other
55
+ return { "links_to" => other, "by" => "import" } if @text.to_s.match?(import_word(other))
56
+
57
+ {}
58
+ end
59
+
60
+ # An @ import of path, as one word of the text.
61
+ #: (String) -> Regexp
62
+ def import_word(path) = %r{(?:\A|\s)@(?:\./)?#{Regexp.escape(path)}(?=\s|\z)}
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../data/project_files"
5
+ require_relative "entry_file"
6
+ require_relative "project_history"
7
+
8
+ module Principles
9
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
10
+ module Models
11
+ # The project's root entry files, which a host loads in every session: CLAUDE.md for Claude Code and AGENTS.md for
12
+ # Codex, by each host's documentation. For each it says whether it exists, and, as each EntryFile says, whether it
13
+ # links the other, and whether git tracks it.
14
+ class EntryFiles
15
+ PATHS = %w[CLAUDE.md AGENTS.md].freeze #: Array[String]
16
+
17
+ # Each entry file as the project holds it, or nil for one that does not exist. history says which git does not
18
+ # track: a path whose own file, or a file its links lead to, git does not hold, so a fresh clone would not find
19
+ # its text there (#695, #896).
20
+ #: (Data::ProjectFiles, ProjectHistory) -> EntryFiles
21
+ def self.read(files, history)
22
+ new(PATHS.to_h { |path| [path, EntryFile.read(files, path, tracked: history.tracks?(path))] })
23
+ end
24
+
25
+ # held maps each entry file's path to the file, or nil when it does not exist.
26
+ #: (Hash[String, EntryFile?]) -> void
27
+ def initialize(held)
28
+ @held = held.freeze #: Hash[String, EntryFile?]
29
+ freeze
30
+ end
31
+
32
+ # Each entry file's facts, in PATHS order.
33
+ #: () -> Array[Hash[String, untyped]]
34
+ def to_a = PATHS.map { |path| facts(path) }
35
+
36
+ private
37
+
38
+ #: (String) -> Hash[String, untyped]
39
+ def facts(path)
40
+ file = @held[path] or return { "path" => path, "exists" => false }
41
+
42
+ file.facts((PATHS - [path]).first.to_s)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "locator"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # One item of the evidence a finding was raised with, as its record keeps it: a path, with a heading, passage or
10
+ # query inside it, or none to name the whole file (D64). It holds that place as the Locator its record's hash
11
+ # gives, which keeps the hash's keys and order, since the record's bytes depend on it.
12
+ class EvidenceItem
13
+ # Where in the project the item points, as MemberFile reads it.
14
+ #: Locator
15
+ attr_reader :locator
16
+
17
+ #: (Hash[String, untyped]) -> void
18
+ def initialize(fields)
19
+ @locator = Locator.new(fields) #: Locator
20
+ freeze
21
+ end
22
+
23
+ # The file it points into.
24
+ #: () -> String
25
+ def path = @locator.path
26
+
27
+ # Whether it names its whole file, with no heading, passage or query inside it.
28
+ #: () -> bool
29
+ def whole_file? = @locator.whole_file?
30
+
31
+ # The lines it names in the project's file at commit, or nil when that commit lacks the file or its place there.
32
+ #: (ProjectHistory, String) -> Array[String]?
33
+ def lines_at(history, commit) = history.lines_at(commit, @locator)
34
+
35
+ # The lines it names in the project's file now; refused when it no longer resolves.
36
+ #: (Project) -> Array[String]
37
+ def lines_now(project) = project.member_file(path).lines_of(@locator)
38
+
39
+ # The item as printed, with facts about it beside its fields.
40
+ #: (Hash[String, untyped]) -> Hash[String, untyped]
41
+ def with_facts(facts) = to_h.merge(facts)
42
+
43
+ # The item as its record holds it.
44
+ #: () -> Hash[String, untyped]
45
+ def to_h = @locator.to_h
46
+ end
47
+ end
48
+ end