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,201 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "shellwords"
5
+ require_relative "../error"
6
+ require_relative "../models/answer_text"
7
+ require_relative "../models/audit"
8
+ require_relative "../models/carried_assessments"
9
+ require_relative "../models/member"
10
+ require_relative "../models/member_grounds"
11
+ require_relative "../models/member_map_history"
12
+ require_relative "../models/members_sharing_lines"
13
+ require_relative "../models/project"
14
+ require_relative "../models/record_judged_against"
15
+ require_relative "../views/audit_result"
16
+ require_relative "../views/lines_shared_with_member"
17
+ require_relative "../views/map_result"
18
+ require_relative "../views/member_repairs"
19
+ require_relative "running_tool"
20
+
21
+ module Principles
22
+ # Each runs one command: reads models, applies their rules and prints through a view.
23
+ # controllers/CLAUDE.md owns the boundary.
24
+ module Controllers
25
+ # What every command controller shares: the project and its models, the views that print, and the
26
+ # running executable, which only a controller knows. Reads change nothing on disk, map writes touch only
27
+ # the committed map, and audit writes only the committed audit record; lib/principles/CLAUDE.md owns the
28
+ # naming rule that keeps the three apart.
29
+ class Command
30
+ # The command as the agent can run it from a checkout or an installed gem: this Ruby and this checkout's
31
+ # executable, because this tool is not on the PATH there. The path is shell-quoted, since an install
32
+ # directory may hold a space. An entry point that is on the PATH names itself instead.
33
+ INVOCATION = "ruby #{Shellwords.escape(File.expand_path("../../../exe/principles", __dir__))}".freeze #: String
34
+
35
+ # tool is this run of the tool: the executable every printed command names, and the version running.
36
+ #: (Models::Project, tool: RunningTool) -> void
37
+ def initialize(project, tool:)
38
+ @project = project
39
+ @tool = tool
40
+ @member_maps = @project.member_maps #: Models::MemberMaps
41
+ @audit_records = @project.audit_records #: Models::AuditRecords
42
+ @history = @project.history #: Models::ProjectHistory
43
+ end
44
+
45
+ # Runs command; each controller runs its own group's commands and refuses the rest.
46
+ #: (String, String?, Hash[String, untyped]) -> untyped
47
+ def call(command, _id, _options)
48
+ naming_the_executable { unknown(command) }
49
+ end
50
+
51
+ private
52
+
53
+ # The history of principle_id's map, read through the collaborators this controller already holds.
54
+ #: (String) -> Models::MemberMapHistory
55
+ def map_history(principle_id)
56
+ Models::MemberMapHistory.new(id: principle_id, history: @history, member_maps: @member_maps)
57
+ end
58
+
59
+ # Saves the map, and nothing else, then prints the result the block builds from the map as written.
60
+ #: (Models::MemberMap) { (Views::PrintedPrinciple) -> Hash[String, untyped] } -> untyped
61
+ def write_map(map, &)
62
+ wrote = @member_maps.save(map)
63
+ yield(printed_principle(map)).merge("wrote" => [wrote])
64
+ end
65
+
66
+ # Runs the block, putting the running executable into any refusal a model raised, followed by this project,
67
+ # so a repair runs on the same project from anywhere, as answer_with does (#572). rerun, when given, prints the
68
+ # command running now with another member id, which a refusal naming a near miss leads its repair with (D56).
69
+ #: [T] (?rerun: (^(String) -> String)?) { () -> T } -> T
70
+ def naming_the_executable(rerun: nil, &)
71
+ yield
72
+ rescue Error => e
73
+ refusal = (rerun ? e.rerun_with_near_miss(&rerun) : e).printing_commands do |command|
74
+ invocation.listed_in_project(command.command, command.values, options: command.options, usage: command.usage)
75
+ end
76
+ raise with_member_repairs(refusal).naming("#{@tool.executable} #{invocation.project_option}")
77
+ end
78
+
79
+ # A broken_members refusal, with each member's own repair commands beside it as repair_with, since a
80
+ # model's repair can only name placeholders (#252). Any other refusal is returned as it is.
81
+ #: (Error) -> Error
82
+ def with_member_repairs(error)
83
+ return error unless error.payload["code"] == "broken_members"
84
+
85
+ repairs = Views::MemberRepairs.new(invocation)
86
+ members = error.payload.fetch("members").map do |entry|
87
+ repaired({ "principle" => error.payload["principle"] }.compact.merge(entry), repairs)
88
+ end
89
+ error.including("members" => members)
90
+ end
91
+
92
+ # entry with repair_with, when it names a member its map still lists.
93
+ #: (Hash[String, untyped], Views::MemberRepairs) -> Hash[String, untyped]
94
+ def repaired(entry, repairs)
95
+ principle = entry["principle"] or return entry
96
+ id = entry["id"] or return entry
97
+ fields = @member_maps.find(principle).member(id).merge(entry.slice("code")).merge("id" => id)
98
+ entry.merge("repair_with" => repairs.commands(principle, fields))
99
+ rescue Error
100
+ entry
101
+ end
102
+
103
+ #: (String) -> bot
104
+ def unknown(command)
105
+ raise Error.new("unknown_command", "Unknown command #{command}",
106
+ "To see every command: #{@tool.executable} --help")
107
+ end
108
+
109
+ # The controller this one hands part of its command to, over the same project and naming the same executable.
110
+ #: [C < Command] (Class[C], **untyped) -> C
111
+ def handed(controller, **)
112
+ controller.new(@project, tool: @tool, **)
113
+ end
114
+
115
+ # The executable and project that printed commands name, built for each run and never stored.
116
+ #: () -> Views::Invocation
117
+ def invocation
118
+ Views::Invocation.new(executable: @tool.executable, project: @project.root)
119
+ end
120
+
121
+ #: () -> Views::AuditResult
122
+ def audit_result
123
+ Views::AuditResult.new(invocation)
124
+ end
125
+
126
+ #: () -> Views::MapResult
127
+ def map_result
128
+ Views::MapResult.new(invocation, in_git: @history.inside_git?)
129
+ end
130
+
131
+ #: (String?) -> String
132
+ def required_id(id)
133
+ unless id
134
+ raise Error.new("missing_id", "An id is required",
135
+ "To see each command's examples: #{@tool.executable} --help")
136
+ end
137
+
138
+ id
139
+ end
140
+
141
+ #: (untyped, String) -> String
142
+ def text(value, field) = Models::AnswerText.new(value).required(field)
143
+
144
+ # The principle as the view prints it, with each member's id beside its locator.
145
+ #: (Models::MemberMap, ?members: Array[Hash[String, untyped]]) -> Views::PrintedPrinciple
146
+ def printed_principle(map, members: map.members_shown)
147
+ Views::PrintedPrinciple.new(id: map.id, file: @member_maps.file(map.id), description: map.description,
148
+ members: members, activation: map.activation)
149
+ end
150
+
151
+ # Each member's hash at base, or nil outside git.
152
+ #: (String?, Models::MemberMap) -> Hash[String, String]?
153
+ def committed(base, map) = base ? @history.hashes_at(base, map.members) : nil
154
+
155
+ # When each of the map's members loads, by id, which a full audit's request lists against the activation and
156
+ # show-principle prints, from the project's hooks as hooks read them.
157
+ #: (Models::MemberMap, ?Models::HookCommands) -> Hash[String, Models::MemberLoads]
158
+ def member_loads(map, hooks = @project.hook_commands) = @project.member_loads(map.members, hooks)
159
+
160
+ # The members a full audit lists to check against its activation, each with the decision its last completed
161
+ # audit, whose record is given or read now, accepted on the member's loading (#355, #502).
162
+ #: (Models::Audit, ?Models::AuditRecord?) -> Models::LoadsToCheck
163
+ def loads_to_check(audit, record = @audit_records.find(audit.id))
164
+ audit.loads_to_check(member_loads(audit.map), record)
165
+ end
166
+
167
+ # The principle's audit record as it stands, as an assessment answer is judged against it: the members whose
168
+ # loads it decides and the assessments it may carry forward, from one read of the record.
169
+ #: (Models::Audit) -> Models::RecordJudgedAgainst
170
+ def record_judged_against(audit)
171
+ record = @audit_records.find(audit.id)
172
+ Models::RecordJudgedAgainst.new(loads: loads_to_check(audit, record),
173
+ carried: carried_assessments(audit, record), record: record)
174
+ end
175
+
176
+ # What each member's assessment was judged on beside its text: its map entry and when it loads (D49).
177
+ #: (Models::Audit) -> Models::MemberGrounds
178
+ def grounds(audit) = audit.grounds(member_loads(audit.map))
179
+
180
+ # Which members' last assessments a full audit carries forward, from its record, given or read now, and the
181
+ # question it asks (D49).
182
+ #: (Models::Audit, ?Models::AuditRecord?) -> Models::CarriedAssessments
183
+ def carried_assessments(audit, record = @audit_records.find(audit.id))
184
+ audit.carried_assessments(record, grounds: grounds(audit), question: Views::AssessmentRequest.question)
185
+ end
186
+
187
+ # Where a member is in its file now, and the hash of what it says there.
188
+ #: (Models::Locator) -> Hash[String, untyped]
189
+ def snapshot(locator)
190
+ @project.locate(locator)
191
+ end
192
+
193
+ # The members sharing a line with member where it is now, in map and in the other maps, as add-member and
194
+ # show-principle print them (#383, #541).
195
+ #: (Models::Member, Models::MemberMap) -> Views::LinesSharedWithMember
196
+ def lines_shared(member, map)
197
+ Views::LinesSharedWithMember.new(**Models::MembersSharingLines.of(@project, member).facts_beside(map))
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,99 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "../models/audit"
6
+ require_relative "../models/correction_answer"
7
+ require_relative "../models/declared_map_changes"
8
+ require_relative "../models/request_id"
9
+ require_relative "../views/prompts"
10
+ require_relative "audit_completion"
11
+
12
+ module Principles
13
+ # Each runs one command: reads models, applies their rules and prints through a view.
14
+ # controllers/CLAUDE.md owns the boundary.
15
+ module Controllers
16
+ # A full audit's correction, which submit-answer and show-answer-check hand over: this tool checks each
17
+ # disposition, and any members moved or whose role or check a fix changed, against the map and hashes the
18
+ # audit began from, and completes the audit (D28, D40).
19
+ class Correction < Command
20
+ # The result of answering a correction request, and the record change it makes, not yet written. A refused
21
+ # answer to a request printed before the record changed is refused as stale, so the agent starts again rather
22
+ # than repairing it (#671).
23
+ #: (Models::RequestId, untyped, Models::Audit) -> [untyped, Array[Models::AuditRecord]]
24
+ def answered(request_id, response, audit)
25
+ Models::CorrectionBasis.new(response.fetch("basis")).printed_after
26
+ .refusing_stale_first(audit.id, @audit_records.find(audit.id)) do
27
+ judged_correction(request_id, response, audit)
28
+ end
29
+ end
30
+
31
+ # What each map entry a fix claims changed, from the map the audit the block gives started from and its map
32
+ # now; the audit is built and the base map read only when a fix claims one (#462).
33
+ #: (untyped) { () -> Models::Audit } -> Array[Hash[String, untyped]]
34
+ def map_entry_changes(response, &)
35
+ changes = Models::DeclaredMapChanges.declared_in(response)
36
+ changes.entry_changes do
37
+ audit = yield
38
+ [started_from(Models::CorrectionBasis.new(response.fetch("basis")), audit, changes), audit.map]
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # The result of judging a correction answer: its basis, moves and dispositions checked, and the audit completed.
45
+ #: (Models::RequestId, untyped, Models::Audit) -> [untyped, Array[Models::AuditRecord]]
46
+ def judged_correction(request_id, response, audit)
47
+ basis = Models::CorrectionBasis.new(response.fetch("basis"))
48
+ changes = Models::DeclaredMapChanges.declared_in(response)
49
+ moves = Models::MemberMoves.new(response["moved_members"])
50
+ before = moves.hashes(confirmed_start(request_id, basis, audit, changes))
51
+ findings = moves.findings(basis.findings)
52
+ start = audit.resumed(basis, before, moves)
53
+ completion(changes, basis).completed(start, audit, dispositions(response, findings, start, audit), findings,
54
+ basis.prior_decided.moved(moves))
55
+ end
56
+
57
+ # The packaged answer guide, which the correction request prints, is the one owner of its samples, the text
58
+ # the agent saw, so its sample for each action is handed to the model, which refuses a field given back as
59
+ # its sample (#789). Should the tool be upgraded between request and answer, that answer is checked only
60
+ # against the new guide's samples.
61
+ #: (untyped, Array[untyped], Models::AuditStart, Models::Audit) -> Array[Models::Disposition]
62
+ def dispositions(response, findings, start, audit)
63
+ samples = Views::Prompts.data("dispose-of-each-finding/answer-guide").fetch("disposition")
64
+ Models::CorrectionAnswer.new(response, findings, samples:) { |locator| snapshot(locator) }
65
+ .dispositions(changed: start.changed(now: audit.now))
66
+ end
67
+
68
+ # The map the audit began from, read from the base commit when the answer declares changes to the map.
69
+ #: (Models::CorrectionBasis, Models::Audit, Models::DeclaredMapChanges) -> Models::MemberMap
70
+ def started_from(basis, audit, changes)
71
+ audit.map_started_from(basis, changes, map_file: @member_maps.file(audit.id)) do |base|
72
+ committed_map(base, audit.id)
73
+ end
74
+ end
75
+
76
+ # The map id as the base commit holds it.
77
+ #: (String, String) -> Models::MemberMap
78
+ def committed_map(base, id)
79
+ map_history(id).map_at(base)
80
+ end
81
+
82
+ # Each member's hash when the audit began, checked against the request id: from the map it started from at the
83
+ # base commit in git, after the declared changes are checked against that map, or from the open record outside
84
+ # git.
85
+ #: (Models::RequestId, Models::CorrectionBasis, Models::Audit, Models::DeclaredMapChanges) -> Hash[String, String]
86
+ def confirmed_start(request_id, basis, audit, changes)
87
+ map = started_from(basis, audit, changes)
88
+ base = basis.base_commit
89
+ found = base ? committed(base, map) : @audit_records.find(audit.id)&.base_members
90
+ audit.confirmed_start(request_id, basis, found)
91
+ end
92
+
93
+ #: (Models::DeclaredMapChanges, Models::CorrectionBasis) -> AuditCompletion
94
+ def completion(map_changes, basis)
95
+ handed(AuditCompletion, map_changes: map_changes, replacing: basis.printed_after)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,107 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../models/command_steps"
5
+ require_relative "command"
6
+ require_relative "../views/help"
7
+ require_relative "../views/workflow_diagram"
8
+
9
+ module Principles
10
+ # Each runs one command: reads models, applies their rules and prints through a view.
11
+ # controllers/CLAUDE.md owns the boundary.
12
+ module Controllers
13
+ # --help: reads every command's steps through the model and hands the help view the plain values it prints.
14
+ class Help
15
+ # tool is the running tool. Only the overview reads its version, which the command line reads from the build
16
+ # or the checkout; its executable heads each command's page and starts the command it says to run next. The
17
+ # diagram names neither.
18
+ #: (tool: RunningTool, ?steps: Models::CommandSteps) -> void
19
+ def initialize(tool:, steps: Models::CommandSteps.load)
20
+ @tool = tool
21
+ @steps = steps
22
+ end
23
+
24
+ #: () -> String
25
+ def overview
26
+ groups = @steps.groups.map do |group|
27
+ entries = @steps.commands.select { |command| command.group == group.id }
28
+ .map { |command| [command.usage, command.summary] }
29
+ [group.heading, entries]
30
+ end
31
+ Views::Help::Overview.new(version: @tool.version, groups: groups, executable: @tool.executable).to_s
32
+ end
33
+
34
+ # The page for the named command, or nil when there is no such command.
35
+ #: (String) -> String?
36
+ def command(name)
37
+ command = @steps.command(name)
38
+ return unless command
39
+
40
+ Views::Help::CommandPage.new(usage: command.usage, prompt: command.help, writes: @steps.writes(command),
41
+ steps: command.steps.map { |step| page_step(command, step) },
42
+ executable: @tool.executable).to_s
43
+ end
44
+
45
+ # The docs page drawing every command's steps and where each leads.
46
+ #: () -> String
47
+ def diagram
48
+ commands = @steps.commands.map do |command|
49
+ nodes = command.steps.map do |step|
50
+ Views::WorkflowDiagram::Node.new(key: key(command, step), label: step.id, agent: step.agent?)
51
+ end
52
+ [command.usage, nodes] #: [String, Array[Views::WorkflowDiagram::Node]]
53
+ end
54
+ Views::WorkflowDiagram.new(commands: commands, edges: @steps.commands.flat_map { |command| edges(command) })
55
+ .to_s
56
+ end
57
+
58
+ private
59
+
60
+ # Each step's edges: to each branch with its condition, to the step after it, or to the first step of
61
+ # the command the agent answers with.
62
+ #: (Models::CommandSteps::Command) -> Array[Views::WorkflowDiagram::Edge]
63
+ def edges(command)
64
+ command.steps.flat_map do |step|
65
+ inside = command.successors(step).each_with_index.map do |successor, index|
66
+ Views::WorkflowDiagram::Edge.new(from: key(command, step), to: key(command, successor),
67
+ label: step.branches[index]&.condition)
68
+ end
69
+ [*inside, *handover(command, step)]
70
+ end
71
+ end
72
+
73
+ #: (Models::CommandSteps::Command, Models::CommandSteps::Step) -> Array[Views::WorkflowDiagram::Edge]
74
+ def handover(command, step)
75
+ target = step.handover.answer_with
76
+ answered = target && @steps.command(target)
77
+ return [] unless target && answered
78
+
79
+ [Views::WorkflowDiagram::Edge.new(from: key(command, step), to: key(answered, answered.steps.fetch(0)),
80
+ label: target, handover: true)]
81
+ end
82
+
83
+ # A step's Mermaid id: its command's and its own position, since step ids repeat across commands.
84
+ #: (Models::CommandSteps::Command, Models::CommandSteps::Step) -> String
85
+ def key(command, step)
86
+ "c#{@steps.commands.index(command)}s#{command.steps.index(step)}"
87
+ end
88
+
89
+ #: (Models::CommandSteps::Command, Models::CommandSteps::Step) -> Views::Help::Step
90
+ def page_step(command, step)
91
+ branches = step.branches.map do |branch|
92
+ [branch.condition, command.steps.index(command.find(branch.to)).to_i + 1] #: [String, Integer]
93
+ end
94
+ Views::Help::Step.new(agent: step.agent?, does: step.does, handover: page_handover(step.handover),
95
+ branches: branches)
96
+ end
97
+
98
+ #: (Models::CommandSteps::Handover) -> Views::Help::Handover
99
+ def page_handover(handover)
100
+ target = handover.answer_with
101
+ Views::Help::Handover.new(checks: handover.checks, cannot_check: handover.cannot_check,
102
+ then_run: handover.answer_usage || (target && @steps.command(target)&.usage),
103
+ then_run_when: handover.answer_when)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,181 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "members_a_commit_changed"
6
+ require_relative "../models/member_problems"
7
+ require_relative "../models/member_state"
8
+ require_relative "../models/meta_principle"
9
+ require_relative "../models/problem_report"
10
+ require_relative "../views/problem_result"
11
+ require_relative "../views/problem_lines"
12
+ require_relative "../views/candidate_files_result"
13
+ require_relative "../views/principle_read_result"
14
+
15
+ module Principles
16
+ # Each runs one command: reads models, applies their rules and prints through a view.
17
+ # controllers/CLAUDE.md owns the boundary.
18
+ module Controllers
19
+ # The reads that show what a project holds: its candidate instruction files, its principles, and each
20
+ # member and how fresh it is against the last audit. None changes anything on disk.
21
+ class Inventory < Command
22
+ #: (String, String?, Hash[String, untyped]) -> untyped
23
+ def call(command, id, options)
24
+ naming_the_executable do
25
+ case command
26
+ when "list-candidate-files" then list_candidate_files.merge("wrote" => [])
27
+ when "list-principles" then listing.merge("wrote" => [])
28
+ when "list-member-problems" then problems_printed(id, options)
29
+ when "show-principle" then members(required_id(id)).merge("wrote" => [])
30
+ else unknown(command)
31
+ end
32
+ end
33
+ end
34
+
35
+ # Each principle id names as list-principles lists it: its description and its members by freshness, or the
36
+ # refusal that kept its map from being read.
37
+ #: (Array[String]) -> Array[Hash[String, untyped]]
38
+ def rows(ids) = ids.map { entry(it) }
39
+
40
+ private
41
+
42
+ #: () -> untyped
43
+ def list_candidate_files
44
+ found = @project.candidate_files
45
+ listed = Views::CandidateFilesResult::Found.new(inventory: found.to_h, untracked: found.untracked,
46
+ owner_asks: found.owner_asks)
47
+ Views::CandidateFilesResult.new(invocation).question(listed, existing_maps: @member_maps.ids,
48
+ map_file: @member_maps.file("ID"), set_up: set_up?)
49
+ end
50
+
51
+ #: (String) -> untyped
52
+ def members(id)
53
+ principle = @member_maps.find(id)
54
+ audit = @audit_records.find(id)
55
+ hooks = @project.hook_commands
56
+ members = shown_members(principle, audit, hooks)
57
+ shown = read_result.shown(printed_principle(principle, members: members),
58
+ broken: broken_ids(members), hook_settings_not_read: hooks.not_read,
59
+ loaded_files: @project.loaded_files(principle.members, hooks).to_h)
60
+ return shown unless audit
61
+
62
+ shown.merge("last_audit" => audit.last_audit(map_version: principle.version))
63
+ end
64
+
65
+ # Each member as show-principle prints it, with its loads read once for the whole map, from hooks.
66
+ #: (Models::MemberMap, Models::AuditRecord?, Models::HookCommands) -> Array[Hash[String, untyped]]
67
+ def shown_members(principle, audit, hooks)
68
+ loads = member_loads(principle, hooks)
69
+ principle.members_shown.map do |member|
70
+ shown_member(member, audit, loads.fetch(member.fetch("id"))).merge(shared(member, map: principle))
71
+ end
72
+ end
73
+
74
+ # What show-principle prints of one member: where it is now, its freshness and when its file loads (D38).
75
+ # The span sits beside the id too, as in add-member's and move-member's results (alpha friction).
76
+ #: (Hash[String, untyped], Models::AuditRecord?, Models::MemberLoads) -> Hash[String, untyped]
77
+ def shown_member(member, audit, loads)
78
+ now = fresh(member, audit)
79
+ member.merge(now.fetch("snapshot", {}).slice("first_line", "last_line"), now,
80
+ "loads" => loads.to_h)
81
+ end
82
+
83
+ # The id of each shown member whose reference is broken, so an audit would refuse it, in the map's order.
84
+ #: (Array[Hash[String, untyped]]) -> Array[String]
85
+ def broken_ids(members)
86
+ members.select { |member| Models::MemberState.new(member.fetch("id"), member.fetch("freshness")).broken? }
87
+ .map { |member| member.fetch("id") }
88
+ end
89
+
90
+ # The members sharing a line with this one, in its map and in others, as add-member names them, so a member
91
+ # whose lines others hold too does not look as if its member_bytes were its alone (#541). A member this tool
92
+ # cannot find now shares nothing it can name.
93
+ #: (Hash[String, untyped], map: Models::MemberMap) -> Hash[String, untyped]
94
+ def shared(member, map:)
95
+ lines_shared(Models::Member.new(member), map).to_h
96
+ rescue Error
97
+ {}
98
+ end
99
+
100
+ # Where the member is now and whether it changed since the last audit assessed it.
101
+ #: (Hash[String, untyped], Models::AuditRecord?) -> Hash[String, untyped]
102
+ def fresh(member, audit)
103
+ @project.freshness(Models::Member.new(member).locator) do |sha|
104
+ audit ? audit.members.freshness(Models::Member.new(member).key, sha) : "unassessed"
105
+ end
106
+ end
107
+
108
+ #: () -> Views::PrincipleReadResult
109
+ def read_result
110
+ Views::PrincipleReadResult.new(invocation)
111
+ end
112
+
113
+ #: () -> untyped
114
+ def listing
115
+ read_result.listing(rows(@member_maps.ids), set_up: set_up?)
116
+ end
117
+
118
+ # Whether principles is set up, as MemberMaps decides it (D68, D75).
119
+ #: () -> bool
120
+ def set_up? = @member_maps.set_up?
121
+
122
+ # list-member-problems' JSON result, or with --short its lines for a trigger (#291), or with --hook-event the
123
+ # after-commit hook's JSON, which MembersACommitChanged prints (D73).
124
+ #: (String?, Hash[String, untyped]) -> untyped
125
+ def problems_printed(id, options)
126
+ if (event = options["hook_event"])
127
+ return handed(MembersACommitChanged, report: -> { checked_report(id) })
128
+ .printed(after: options.fetch("commits_after"), event: event)
129
+ end
130
+
131
+ problems = Models::ProblemReport.new(problems_of(id), history: @history)
132
+ report = member_problems(problems)
133
+ return report.merge("wrote" => []) unless options["short"]
134
+
135
+ Views::ProblemLines.new(invocation).short(report, nothing_changed: Views::ProblemResult::NOTHING_CHANGED,
136
+ assessed_change: problems.assessed_change?)
137
+ end
138
+
139
+ # list-member-problems' JSON result for map id, or every map, without the files it wrote.
140
+ #: (String?) -> Hash[String, untyped]
141
+ def checked_report(id) = member_problems(Models::ProblemReport.new(problems_of(id), history: @history))
142
+
143
+ # The result for problems, every member of map id or of every map that is broken or whose own text changed
144
+ # (#187).
145
+ #: (Models::ProblemReport) -> untyped
146
+ def member_problems(problems)
147
+ report = problems.checked do |map_id, member, baseline, now|
148
+ map_history(map_id).member_history(member, baseline: baseline, now: now)
149
+ end
150
+ Views::ProblemResult.new(invocation).checked(report, not_read: @project.unread_older_files,
151
+ no_baseline_files: problems.files_to_commit(@member_maps),
152
+ acknowledgeable: problems.acknowledgeable)
153
+ end
154
+
155
+ # Map id's members against their baselines, or every map's, listing a map it cannot read among them.
156
+ #: (String?) -> Array[Models::MemberProblems]
157
+ def problems_of(id)
158
+ return [Models::MemberProblems.for(@project, id)] if id
159
+
160
+ @member_maps.ids.map { Models::MemberProblems.for(@project, it, listing_all: true) }
161
+ end
162
+
163
+ # One map's row; a map this tool cannot read is listed with its refusal, so the others still show (D29).
164
+ #: (String) -> Hash[String, untyped]
165
+ def entry(id)
166
+ map = @member_maps.find(id)
167
+ row = { "id" => id, "description" => map.description, "members_by_freshness" => members_by_freshness(map) }
168
+ map.unstarted? ? row.merge(read_result.no_members(@member_maps.file(id))) : row
169
+ rescue Error => e
170
+ { "id" => id, "refusal" => e.payload }
171
+ end
172
+
173
+ # How many of map's members are in each freshness state, against its principle's last audit.
174
+ #: (Models::MemberMap) -> Hash[String, Integer]
175
+ def members_by_freshness(map)
176
+ audit = @audit_records.find(map.id)
177
+ map.members_shown.map { |member| fresh(member, audit).fetch("freshness") }.tally.sort.to_h
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "command"
5
+ require_relative "../models/activation"
6
+
7
+ module Principles
8
+ # Each runs one command: reads models, applies their rules and prints through a view.
9
+ # controllers/CLAUDE.md owns the boundary.
10
+ module Controllers
11
+ # The writes to what a principle declares as a whole in its committed map: that it exists, its
12
+ # description, and when its work is in hand (D38). This tool asks, the agent answers, and this tool writes
13
+ # the map and nothing else. MemberEntries writes each member's entry.
14
+ class Mapping < Command
15
+ # Each command and the method that runs it.
16
+ COMMANDS = {
17
+ "create-principle" => :create_principle, "change-description" => :change_description,
18
+ "change-activation" => :change_activation
19
+ }.freeze #: Hash[String, Symbol]
20
+
21
+ #: (String, String?, Hash[String, untyped]) -> untyped
22
+ def call(command, id, options)
23
+ naming_the_executable do
24
+ method = COMMANDS[command] or unknown(command)
25
+ send(method, required_id(id), options)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ #: (String, Hash[String, untyped]) -> untyped
32
+ def create_principle(id, options)
33
+ map = @member_maps.create(id) { text(options["description"], "--description") }
34
+ write_map(map) { |printed| map_result.created(printed) }
35
+ end
36
+
37
+ #: (String, Hash[String, untyped]) -> untyped
38
+ def change_description(id, options)
39
+ map = @member_maps.find(id).describing(text(options["description"], "--description"))
40
+ write_map(map) { |printed| map_result.description_changed(printed) }
41
+ end
42
+
43
+ # Records when the principle's work is in hand, as the agent judged it (D38).
44
+ #: (String, Hash[String, untyped]) -> untyped
45
+ def change_activation(id, options)
46
+ activation = Models::Activation.declared(when_files: options["when_files"],
47
+ when_invoked: options["when_invoked"], always: options["always"])
48
+ map = @member_maps.find(id)
49
+ activated = map.activating(activation)
50
+ return map_result.activation_unchanged(printed_principle(map)).merge("wrote" => []) if activated.same_as?(map)
51
+
52
+ write_map(activated) { |printed| map_result.activation_changed(printed, lists: activation.full_audit_lists) }
53
+ end
54
+ end
55
+ end
56
+ end