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,249 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "audit_start"
5
+ require_relative "cited_files"
6
+ require_relative "decided_prior_findings"
7
+ require_relative "open_finding"
8
+ require_relative "principles_package"
9
+ require_relative "recorded_findings"
10
+ require_relative "recorded_members"
11
+ require_relative "json_digest"
12
+ require_relative "write_up_repoint"
13
+ require_relative "../data/schema_check"
14
+
15
+ module Principles
16
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
17
+ module Models
18
+ # The committed record of a principle's last audit: what it began from, each member's hash when it was
19
+ # made and each finding's outcome with its problem (D34). Outside git it also keeps the hashes the audit
20
+ # began from, which git keeps otherwise, and an open record keeps the last completed one, whose deferred
21
+ # and accepted findings the next audit must still decide (D35).
22
+ class AuditRecord
23
+ # The record an audit that began at start writes, with each member's hash, the result reported for its
24
+ # check, if any (D30), and each finding's outcome. The hashes the audit began from are kept only outside
25
+ # git, where start has no base commit.
26
+ #: (AuditStart, status: String, audited_at: String, members: Hash[String, String], findings: Array[untyped])
27
+ #| -> AuditRecord
28
+ def self.written(start, status:, audited_at:, members:, findings:)
29
+ fields = { "principle_id" => start.id, "status" => status, "map_version" => start.map_version,
30
+ "base_commit" => start.base, "audited_at" => audited_at,
31
+ "members" => start.recorded(members, assessed_at: audited_at), "findings" => findings }
32
+ new(start.base ? fields : fields.merge("base_members" => start.before))
33
+ end
34
+
35
+ #: (Hash[String, untyped]) -> void
36
+ def initialize(fields)
37
+ @fields = fields.freeze #: Hash[String, untyped]
38
+ @members = RecordedMembers.new(fields.fetch("members")) #: RecordedMembers
39
+ freeze
40
+ end
41
+
42
+ # The record with only the fields this tool knows, for printing.
43
+ #: () -> Hash[String, untyped]
44
+ def shown
45
+ Data::SchemaCheck.new("audit-record").known(@fields)
46
+ end
47
+
48
+ # The record as its file holds it.
49
+ #: () -> Hash[String, untyped]
50
+ def to_h
51
+ @fields
52
+ end
53
+
54
+ # A digest of the whole record. A correction carries the last completed record's from when its request was
55
+ # printed, so it is refused once that record changed (#646). A formatter that only reorders its keys changes
56
+ # neither what the record says nor its revision, as same_as? already compares it (#656).
57
+ #: () -> String
58
+ def revision = JsonDigest.new(@fields).to_s
59
+
60
+ #: () -> String
61
+ def principle_id
62
+ @fields.fetch("principle_id")
63
+ end
64
+
65
+ # The hashes the audit began from, kept outside git only.
66
+ #: () -> Hash[String, String]?
67
+ def base_members
68
+ @fields["base_members"]
69
+ end
70
+
71
+ # Whether other says what this record says, apart from when it and its assessments were made.
72
+ #: (AuditRecord?) -> bool
73
+ def same_as?(other)
74
+ !other.nil? && other.undated == undated
75
+ end
76
+
77
+ # What this record says, without when it and each of its assessments were made.
78
+ #: () -> Hash[String, untyped]
79
+ def undated = @fields.except("audited_at").merge("members" => members.undated)
80
+
81
+ # The commit the audit began from, or nil outside git.
82
+ #: () -> String?
83
+ def base_commit
84
+ @fields["base_commit"]
85
+ end
86
+
87
+ # The record's status, when and from which commit it was made, and whether the map changed since.
88
+ #: (map_version: String) -> Hash[String, untyped]
89
+ def last_audit(map_version:)
90
+ { "status" => @fields["status"], "audited_at" => @fields["audited_at"], "base_commit" => @fields["base_commit"],
91
+ "map_changed" => @fields["map_version"] != map_version }
92
+ end
93
+
94
+ # Why a one-member audit cannot judge member's change against this record, or nil when it can (D33): the
95
+ # record must be completed and have assessed the member, the member's text must differ from the one it
96
+ # assessed, and the member must have no check. The one owner of these preconditions, which list-member-problems
97
+ # asks before offering the audit that Acknowledgement refuses by them (#626, #632); git is the callers' to know.
98
+ #: (Member, now_sha256: String?) -> Symbol?
99
+ def acknowledgement_refusal(member, now_sha256:)
100
+ recorded = members.hash_for(member.key)
101
+ return :not_completed unless completed?
102
+ return :not_assessed unless recorded
103
+ return :unchanged if recorded == now_sha256
104
+
105
+ :checked if member.check
106
+ end
107
+
108
+ # Whether the audit this record describes completed, rather than being left open outside git.
109
+ #: () -> bool
110
+ def completed?
111
+ @fields["status"] == "completed"
112
+ end
113
+
114
+ # This open record, keeping the last completed record it replaces outside git, so the next audit still
115
+ # decides that record's findings (D35); itself when there is none.
116
+ #: (AuditRecord?) -> AuditRecord
117
+ def keeping(last_completed)
118
+ last_completed ? AuditRecord.new(@fields.merge("last_completed" => last_completed.to_h)) : self
119
+ end
120
+
121
+ # This record, saying what its assessments were judged against, as AuditStart#judged_against digests it (D49).
122
+ #: (String) -> AuditRecord
123
+ def judged(against) = AuditRecord.new(@fields.merge("judged_against" => against))
124
+
125
+ # This record, keeping the release of principles its audit ran under, when the principle keeps one (D68).
126
+ #: (PrinciplesPackage?) -> AuditRecord
127
+ def packaged(package) = package ? AuditRecord.new(@fields.merge("principles_package" => package.to_h)) : self
128
+
129
+ # The release of principles its audit ran under, or nil when it kept none.
130
+ #: () -> PrinciplesPackage?
131
+ def package = PrinciplesPackage.kept(@fields["principles_package"])
132
+
133
+ # Whether this record kept its members' assessments, which a record written before D49 did not.
134
+ #: () -> bool
135
+ def kept_assessments? = !@fields["judged_against"].nil?
136
+
137
+ # Whether this record's assessments answered what against digests, as AuditStart#judged_against does (D49).
138
+ #: (String) -> bool
139
+ def judged_against?(against) = @fields["judged_against"] == against
140
+
141
+ # This record with each assessment its answer carried forward taken from last, the record it replaces (D49).
142
+ #: (AuditRecord?) -> AuditRecord
143
+ def carrying(last)
144
+ AuditRecord.new(@fields.merge("members" => members.carrying(last&.last_completed&.members).to_h))
145
+ end
146
+
147
+ # The last completed audit's record: this one, or the one an open record keeps, or nil (D35).
148
+ #: () -> AuditRecord?
149
+ def last_completed
150
+ return self if completed?
151
+
152
+ kept = @fields["last_completed"]
153
+ kept ? AuditRecord.new(kept) : nil
154
+ end
155
+
156
+ # This record with the member known by key moved to change's to_sha256, and change kept beside it (D33).
157
+ # Nothing else changes: the map version, base commit and findings stay the audit's own.
158
+ #: (String, Hash[String, untyped]) -> AuditRecord
159
+ def acknowledged(key, change)
160
+ AuditRecord.new(@fields.merge("members" => members.acknowledged(key, change).to_h))
161
+ end
162
+
163
+ # This record with each finding a repoint names moved by it at at (D59); the last repoint naming a finding
164
+ # wins.
165
+ #: (Array[WriteUpRepoint], at: String) -> AuditRecord
166
+ def repointed(repoints, at:)
167
+ AuditRecord.new(@fields.merge("findings" => findings.repointed(repoints, at: at).to_a))
168
+ end
169
+
170
+ # The ids of the findings it leaves open whose recorded_at points elsewhere than earlier's did, as a repoint
171
+ # moved them (D59, T16).
172
+ #: (AuditRecord) -> Array[String]
173
+ def write_ups_moved_since(earlier)
174
+ before = earlier.left_open.to_h { |finding| [finding.id, finding.recorded_at] }
175
+ left_open.select { |finding| before.key?(finding.id) && before[finding.id] != finding.recorded_at }.map(&:id)
176
+ end
177
+
178
+ # Each finding the last completed audit deferred or accepted, which the next full audit must decide (D35):
179
+ # this record's own, or, while it is open outside git, those of the completed record it keeps, since its own
180
+ # wait on its correction (#655). The one owner of which record's findings are open.
181
+ #: () -> Array[OpenFinding]
182
+ def left_open
183
+ return findings.left_open if completed?
184
+
185
+ last_completed&.left_open || []
186
+ end
187
+
188
+ # The ids of the findings it leaves open, as list-open-findings lists them: deferred or accepted, its own and any
189
+ # prior finding kept (#714).
190
+ #: () -> Array[String]
191
+ def open_ids = left_open.map(&:id)
192
+
193
+ # The ids of the findings its audit resolved: those it fixed, and each prior finding decided resolved, never
194
+ # one withdrawn (#714).
195
+ #: (DecidedPriorFindings) -> Array[String]
196
+ def resolved_ids(decided)
197
+ findings.fixed_ids + decided.resolved_ids
198
+ end
199
+
200
+ # Each finding left open, as list-open-findings prints it (D35, brief finding 13).
201
+ #: () -> Array[Hash[String, untyped]]
202
+ def open_findings = left_open.map(&:listed)
203
+
204
+ # Each finding left open, as the next full audit's request lists it (D35).
205
+ #: () -> Array[Hash[String, untyped]]
206
+ def prior_findings = left_open.map(&:for_next_audit)
207
+
208
+ # The path of each file this record points at, through its assessments' evidence and its findings' evidence
209
+ # and write-ups, which a clone must hold for the record to be checked there (#724).
210
+ #: () -> Array[String]
211
+ def cited_paths = (members.cited_paths + findings.cited_paths).uniq
212
+
213
+ # The files this record cites, as history holds them now: which go with it when its completed audit is
214
+ # committed, compared with HEAD only in git, and which no longer exist (#724, #861).
215
+ #: (ProjectHistory) -> CitedFiles
216
+ def cited_files(history) = CitedFiles.new(cited_paths, history, in_git: !base_commit.nil?)
217
+
218
+ # The members this record assessed, each with its hash, check, loading decision and acknowledgements.
219
+ #: RecordedMembers
220
+ attr_reader :members
221
+
222
+ # The command this record ran for the member known by key's check, as its RecordedMembers answers it, or nil
223
+ # when it gave that check no exit status.
224
+ #: (String) -> String?
225
+ def check_run_for(key) = members.check_run_for(key)
226
+
227
+ # The line saying what the review of the check this record ran for the member known by key rested on, with the
228
+ # date it was first kept: its own review_basis_since, or else this record's audited_at, which dates a line kept
229
+ # here first (D63, D52). Nothing when the record ran no check for it or kept no line.
230
+ #: (String) -> Hash[String, untyped]
231
+ def check_review_for(key)
232
+ kept = members.check_review_as_kept(key)
233
+ return {} unless kept.key?("review_basis")
234
+
235
+ { "review_basis_since" => @fields["audited_at"] }.merge(kept)
236
+ end
237
+
238
+ private
239
+
240
+ # The findings its own audit recorded, whatever it did about each, with the principle, audit time and members
241
+ # it recorded them under.
242
+ #: () -> RecordedFindings
243
+ def findings
244
+ RecordedFindings.new(@fields.fetch("findings"), principle_id: principle_id, audited_at: @fields["audited_at"],
245
+ members: members)
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,103 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "../data/audit_files"
6
+ require_relative "audit_record"
7
+ require_relative "project_history"
8
+
9
+ module Principles
10
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # Every principle's audit record, loaded and saved through Data::AuditFiles, and read as a commit held it
13
+ # through ProjectHistory.
14
+ class AuditRecords
15
+ #: (Data::AuditFiles, ProjectHistory) -> void
16
+ def initialize(files, history)
17
+ @files = files
18
+ @history = history
19
+ end
20
+
21
+ # The file id's record was in at commit, and that record, or nil when commit holds it in neither the folder
22
+ # nor its earlier name, as outside git (D53). It is read only at a commit an agent names; nothing walks history.
23
+ #: (String, String) -> [String, AuditRecord]?
24
+ def at(id, commit)
25
+ [file(id), earlier_file(id)].each do |path|
26
+ text = @history.text_at(commit, path) or next
27
+ return [path, AuditRecord.new(@files.parse(id, text))]
28
+ end
29
+ nil
30
+ end
31
+
32
+ # id's record, or nil when it has none; Data::AuditFiles refuses one that is unparseable.
33
+ #: (String) -> AuditRecord?
34
+ def find(id)
35
+ fields = @files.read(id)
36
+ fields && AuditRecord.new(fields)
37
+ end
38
+
39
+ # The findings id's record leaves open: none when it has no record, and none when its record cannot be read,
40
+ # which list-member-problems reports, so a command reading other principles' findings beside its own is not
41
+ # refused for theirs (#772).
42
+ #: (String) -> Array[OpenFinding]
43
+ def left_open_or_none(id)
44
+ find(id)&.left_open || []
45
+ rescue Error
46
+ []
47
+ end
48
+
49
+ # The record of each of ids that has one.
50
+ #: (Array[String]) -> Array[AuditRecord]
51
+ def of(ids) = ids.filter_map { |id| find(id) }
52
+
53
+ # id's record, refused when it has none.
54
+ #: (String) -> AuditRecord
55
+ def fetch(id)
56
+ find(id) || raise(Error.new("unknown_audit", "#{id} has no audit record",
57
+ "To audit it: #{Error::EXECUTABLE} start-audit #{id}", file(id)))
58
+ end
59
+
60
+ #: (String) -> bool
61
+ def exist?(id)
62
+ @files.exist?(id)
63
+ end
64
+
65
+ # The record as a change to write, unless it says what the record on disk says, apart from when.
66
+ #: (AuditRecord) -> Array[AuditRecord]
67
+ def changes(record)
68
+ record.same_as?(find(record.principle_id)) ? [] : [record]
69
+ end
70
+
71
+ # What record's completed audit asks to commit: its own file, then the files the record says go with it, of
72
+ # changed and those it cites, with what reported cites that the record does not keep (#855, #860).
73
+ #: (AuditRecord, Array[String], ?AssessmentReport) -> Array[String]
74
+ def files_to_commit(record, changed, reported = AssessmentReport::NONE)
75
+ [file(record.principle_id), *record.cited_files(@history).committed_with(changed, reported)]
76
+ end
77
+
78
+ # Each file record cites that no longer exists, which files_to_commit leaves out (#861).
79
+ #: (AuditRecord) -> Array[String]
80
+ def cited_files_missing(record) = record.cited_files(@history).missing
81
+
82
+ # Writes record's file and names it, relative to the project.
83
+ #: (AuditRecord) -> String
84
+ def save(record)
85
+ id = record.principle_id
86
+ @files.write(id, record.to_h)
87
+ file(id)
88
+ end
89
+
90
+ # The file id's record is kept in, relative to the project.
91
+ #: (String) -> String
92
+ def file(id)
93
+ @files.relative(id)
94
+ end
95
+
96
+ # The file id's record was kept in under the folder's earlier name, relative to the project (#536).
97
+ #: (String) -> String
98
+ def earlier_file(id)
99
+ @files.earlier_relative(id)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,96 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "assessment_report"
5
+ require_relative "json_digest"
6
+ require_relative "member_map"
7
+
8
+ module Principles
9
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
10
+ module Models
11
+ # What an audit began from: the principle and its map, the commit it was based on (nil outside git) and
12
+ # each member's hash then, and what the assessment reported beyond its findings, as its AssessmentReport holds it.
13
+ class AuditStart
14
+ #: String?
15
+ attr_reader :base
16
+ #: Hash[String, String]
17
+ attr_reader :before
18
+
19
+ # What the assessment reported beyond its findings, such as each member's record entry fields.
20
+ #: AssessmentReport
21
+ attr_reader :reported
22
+
23
+ # The map of the principle the audit is of.
24
+ #: MemberMap
25
+ attr_reader :map
26
+
27
+ #: (map: MemberMap, base: String?, before: Hash[String, String], ?reported: AssessmentReport) -> void
28
+ def initialize(map:, base:, before:, reported: AssessmentReport::NONE)
29
+ @map = map
30
+ @base = base
31
+ @before = before
32
+ @reported = reported
33
+ freeze
34
+ end
35
+
36
+ # The principle the audit is of, as its map names it.
37
+ #: () -> String
38
+ def id = @map.id
39
+
40
+ # Each member's entry in the record, by id, from its hash in members: the hash, its key when it has moved,
41
+ # and what the assessment reported for it, if anything, its assessment kept as made at assessed_at. A member
42
+ # added during the audit has no entry, since no assessment read it: the record gives it no baseline, so the
43
+ # next audit judges it (T69).
44
+ #: (Hash[String, String], assessed_at: String) -> Hash[String, Hash[String, untyped]]
45
+ def recorded(members, assessed_at:)
46
+ keys = @map.members.moved_keys
47
+ members.select { |id, _| began_with?(id) }.to_h do |id, sha|
48
+ entry = { "sha256" => sha }.merge(keys.key?(id) ? { "key" => keys[id] } : {})
49
+ [id, with_assessment(entry.merge(@reported.entry_fields_of(id)), assessed_at, changed: @before[id] != sha)]
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Whether the audit began with member id, rather than a correction adding it (T69).
56
+ #: (String) -> bool
57
+ def began_with?(id) = @before.key?(id)
58
+
59
+ # The entry with its assessment dated assessed_at, and an observed sighting the answer gave no observed_at for
60
+ # dated then too (#680), or a carried one left for completion to fill; when the member changed during the
61
+ # audit, as a fix in its correction changes it, no assessment read the text it is recorded at, so it keeps none
62
+ # and says why (D49).
63
+ #: (Hash[String, untyped], String, changed: bool) -> Hash[String, untyped]
64
+ def with_assessment(entry, assessed_at, changed:)
65
+ kept = entry["assessment"] or return entry
66
+ return entry.except("assessment").merge("changed_during_last_audit" => true) if changed
67
+ return entry if kept["carried_forward"]
68
+
69
+ seen = kept["delivery"] == "observed" ? assessed_at : nil
70
+ entry.merge("assessment" => { "assessed_at" => assessed_at, "observed_at" => seen }.compact.merge(kept))
71
+ end
72
+
73
+ public
74
+
75
+ # A digest of what this audit's assessments answered: the principle's description and question, the question
76
+ # this tool asked. When either changes, the next full audit carries no assessment forward (D49).
77
+ #: (String) -> String
78
+ def judged_against(question) = JsonDigest.new("description" => @map.description, "question" => question).to_s
79
+
80
+ # The map's version: the digest of its JSON, so any change to it gives a new version.
81
+ #: () -> String
82
+ def map_version = @map.version
83
+
84
+ # The members in now that the audit did not begin with, as a correction adds a member a finding said was
85
+ # missing (T69).
86
+ #: (now: Hash[String, String]) -> Array[String]
87
+ def added(now:) = now.keys.reject { |id| began_with?(id) }
88
+
89
+ # The members whose hash in now differs from their hash when the audit began.
90
+ #: (now: Hash[String, String]) -> Array[String]
91
+ def changed(now:)
92
+ now.reject { |member, sha| @before[member] == sha }.keys
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "evidence_item"
6
+ require_relative "located_text"
7
+ require_relative "line_span"
8
+
9
+ module Principles
10
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
11
+ module Models
12
+ # The lines a full audit's members span now, each with its file, against which each evidence item of another
13
+ # principle's open finding is said to share lines or not (#792, D64). These are lines, not a verdict on whether
14
+ # the finding is about the same problem, which the agent judges.
15
+ class AuditedMemberLines
16
+ # located gives, under each audited member's id, the text found for it now: its path and its lines.
17
+ #: (Hash[String, LocatedText]) -> void
18
+ def initialize(located)
19
+ @spans = located.transform_values { |found| [found.path, found.span] } #: Hash[String, [String, LineSpan]]
20
+ freeze
21
+ end
22
+
23
+ # The item as the record keeps it, with what its lines are now beside these members': the members whose lines it
24
+ # shares, or that it shares none, with its lines; that it names only a whole file; or that locate, which finds
25
+ # it in its file now, can no longer find it, so one moved write-up never refuses the request.
26
+ #: (EvidenceItem) { (Locator) -> LocatedText } -> Hash[String, untyped]
27
+ def facts_of(item, &locate)
28
+ return item.with_facts("whole_file" => true) if item.whole_file?
29
+
30
+ span = locate.call(item.locator).span
31
+ item.with_facts("first_line" => span.first_line, "last_line" => span.last_line, **sharing(item.path, span))
32
+ rescue Error
33
+ item.with_facts("not_located" => true)
34
+ end
35
+
36
+ private
37
+
38
+ #: (String, LineSpan) -> Hash[String, untyped]
39
+ def sharing(path, span)
40
+ ids = @spans.select { |_, (on, member)| on == path && member.shares_a_line_with?(span) }.keys
41
+ ids.empty? ? { "shares_no_lines" => true } : { "shares_lines_with" => ids }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
6
+ module Models
7
+ # The instruction files list-candidate-files offers, shortened without judging what is guidance. A
8
+ # skill's templates, references and examples once filled most of a 10KB inventory (alpha 011
9
+ # friction 2). So Markdown files and harness settings are listed one by one, except inside a skill,
10
+ # where only SKILL.md, the file the harness loads, is listed. Every other file is counted under its
11
+ # directory. The agent still sees where every file is, and decides what to read.
12
+ class CandidateFiles
13
+ # A file a harness reads as settings, listed by name like guidance.
14
+ SETTINGS = %r{(?:\A|/)\.claude/settings[^/]*\.json\z}
15
+ MARKDOWN = /\.md\z/i
16
+ # A file inside a skill's folder, and whether it is the skill's entry point.
17
+ IN_SKILL = %r{(?:\A|/)\.claude/skills/[^/]+/}
18
+ SKILL_ENTRY = %r{(?:\A|/)\.claude/skills/[^/]+/SKILL\.md\z}
19
+
20
+ #: (Array[Hash[String, untyped]]) -> void
21
+ def initialize(files)
22
+ @files = files #: Array[Hash[String, untyped]]
23
+ freeze
24
+ end
25
+
26
+ # The inventory's files, the other files counted by directory, and those test-data hints that name a
27
+ # listed file.
28
+ #: (Array[Hash[String, untyped]]) -> Hash[String, untyped]
29
+ def listing(test_data_hints)
30
+ paths = listed.map { |file| file.fetch("path") }
31
+ { "files" => listed, "other_files_by_directory" => other_files_by_directory,
32
+ "possibly_test_data" => test_data_hints.select { |hint| paths.include?(hint.fetch("path")) } }
33
+ end
34
+
35
+ # Each Markdown or settings file, with its size.
36
+ #: () -> Array[Hash[String, untyped]]
37
+ def listed
38
+ @files.select { |file| named?(file.fetch("path")) }
39
+ end
40
+
41
+ # Each directory holding other files, and how many.
42
+ #: () -> Array[Hash[String, untyped]]
43
+ def other_files_by_directory
44
+ others = @files.map { |file| file.fetch("path") }.reject { |path| named?(path) }
45
+ others.group_by { |path| directory(path) }.sort.map do |directory, paths|
46
+ { "directory" => directory, "files" => paths.length }
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ #: (String) -> String
53
+ def directory(path)
54
+ parent = path.rpartition("/").first
55
+ parent.empty? ? "." : parent
56
+ end
57
+
58
+ #: (String) -> bool
59
+ def named?(path)
60
+ return path.match?(SKILL_ENTRY) if path.match?(IN_SKILL)
61
+
62
+ path.match?(MARKDOWN) || path.match?(SETTINGS)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "candidate_listing"
5
+ require_relative "project_history"
6
+
7
+ module Principles
8
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # What list-candidate-files found: the inventory of candidate files as CandidateListing lists them, and in git
11
+ # which of them, or of the files their links lead to, git does not track. Each list that names a file is
12
+ # something the agent asks the project's owner about before mapping, and only those are asked, since asks for
13
+ # empty lists hid the one that applied (#405).
14
+ class CandidateInventory
15
+ # The lists the agent asks the owner about, in the order the asks are put.
16
+ OWNER_ASK_KEYS = %w[possibly_test_data untracked ignored ignored_folders].freeze
17
+
18
+ # The most paths an ask names inline: a short list can be pasted to the owner as it stands, while a
19
+ # longer one would bury the ask, so the agent reads it from its list instead (#412).
20
+ NAMED_INLINE_AT_MOST = 3
21
+
22
+ # What the project's inventory lists, and which listed files, or files their links lead to, history says
23
+ # git does not track.
24
+ #: (Hash[String, untyped], ProjectHistory) -> CandidateInventory
25
+ def self.found(inventory, history)
26
+ listed = CandidateListing.new(inventory).to_h
27
+ new(listed, untracked: history.untracked(listed.fetch("files").map { it.fetch("path") }))
28
+ end
29
+
30
+ # The listing, as list-candidate-files prints it.
31
+ #: Hash[String, untyped]
32
+ attr_reader :listing
33
+
34
+ # The listed files, and the files their links lead to, git does not track; nil outside git, where nothing
35
+ # checked them.
36
+ #: Array[String]?
37
+ attr_reader :untracked
38
+
39
+ #: (Hash[String, untyped], untracked: Array[String]?) -> void
40
+ def initialize(listing, untracked:)
41
+ @listing = listing
42
+ @untracked = untracked
43
+ @lists = listing.merge("untracked" => untracked) #: Hash[String, untyped]
44
+ freeze
45
+ end
46
+
47
+ #: () -> Hash[String, untyped]
48
+ def to_h = @listing
49
+
50
+ # Each list that names a file, in OWNER_ASK_KEYS' order, with the paths its ask names inline: all of
51
+ # them when there are at most NAMED_INLINE_AT_MOST, and none beyond.
52
+ #: () -> Hash[String, Array[String]]
53
+ def owner_asks
54
+ OWNER_ASK_KEYS.select { |key| @lists[key]&.any? }.to_h do |key|
55
+ paths = @lists.fetch(key).map { |entry| named(entry) }
56
+ [key, paths.length > NAMED_INLINE_AT_MOST ? [] : paths]
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # The path one list entry names: a file's path, a folder, or the path itself.
63
+ #: (untyped) -> String
64
+ def named(entry) = entry.is_a?(Hash) ? entry.fetch("path") { entry.fetch("folder") } : entry
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "candidate_files"
5
+ require_relative "ignored_candidates"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # The inventory of candidate files as list-candidate-files lists it: the instruction files agent hosts load
11
+ # by convention, the other files counted by directory, the listed files that look like test data, and in git
12
+ # the ignored ones, unread, each folder git ignores counted once.
13
+ class CandidateListing
14
+ #: (Hash[String, untyped]) -> void
15
+ def initialize(inventory)
16
+ @inventory = inventory
17
+ freeze
18
+ end
19
+
20
+ #: () -> Hash[String, untyped]
21
+ def to_h
22
+ files = CandidateFiles.new(@inventory.fetch("files"))
23
+ listed = @inventory.except("ignored", "ignored_ancestor_folders")
24
+ .merge(files.listing(@inventory.fetch("possibly_test_data")))
25
+ return listed unless @inventory.key?("ignored")
26
+
27
+ listed.merge(IgnoredCandidates.new(paths: @inventory.fetch("ignored"),
28
+ folders: @inventory.fetch("ignored_ancestor_folders")).listing)
29
+ end
30
+ end
31
+ end
32
+ end