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,45 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_loads"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # What a member's loads say about when it loads and what, at any depth, without the file's or the member's size.
10
+ # A file's size moves with every edit elsewhere in a shared file, so a judgement weighed on a member's loading
11
+ # compares these facts, not the sizes (second alpha user on 5bedec4, #515).
12
+ class LoadingFacts
13
+ # loads is a member's loads as MemberLoads#to_h gives them, or as a record keeps them.
14
+ #: (Hash[String, untyped]) -> void
15
+ def initialize(loads)
16
+ @facts = without_sizes(loads) #: untyped
17
+ freeze
18
+ end
19
+
20
+ #: () -> untyped
21
+ def to_h = @facts
22
+
23
+ #: (untyped) -> bool
24
+ def ==(other) = other.is_a?(LoadingFacts) && other.to_h == @facts
25
+
26
+ alias eql? ==
27
+
28
+ #: () -> Integer
29
+ def hash = @facts.hash
30
+
31
+ private
32
+
33
+ #: (untyped) -> untyped
34
+ def without_sizes(value)
35
+ case value
36
+ when Hash
37
+ value.except(*MemberLoads::FILE_SIZE_FIELDS, *MemberLoads::MEMBER_SIZE_FIELDS)
38
+ .transform_values { |each| without_sizes(each) }
39
+ when Array then value.map { |each| without_sizes(each) }
40
+ else value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,93 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member_loads"
6
+ require_relative "loading_decision"
7
+ require_relative "accepted_loading"
8
+ require_relative "activation"
9
+
10
+ module Principles
11
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
12
+ module Models
13
+ # The members a full audit lists for the agent to weigh against the principle's activation, each with the
14
+ # facts that put it there (#355 slice 3, brief 2026-11-09-1322). It lists by fact and compares no globs, so
15
+ # whether a member loads outside the activation is the agent's judgement.
16
+ class LoadsToCheck
17
+ # activation is the map's, or nil when none is declared; loads is each member's MemberLoads by id; accepted
18
+ # is, by id, the decision the last completed audit accepted on a member's loading; hashes is each member's
19
+ # text hash now, by id.
20
+ #: (activation: Hash[String, untyped]?, loads: Hash[String, MemberLoads],
21
+ #| ?accepted: Hash[String, AcceptedLoading], ?hashes: Hash[String, String]) -> void
22
+ def initialize(activation:, loads:, accepted: {}, hashes: {})
23
+ @activation = activation
24
+ @loads = loads
25
+ @accepted = accepted
26
+ @hashes = hashes
27
+ freeze
28
+ end
29
+
30
+ # The activation, each listed member with what put it there and its loads, and the members this tool knows no
31
+ # carrier for; nil when the activation lists nothing to check.
32
+ #: () -> Hash[String, untyped]?
33
+ def listed
34
+ activation = @activation
35
+ return unless activation && Activation.new(activation).full_audit_lists == "session_start_members"
36
+
37
+ { "activation" => activation, "rows" => rows,
38
+ "not_listed_no_known_carrier" => @loads.select { |_, loads| loads.moments == ["no_known_carrier"] }.keys }
39
+ end
40
+
41
+ # Each listed member's accepted decision, by id, as the record keeps it: its reason and the loads it accepted;
42
+ # refused unless each listed member is decided exactly once, each accepted with a one-line reason.
43
+ #: (untyped) -> Hash[String, Hash[String, untyped]]
44
+ def decided(decisions)
45
+ given = loading_decisions(decisions)
46
+ expected = listed_by_id
47
+ raise incomplete(expected.keys) unless given.map(&:member_id).sort == expected.keys.sort
48
+
49
+ given.each(&:check)
50
+ given.to_h { |decision| [decision.member_id, decision.kept(expected.fetch(decision.member_id))] }
51
+ end
52
+
53
+ private
54
+
55
+ # Each of the answer's loading decisions, none when it gives no list.
56
+ #: (untyped) -> Array[LoadingDecision]
57
+ def loading_decisions(decisions)
58
+ given = decisions.is_a?(Array) ? decisions : []
59
+ given.each_with_index.map { |decision, index| LoadingDecision.new(decision, index) }
60
+ end
61
+
62
+ # Each listed member's row, by its id; none when the activation lists nothing to check.
63
+ #: () -> Hash[String, Hash[String, untyped]]
64
+ def listed_by_id = (listed&.fetch("rows") || []).to_h { |row| [row.fetch("member_id"), row] }
65
+
66
+ #: (Array[String]) -> Error
67
+ def incomplete(expected)
68
+ Error.new("incomplete_loading_decisions",
69
+ "Decide each member listed in loads_to_check_against_activation exactly once: " \
70
+ "#{expected.empty? ? "none is listed" : expected.join(", ")}",
71
+ "Give loading_decisions one {member_id, decision} for each listed member, and none for another.")
72
+ end
73
+
74
+ #: () -> Array[Hash[String, untyped]]
75
+ def rows
76
+ @loads.filter_map do |id, loads|
77
+ listed_for = loads.moments & ["session_start"]
78
+ next if listed_for.empty?
79
+
80
+ { "member_id" => id, "listed_for" => listed_for, "loads" => loads.to_h }.merge(last_accepted(id, loads.to_h))
81
+ end
82
+ end
83
+
84
+ # What the last audit accepted on the member's loading, and whether what it weighed changed since; nothing
85
+ # when no audit accepted it.
86
+ #: (String, Hash[String, untyped]) -> Hash[String, untyped]
87
+ def last_accepted(id, now)
88
+ accepted = @accepted[id] or return {}
89
+ { "last_accepted" => accepted.last_accepted(now, @hashes[id]) }
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_file"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # A refusal to locate a member, as its payload gives it, and what it makes of the member's state: lost in its
10
+ # file when its place there is gone, or else unavailable, as when its file cannot be read.
11
+ class LocateRefusal
12
+ #: (Hash[String, untyped]) -> void
13
+ def initialize(payload)
14
+ @payload = payload
15
+ freeze
16
+ end
17
+
18
+ # locator_lost when its code says the member's place in its file is gone, or else unavailable.
19
+ #: () -> String
20
+ def member_state = MemberFile::PLACE_LOST.include?(@payload["code"]) ? "locator_lost" : "unavailable"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "line_span"
5
+
6
+ module Principles
7
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # The text a locator found now: its file, the lines it spans and the hash of what it says. A Locator says where
10
+ # to look; this is what was found there, as Project#locate finds it (#909).
11
+ class LocatedText
12
+ # The text found, from what Project#locate gives.
13
+ #: (Hash[String, untyped]) -> LocatedText
14
+ def self.of(found)
15
+ new(path: found.fetch("path"), first_line: found.fetch("first_line"), last_line: found.fetch("last_line"),
16
+ sha256: found.fetch("sha256"))
17
+ end
18
+
19
+ #: String
20
+ attr_reader :path, :sha256
21
+
22
+ #: Integer
23
+ attr_reader :first_line, :last_line
24
+
25
+ #: (path: String, first_line: Integer, last_line: Integer, sha256: String) -> void
26
+ def initialize(path:, first_line:, last_line:, sha256:)
27
+ @path = path
28
+ @first_line = first_line
29
+ @last_line = last_line
30
+ @sha256 = sha256
31
+ freeze
32
+ end
33
+
34
+ # The lines it spans, whole_file when it is its whole file.
35
+ #: (?whole_file: bool) -> LineSpan
36
+ def span(whole_file: false) = LineSpan.new(@first_line, @last_line, whole_file:)
37
+
38
+ # Its first and last lines, as a request prints them.
39
+ #: () -> Hash[String, Integer]
40
+ def lines = { "first_line" => @first_line, "last_line" => @last_line }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
6
+ module Models
7
+ # Where some text is in the project: a path, with a heading, a passage between starting_at and ending_at, or a
8
+ # frontmatter query inside it, or none of those to name the whole file. A member's reference, an evidence item
9
+ # and a write-up pointer each name text this way. It keeps the hash it was given, keys and order, since request
10
+ # ids and record bytes digest it.
11
+ class Locator
12
+ # What narrows a locator within its path, as references and the answer schemas name them; a locator naming
13
+ # none of them names its whole file. A member map calls the heading "section", which Member#reference
14
+ # translates.
15
+ KEYS = %w[heading starting_at ending_at query].freeze #: Array[String]
16
+
17
+ #: (Hash[String, untyped]) -> void
18
+ def initialize(fields)
19
+ @fields = fields.dup.freeze #: Hash[String, untyped]
20
+ freeze
21
+ end
22
+
23
+ # The file it points into.
24
+ #: () -> String
25
+ def path = @fields.fetch("path")
26
+
27
+ # The heading of the section it names, if it names one.
28
+ #: () -> String?
29
+ def heading = @fields["heading"]
30
+
31
+ # The opening text of the passage it names, if it names one.
32
+ #: () -> String?
33
+ def starting_at = @fields["starting_at"]
34
+
35
+ # The closing text of the passage it names, if it gives one.
36
+ #: () -> String?
37
+ def ending_at = @fields["ending_at"]
38
+
39
+ # The frontmatter field it names, as a JSONPath query, if it names one.
40
+ #: () -> String?
41
+ def query = @fields["query"]
42
+
43
+ # Whether it names its whole file, with no heading, passage or query inside it.
44
+ #: () -> bool
45
+ def whole_file? = KEYS.none? { |key| @fields.key?(key) }
46
+
47
+ # The locator as it was given.
48
+ #: () -> Hash[String, untyped]
49
+ def to_h = @fields
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,171 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member"
6
+ require_relative "misspelt_member_id"
7
+
8
+ module Principles
9
+ # The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
10
+ module Models
11
+ # One principle map's members, as Members: finding one by its id, and the rules on ids and keys among them,
12
+ # which MemberMap asks as it changes the map. It is Enumerable over its members, in the map's order.
13
+ #: [out Elem = Member]
14
+ class MapMembers
15
+ include Enumerable
16
+
17
+ # The principle whose map these are.
18
+ #: String
19
+ attr_reader :principle_id
20
+
21
+ #: (String, Array[Member]) -> void
22
+ def initialize(principle_id, list)
23
+ @principle_id = principle_id
24
+ @list = list.freeze #: Array[Member]
25
+ freeze
26
+ end
27
+
28
+ # Each member, in the map's order, which every Enumerable method iterates.
29
+ # @override
30
+ #: () { (Member) -> void } -> void
31
+ def each(&) = @list.each(&)
32
+
33
+ # The members as the frozen list they are held in, without the copy Enumerable#to_a would make.
34
+ #: () -> Array[Member]
35
+ def to_a = @list
36
+
37
+ # Whether there are none, as in a map with no members yet.
38
+ #: () -> bool
39
+ def empty? = @list.empty?
40
+
41
+ # Each member's id, in the map's order.
42
+ #: () -> Array[String]
43
+ def ids = @list.map(&:id)
44
+
45
+ # Each member's key, in the map's order: its stored key, or its id when it has none.
46
+ #: () -> Array[String]
47
+ def keys = @list.map(&:key)
48
+
49
+ # Each member by its id.
50
+ #: () -> Hash[String, Member]
51
+ def by_id = @list.to_h { |member| [member.id, member] }
52
+
53
+ # Each file a member is in, once, in the map's order.
54
+ #: () -> Array[String]
55
+ def paths = @list.map(&:path).uniq
56
+
57
+ # Each member in one of paths, in the map's order.
58
+ #: (Array[String]) -> Array[Member]
59
+ def in_files(paths) = @list.select { |member| paths.include?(member.path) }
60
+
61
+ # The member with member_id now, or nil when none has it.
62
+ #: (untyped) -> Member?
63
+ def with_id(member_id) = @list.find { |member| member.id == member_id }
64
+
65
+ # The member known by key, its id before its first move or its id when it never moved, or nil when none is.
66
+ # The first is the only one, since a map whose members share a key is refused (repeated_keys).
67
+ #: (String) -> Member?
68
+ def with_key(key) = @list.find { |member| member.key == key }
69
+
70
+ # The check of the member with member_id, or nil when it has none or no member has that id (D30).
71
+ #: (untyped) -> String?
72
+ def check(member_id) = with_id(member_id)&.check
73
+
74
+ # Each key more than one member is known by, a member with no stored key being known by its id, with the ids of
75
+ # the members that share it. An audit record files each member under its key, so one would stand for the other.
76
+ #: () -> Hash[String, Array[String]]
77
+ def repeated_keys
78
+ @list.group_by(&:key).select { |_, sharing| sharing.length > 1 }.transform_values { it.map(&:id) }
79
+ end
80
+
81
+ # Refuses these members when two share an id or a key, as a hand edit or a merge can leave them. where names the
82
+ # map as read, and file the map file the refusal points at.
83
+ #: (where: String, file: String) -> void
84
+ def refuse_repeats(where:, file:)
85
+ refuse_repeated_ids(where, file)
86
+ refuse_repeated_keys(where, file)
87
+ end
88
+
89
+ # Each member's key by id, for the members whose key is not their id because they have moved.
90
+ #: () -> Hash[String, String]
91
+ def moved_keys = @list.to_h { |member| [member.id, member.key] }.reject { |id, key| id == key }
92
+
93
+ # The member with member_id, refused unless one has it.
94
+ #: (String) -> Member
95
+ def found(member_id)
96
+ with_id(member_id) || raise(unknown(member_id))
97
+ end
98
+
99
+ #: (String) -> bool
100
+ def taken?(member_id) = !with_id(member_id).nil?
101
+
102
+ # member, with a key of its own when its id is already the key of a member that moved away from there;
103
+ # otherwise the new member would inherit that member's audit.
104
+ #: (Member) -> Member
105
+ def keyed(member)
106
+ return member unless keys.include?(member.id)
107
+
108
+ free = (2..).lazy.map { |n| "#{member.id}~#{n}" }.find { |key| !keys.include?(key) }
109
+ member.keyed_as(free || raise(KeyError, "no free key for #{member.id}"))
110
+ end
111
+
112
+ # The refusal for an id no member has now, naming the member that had it before it moved, since a member keeps
113
+ # its id from before its first move as its key (#552). A member moved twice is not found by its middle id.
114
+ #: (String) -> Error
115
+ def unknown(member_id)
116
+ ids = "To see each member's id: #{Error::EXECUTABLE} show-principle #{@principle_id}"
117
+ moved = @list.select { |each| each.key == member_id }
118
+ return misspelt(member_id, ids) unless moved.one?
119
+
120
+ now = moved.fetch(0).id
121
+ Error.new("unknown_member", "#{member_id} is not a member of #{@principle_id}: it moved, and its id is now " \
122
+ "#{now}", "Give its id now, #{now}, in place of #{member_id}.\n#{ids}")
123
+ end
124
+
125
+ # The refusal for an id no member has or had, naming the one mapped id it is spelled like apart from case and
126
+ # punctuation, if any, under did_you_mean, so the controller can print the command again with it. The near miss
127
+ # itself is never accepted (D56, #729).
128
+ #: (String, String) -> Error
129
+ def misspelt(member_id, ids)
130
+ unknown = "#{member_id} is not a member of #{@principle_id}"
131
+ closest = MisspeltMemberId.new(member_id).closest(@list.map(&:id))
132
+ return Error.new("unknown_member", unknown, ids) unless closest
133
+
134
+ Error.new("unknown_member", "#{unknown}; did you mean #{closest}?", ids).including("did_you_mean" => closest)
135
+ end
136
+
137
+ # The oldest map format that holds every member: 1 unless a member uses a later field.
138
+ #: () -> Integer
139
+ def format = @list.map(&:map_format).max || 1
140
+
141
+ private
142
+
143
+ # The ids more than one member has, which a hand edit or a merge can leave.
144
+ #: () -> Array[String]
145
+ def repeated_ids = @list.map(&:id).tally.select { |_, count| count > 1 }.keys
146
+
147
+ # Every member is found and recorded by its id, so of two sharing one, one would silently stand for both.
148
+ #: (String, String) -> void
149
+ def refuse_repeated_ids(where, file)
150
+ shared = repeated_ids
151
+ return if shared.empty?
152
+
153
+ raise Error.new("duplicate_member", "#{where} has more than one member with id #{shared.join(", ")}",
154
+ "Keep one of each: edit the map file so that no two members point at the same place, " \
155
+ "then commit it.", file)
156
+ end
157
+
158
+ # Two members with ids of their own known by one key (#647): which keeps it decides which keeps the audit filed
159
+ # under it, so the repair leaves that to the agent.
160
+ #: (String, String) -> void
161
+ def refuse_repeated_keys(where, file)
162
+ key, ids = repeated_keys.first
163
+ return unless ids
164
+
165
+ raise Error.new("duplicate_member", "#{where} has more than one member known by key #{key}: #{ids.join(", ")}",
166
+ "Keep one key per member: the member that keeps #{key} keeps the audit filed under it, so " \
167
+ "give the other a key of its own, such as #{key}~2, then commit the map.", file)
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,92 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "member_file"
5
+ require_relative "frontmatter_fields"
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
+ # A Markdown file, where a member may be a section: a heading and what follows it, up to the next heading
11
+ # at its level or above.
12
+ class MarkdownFile < MemberFile
13
+ private
14
+
15
+ # The frontmatter field the reference's query names, or the section under its heading, or the whole text.
16
+ #: (Array[String], Locator) -> [Integer, Integer]
17
+ def region(lines, locator)
18
+ query = locator.query
19
+ return FrontmatterFields.new(path: @path, lines: lines).field(query) if query
20
+
21
+ section(lines, locator.heading)
22
+ end
23
+
24
+ # The line range of the Markdown section under heading, or the whole text without one. A section runs
25
+ # to the next heading at its level or above, so its subsections come with it.
26
+ #: (Array[String], String?) -> [Integer, Integer]
27
+ def section(lines, heading)
28
+ return [0, lines.length] unless heading
29
+
30
+ headings = heading_lines(lines)
31
+ first = only_heading(lines, headings, heading)
32
+ level = lines.fetch(first)[/\A\#+/].to_s.length
33
+ [first, headings.find { |i| i > first && lines.fetch(i).match?(/\A\#{1,#{level}}\s/) } || lines.length]
34
+ end
35
+
36
+ # The indexes of lines that are Markdown headings: a # line outside the leading frontmatter and outside
37
+ # fenced code, where # starts a comment.
38
+ #: (Array[String]) -> Array[Integer]
39
+ def heading_lines(lines)
40
+ fence = nil #: String?
41
+ (frontmatter_end(lines)...lines.length).select do |i|
42
+ marker = lines.fetch(i)[/\A {0,3}(`{3,}|~{3,})/, 1]
43
+ fence = fenced_after(fence, marker) if marker
44
+ !marker && fence.nil? && lines.fetch(i).match?(/\A\#{1,6}\s/)
45
+ end
46
+ end
47
+
48
+ # The fence still open after a line that starts with marker: it opens one, or closes the one open
49
+ # when it repeats that fence's character at least as many times.
50
+ #: (String?, String) -> String?
51
+ def fenced_after(fence, marker)
52
+ return marker unless fence
53
+
54
+ marker.start_with?(fence) ? nil : fence
55
+ end
56
+
57
+ # The index of the first line after the frontmatter, which runs from a first line of --- to the next.
58
+ #: (Array[String]) -> Integer
59
+ def frontmatter_end(lines)
60
+ return 0 unless lines.first&.chomp == "---"
61
+
62
+ closing = (1...lines.length).find { |i| lines.fetch(i).chomp == "---" }
63
+ closing ? closing + 1 : 0
64
+ end
65
+
66
+ # Whether line is a heading of one to six #s, then whitespace, whose text is heading. It compares strings
67
+ # rather than building a pattern from heading, so a heading from a map cannot make the match slow.
68
+ #: (String, String) -> bool
69
+ def titled?(line, heading)
70
+ marks = line[/\A#+/].to_s.length
71
+ return false unless (1..6).cover?(marks)
72
+
73
+ rest = line[marks..].to_s
74
+ rest.match?(/\A\s/) && rest.strip == heading
75
+ end
76
+
77
+ #: (Array[String], Array[Integer], String) -> Integer
78
+ def only_heading(lines, headings, heading)
79
+ matches = headings.select { |i| titled?(lines.fetch(i), heading) }
80
+ return matches.fetch(0) if matches.length == 1
81
+ if matches.empty?
82
+ raise Error.new("section_not_found", "No Markdown heading #{heading.inspect}",
83
+ "Give the heading's exact text without its # marks, " \
84
+ "or leave out the heading to point at the whole file.", @path)
85
+ end
86
+
87
+ raise Error.new("ambiguous_heading", "Heading #{heading.inspect} has #{matches.length} matches",
88
+ "Use a unique Markdown heading, or leave out the heading to point at the whole file.", @path)
89
+ end
90
+ end
91
+ end
92
+ end