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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51ce6acd3b9d638e2948da0d6e41e0eb4b7656dff90a976bf976f4232020c431
4
+ data.tar.gz: 803b232afd27b6050db0197f2d77accc7afe852545d17000f2c768a4138aa402
5
+ SHA512:
6
+ metadata.gz: bb6296b076b2b9ce37d5ad707d1b8312a95edd7e0341da94ee9f7da1587471536db19d1fb3afe072de8f297100fe02da308a7f553b13e99608509413eadcdd0a
7
+ data.tar.gz: 89b4e1074966cc96af2769263ac1ed9210dd0175c3d0c4490ff681dbfc41da09157a20189e1380e06ae61c5fea7cc261b479c1d1bd1783265163ebd167baafa9
data/exe/principles ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ # A Ruby this tool cannot run on is refused, or handed to one it can, before lib/ loads.
6
+ require_relative "../lib/principles/supported_ruby"
7
+ Principles::SupportedRuby.new(RUBY_VERSION, ENV.to_h).run_here_or_on_another(__FILE__, ARGV)
8
+
9
+ require_relative "../lib/principles"
10
+ exit Principles::CLI.run(ARGV)
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "invocation_options"
5
+
6
+ module Principles
7
+ # The options the plugin's after-commit hook gives list-member-problems, --commits-after and --hook-event (D73),
8
+ # and what is wrong with them as given, which CommandLine refuses as a usage error.
9
+ class AfterCommitHookOptions
10
+ # The events Claude Code runs the after-commit hook on, which --hook-event echoes back to it.
11
+ EVENTS = %w[PostToolUse PostToolUseFailure].freeze #: Array[String]
12
+ # A commit's full sha, SHA-1 or SHA-256, which the hook resolves HEAD@{1} to before passing it.
13
+ FULL_SHA = /\A\h{40}(?:\h{24})?\z/ #: Regexp
14
+
15
+ #: (InvocationOptions) -> void
16
+ def initialize(options)
17
+ @after = options.value("commits_after") #: String?
18
+ @event = options.value("hook_event") #: String?
19
+ @short = !options.value("short").nil? #: bool
20
+ freeze
21
+ end
22
+
23
+ # What is wrong with the options as given, or nil when they fit or neither is given.
24
+ #: () -> String?
25
+ def fault
26
+ after = @after
27
+ event = @event
28
+ return if after.nil? && event.nil?
29
+ return "--commits-after and --hook-event are given together, for the after-commit hook" unless after && event
30
+
31
+ value_fault(after, event) || ("--commits-after prints the hook's JSON, not the --short lines" if @short)
32
+ end
33
+
34
+ private
35
+
36
+ # What is wrong with either value, or nil when both are ones the hook passes.
37
+ #: (String, String) -> String?
38
+ def value_fault(after, event)
39
+ return "--hook-event takes PostToolUse or PostToolUseFailure, not #{event}" unless EVENTS.include?(event)
40
+
41
+ "--commits-after takes a commit's full sha, not #{after}" unless FULL_SHA.match?(after)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,149 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require_relative "data/running_version"
6
+ require_relative "models/project"
7
+ require_relative "command_routes"
8
+ require_relative "controllers/help"
9
+ require_relative "command_line"
10
+
11
+ module Principles
12
+ # Parses one invocation, runs its step and prints the JSON result. Every refusal is one JSON object
13
+ # naming its code and repair, because an agent's shell may merge stdout and stderr.
14
+ class CLI
15
+ # The repair for a shape error raised from this tool's own code. Such an error cannot say which file was at
16
+ # fault, so the repair names every file a command reads, and blames none (D29).
17
+ SHAPE_REPAIR = "A file this command read is not in the shape this tool writes: the map in " \
18
+ ".principles/maps/ID.json, its audit record in .principles/audits/ID.json, or the answer given " \
19
+ "with --file. Compare it with git, or with the request's response_template."
20
+
21
+ # Runs one invocation and returns its exit status: 0 for a result, 1 for a refusal, 2 for a usage error, and 3
22
+ # when list-member-problems --short finds a member an audit assessed changed, or 4 when it finds only edits to
23
+ # members no audit has assessed (D44). executable is the command every printed command
24
+ # names: the entry point says how it is run.
25
+ #: (Array[String], ?IO, ?IO, ?IO, ?executable: String) -> Integer
26
+ def self.run(argv, stdout = $stdout, stderr = $stderr, stdin = $stdin,
27
+ executable: Controllers::Command::INVOCATION)
28
+ new(stdout, stderr, stdin, executable: executable).run(argv)
29
+ end
30
+
31
+ #: (IO, IO, IO, executable: String) -> void
32
+ def initialize(stdout, stderr, stdin, executable:)
33
+ @stdout = stdout
34
+ @stderr = stderr
35
+ @stdin = stdin
36
+ @executable = executable
37
+ end
38
+
39
+ #: (Array[String]) -> Integer
40
+ def run(argv)
41
+ dispatch(CommandLine.new(argv, positional: CommandRoutes::POSITIONAL, executable: @executable))
42
+ rescue Error => e
43
+ refused(e.payload, 1)
44
+ rescue OptionParser::ParseError, KeyError, TypeError, NoMethodError, ArgumentError, SystemCallError => e
45
+ refused(refusal(e), e.is_a?(OptionParser::ParseError) ? 2 : 1)
46
+ end
47
+
48
+ private
49
+
50
+ #: (CommandLine) -> Integer
51
+ def dispatch(line)
52
+ return printed(running_version) if line.option("version")
53
+
54
+ command = line.command or return printed(help.overview)
55
+ return printed(help_for(command)) if line.option("help")
56
+
57
+ line.check
58
+ printed_result(result(command, line))
59
+ end
60
+
61
+ # The command's result, from the controller for its group.
62
+ #: (String, CommandLine) -> untyped
63
+ def result(command, line)
64
+ options = line.command_options
65
+ # One byte past the limit is enough for the answer's own check to refuse it as too large.
66
+ options["stdin"] = @stdin.read(Data::BoundedFile::LIMIT + 1).to_s if options["file"] == "-"
67
+ project = Models::Project.at(options.fetch("project"))
68
+ CommandRoutes::CONTROLLERS.fetch(command).new(project, tool: running_tool)
69
+ .call(command, line.id, options)
70
+ end
71
+
72
+ # The JSON object an agent reads for a failure that is not one of this tool's refusals.
73
+ #: (StandardError) -> Hash[untyped, untyped]
74
+ def refusal(error)
75
+ case error
76
+ when OptionParser::ParseError
77
+ failure(error, status: "usage_error", code: "invalid_invocation", repair: usage_repair(error))
78
+ when SystemCallError
79
+ failure(error, status: "refused", code: "filesystem_error",
80
+ repair: "Check the project path and file permissions, then retry.")
81
+ else
82
+ failure(error, status: "refused", code: "invalid_shape", repair: SHAPE_REPAIR)
83
+ end
84
+ end
85
+
86
+ # The repair a refused command line carries for the command it names, or the advice to see every command.
87
+ #: (OptionParser::ParseError) -> String
88
+ def usage_repair(error)
89
+ error.is_a?(CommandLine::UsageRefusal) ? error.repair : "To see every command: #{@executable} --help"
90
+ end
91
+
92
+ # Prints the refusal for the agent and returns the exit status.
93
+ #: (Hash[untyped, untyped], Integer) -> Integer
94
+ def refused(payload, status)
95
+ @stderr.puts(visible(JSON.generate(payload)))
96
+ status
97
+ end
98
+
99
+ #: (StandardError, status: String, code: String, repair: String) -> Hash[Symbol, String]
100
+ def failure(error, status:, code:, repair:)
101
+ { status: status, code: code, message: error.message, repair: repair }
102
+ end
103
+
104
+ # json with every invisible formatting character and line or paragraph separator escaped, as JSON allows.
105
+ # Text from project files can hold a right-to-left override or a zero-width space, which makes a printed
106
+ # check or id look like something it is not (#227).
107
+ #: (String) -> String
108
+ def visible(json)
109
+ json.gsub(/[\p{Cf}\p{Zl}\p{Zp}]/) do |character|
110
+ character.encode("UTF-16BE").unpack("n*").map { |unit| format("\\u%04x", unit) }.join
111
+ end
112
+ end
113
+
114
+ # A result prints as JSON, or as lines with their own exit status, as list-member-problems --short does.
115
+ #: (untyped) -> Integer
116
+ def printed_result(result)
117
+ return printed(visible(JSON.pretty_generate(result))) unless result.is_a?(Views::Lines)
118
+
119
+ @stdout.print(result.stdout)
120
+ @stderr.print(result.stderr)
121
+ result.status
122
+ end
123
+
124
+ #: (String) -> Integer
125
+ def printed(text)
126
+ @stdout.puts(text)
127
+ 0
128
+ end
129
+
130
+ #: () -> Controllers::Help
131
+ def help
132
+ Controllers::Help.new(tool: running_tool)
133
+ end
134
+
135
+ # This run of the tool: the executable it was invoked as, and its version, read only when a command needs it.
136
+ #: () -> Controllers::RunningTool
137
+ def running_tool = Controllers::RunningTool.new(executable: @executable, version: -> { running_version })
138
+
139
+ # The version of this tool that is running, as a build stamped it or a checkout's commit gives it.
140
+ #: () -> String
141
+ def running_version = Data::RunningVersion.new.to_s
142
+
143
+ # A command's --help, from the steps command-steps.json lists for it.
144
+ #: (String) -> String
145
+ def help_for(command)
146
+ help.command(command) || raise(OptionParser::InvalidArgument, "#{command} has no help")
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,188 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require_relative "invocation_options"
6
+ require_relative "after_commit_hook_options"
7
+ require_relative "models/command_steps"
8
+
9
+ module Principles
10
+ # One invocation's words: the command, its positional arguments and its options, refused with an
11
+ # OptionParser error when they do not fit what the command takes. CLI decides what to run from it.
12
+ class CommandLine
13
+ # The sources Claude Code's SessionStart hook names, which list-open-findings --short --session-start takes (#717).
14
+ SESSION_STARTS = %w[startup clear compact].freeze #: Array[String]
15
+
16
+ # argv is parsed at once; positional names each command's positional arguments, in order; executable is the
17
+ # invocation a refusal tells the agent to run --help with (#562).
18
+ #: (Array[String], positional: Hash[String, Array[String]], executable: String) -> void
19
+ def initialize(argv, positional:, executable:)
20
+ @positional = positional
21
+ @executable = executable
22
+ @options = InvocationOptions.new #: InvocationOptions
23
+ @arguments = argv.dup #: Array[String]
24
+ # The words as typed: parsing takes the options out of @arguments, and a value option is checked against
25
+ # the word that followed it (#750).
26
+ @words = argv.dup.freeze #: Array[String]
27
+ parse(argv)
28
+ end
29
+
30
+ # A usage refusal that carries the repair for the command it names, which CLI prints in place of its
31
+ # general advice (#576). It stays an OptionParser::ParseError, so it exits 2 like every usage refusal.
32
+ class UsageRefusal < OptionParser::ParseError
33
+ #: String
34
+ attr_reader :repair
35
+
36
+ #: (String, String) -> void
37
+ def initialize(fault, repair)
38
+ super()
39
+ @fault = fault
40
+ @repair = repair
41
+ end
42
+
43
+ #: () -> String
44
+ def message = @fault
45
+ end
46
+
47
+ # A parsed option's value, or true for a flag that was given.
48
+ #: (String) -> untyped
49
+ def option(name)
50
+ @options.value(name)
51
+ end
52
+
53
+ # The command named first; nil when none is, and refused when this tool has no such command.
54
+ #: () -> String?
55
+ def command
56
+ name = @arguments.first or return nil
57
+ return name if @positional.key?(name)
58
+
59
+ raise OptionParser::InvalidArgument, "unknown command #{name}"
60
+ end
61
+
62
+ # Refuses the wrong number of arguments, an option the command does not take or given an option as its value,
63
+ # and an answer command without --file.
64
+ #: () -> void
65
+ def check
66
+ name = command.to_s
67
+ check_arguments(name)
68
+ check_options(name)
69
+ check_values_are_not_options(name)
70
+ check_session_start(name)
71
+ check_hook_report(name)
72
+ check_audit_kind(name)
73
+ return unless %w[submit-answer show-answer-check].include?(name) && !@options.value("file")
74
+
75
+ raise OptionParser::MissingArgument, "#{name} needs --file FILE, or --file - for stdin"
76
+ end
77
+
78
+ # The principle id, for a command whose first argument is one.
79
+ #: () -> String?
80
+ def id
81
+ %w[id id?].include?(names.first) ? values.first : nil
82
+ end
83
+
84
+ # The options, with every positional argument but the id under its name.
85
+ #: () -> Hash[String, untyped]
86
+ def command_options
87
+ names.zip(values).each_with_object(@options.to_h) do |(name, value), all|
88
+ all[name] = value unless %w[id id?].include?(name)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ #: () -> Array[String]
95
+ def names
96
+ @positional.fetch(command.to_s)
97
+ end
98
+
99
+ #: () -> Array[String]
100
+ def values
101
+ @arguments.drop(1)
102
+ end
103
+
104
+ #: (String) -> void
105
+ def check_arguments(name)
106
+ return if values.length.between?(names.count { |each| !each.end_with?("?") }, names.length)
107
+
108
+ takes = "#{name} takes #{shown_arguments}".strip.sub(/ takes\z/, " takes no arguments")
109
+ raise OptionParser::InvalidArgument, takes
110
+ end
111
+
112
+ # The positional arguments as usage shows them; a name ending in ? is optional and shown in brackets.
113
+ #: () -> String
114
+ def shown_arguments
115
+ names.map { |each| each.end_with?("?") ? "[#{each.chomp("?").upcase}]" : each.upcase }.join(" ")
116
+ end
117
+
118
+ # An option a command does not take is refused, so a mistyped command never runs as something else.
119
+ #: (String) -> void
120
+ def check_options(name)
121
+ taken = Models::CommandSteps.load.command(name)&.options || []
122
+ unknown = (@options.given - taken - ["project"]).map { |option| "--#{option}" }
123
+ return if unknown.empty?
124
+
125
+ raise UsageRefusal.new("#{name} does not take #{unknown.join(", ")}", help_repair(name))
126
+ end
127
+
128
+ # An option that takes a value takes the next word, whatever it is, so --member --help took --help as the member
129
+ # (#750). Only this tool's own option words are refused there, since a quote may start with dashes; such a word is
130
+ # given as a value after =, as --starting-at=--role is.
131
+ #: (String) -> void
132
+ def check_values_are_not_options(name)
133
+ @words.each_cons(2) do |pair|
134
+ option = pair.fetch(0)
135
+ value = pair.fetch(1)
136
+ next unless InvocationOptions.takes_a_value?(option) && InvocationOptions::WORDS.include?(value)
137
+
138
+ raise UsageRefusal.new("#{option} takes a value, not the option #{value}; to give that word as its value, " \
139
+ "write #{option}=#{value}", help_repair(name))
140
+ end
141
+ end
142
+
143
+ # --session-start names one of the hook's sources, and words only the --short line.
144
+ #: (String) -> void
145
+ def check_session_start(name)
146
+ start = @options.value("session_start") or return
147
+ unless SESSION_STARTS.include?(start)
148
+ raise UsageRefusal.new("--session-start takes startup, clear or compact, not #{start}", help_repair(name))
149
+ end
150
+ return if @options.value("short")
151
+
152
+ raise UsageRefusal.new("--session-start words only the --short line", help_repair(name))
153
+ end
154
+
155
+ # --commits-after and --hook-event ask together for the after-commit hook's JSON, which --short's lines are not.
156
+ #: (String) -> void
157
+ def check_hook_report(name)
158
+ fault = AfterCommitHookOptions.new(@options).fault or return
159
+ raise UsageRefusal.new(fault, help_repair(name))
160
+ end
161
+
162
+ # --member and --write-ups each ask start-audit for a different audit, so only one may be given (#760).
163
+ #: (String) -> void
164
+ def check_audit_kind(name)
165
+ return unless @options.value("member") && @options.value("write_ups")
166
+
167
+ raise UsageRefusal.new("--member and --write-ups each start a different audit; give one", help_repair(name))
168
+ end
169
+
170
+ # Parses argv's options; an option no command defines fails here, before the command is checked, and is
171
+ # repaired with the help of the command argv names, or left to CLI's general advice when it names none.
172
+ #: (Array[String]) -> void
173
+ def parse(argv)
174
+ @options.parse!(@arguments)
175
+ rescue OptionParser::InvalidOption => e
176
+ name = named_command(argv) or raise
177
+ raise UsageRefusal.new(e.message, help_repair(name))
178
+ end
179
+
180
+ # The first word that names a command. The command comes first among the positional words, but an option
181
+ # and its value may come before it; a value that is itself a command's name leads to that command's help.
182
+ #: (Array[String]) -> String?
183
+ def named_command(argv) = argv.find { |word| @positional.key?(word) }
184
+
185
+ #: (String) -> String
186
+ def help_repair(name) = "To see what it takes: #{@executable} #{name} --help"
187
+ end
188
+ end
@@ -0,0 +1,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "controllers/inventory"
5
+ require_relative "controllers/mapping"
6
+ require_relative "controllers/meta_principle_setup"
7
+ require_relative "controllers/member_entries"
8
+ require_relative "controllers/audit"
9
+ require_relative "controllers/answer"
10
+
11
+ module Principles
12
+ # Every command this tool runs, each with its route: the positional words it takes, which CommandLine parses, and
13
+ # the controller that runs it, which CLI hands the parsed line to.
14
+ module CommandRoutes
15
+ # One command's route.
16
+ class Route
17
+ #: Array[String]
18
+ attr_reader :positional
19
+ #: singleton(Controllers::Command)
20
+ attr_reader :controller
21
+
22
+ # positional names each positional argument in order, a trailing ? marking one the command may leave out; the
23
+ # rest are named options.
24
+ #: (singleton(Controllers::Command), Array[String]) -> void
25
+ def initialize(controller, positional)
26
+ @controller = controller
27
+ @positional = positional.freeze #: Array[String]
28
+ freeze
29
+ end
30
+ end
31
+
32
+ ROUTES = {
33
+ "list-candidate-files" => Route.new(Controllers::Inventory, []),
34
+ "list-principles" => Route.new(Controllers::Inventory, []),
35
+ "list-member-problems" => Route.new(Controllers::Inventory, %w[id?]),
36
+ "show-principle" => Route.new(Controllers::Inventory, %w[id]),
37
+ "show-audit" => Route.new(Controllers::Audit, %w[id]),
38
+ "list-open-findings" => Route.new(Controllers::Audit, %w[id?]),
39
+ "show-meta-principle-passage" => Route.new(Controllers::MetaPrincipleSetup, []),
40
+ "create-principle" => Route.new(Controllers::Mapping, %w[id]),
41
+ "change-description" => Route.new(Controllers::Mapping, %w[id]),
42
+ "change-activation" => Route.new(Controllers::Mapping, %w[id]),
43
+ "add-member" => Route.new(Controllers::MemberEntries, %w[id path]),
44
+ "move-member" => Route.new(Controllers::MemberEntries, %w[id member]),
45
+ "change-member-role" => Route.new(Controllers::MemberEntries, %w[id member]),
46
+ "change-member-check" => Route.new(Controllers::MemberEntries, %w[id member]),
47
+ "remove-member-check" => Route.new(Controllers::MemberEntries, %w[id member]),
48
+ "remove-member" => Route.new(Controllers::MemberEntries, %w[id member]),
49
+ "start-audit" => Route.new(Controllers::Audit, %w[id]),
50
+ "submit-answer" => Route.new(Controllers::Answer, %w[id]),
51
+ "show-answer-check" => Route.new(Controllers::Answer, %w[id])
52
+ }.freeze #: Hash[String, Route]
53
+
54
+ # Each command's positional arguments, in order, as CommandLine takes them.
55
+ POSITIONAL = ROUTES.transform_values(&:positional).freeze #: Hash[String, Array[String]]
56
+
57
+ # The controller that runs each command: one per set of commands that change for one reason.
58
+ CONTROLLERS = ROUTES.transform_values(&:controller).freeze #: Hash[String, singleton(Controllers::Command)]
59
+ end
60
+ end
@@ -0,0 +1,106 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require_relative "command"
6
+ require_relative "../models/acknowledgement"
7
+ require_relative "../views/acknowledgement_result"
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
+ # A one-member audit (D33): start-audit ID --member MEMBER asks whether a committed change keeps that member's
14
+ # meaning, and submit-answer moves its baseline in the audit record when the answer holds.
15
+ class Acknowledgement < Command
16
+ # The request for principle id's member.
17
+ #: (String, String) -> Hash[String, untyped]
18
+ def start(id, member_id)
19
+ naming_the_executable(rerun: one_member_audit(id)) do
20
+ acknowledgement, member, base = prepared(id, member_id)
21
+ baseline, lines = since_baseline(id, acknowledgement, member)
22
+ change = Views::MemberChangeSinceBaseline.new(
23
+ member: shown(acknowledgement, member, baseline), member_lines: lines.shown,
24
+ commits: commits(acknowledgement, baseline, base), paths: acknowledgement.paths_since(baseline)
25
+ )
26
+ view.request(request_id: acknowledgement.request_id.to_s, principle_id: id, change: change,
27
+ open_findings: acknowledgement.open_findings).merge("wrote" => [])
28
+ end
29
+ end
30
+
31
+ # The result of answering request_id with response, and the record it changes, not yet written.
32
+ #: (Models::RequestId, untyped) -> [Hash[String, untyped], Array[Models::AuditRecord]]
33
+ def answered(request_id, response)
34
+ id = request_id.principle_id
35
+ acknowledgement, member, = prepared(id, response.fetch("basis").fetch("member_id"))
36
+ stale(id, member.id) unless acknowledgement.request_id.to_s == request_id.to_s
37
+ [view.acknowledged(principle_id: id, member_id: member.id, file: @audit_records.file(id)),
38
+ [accepted(id, acknowledgement, member, response)]]
39
+ end
40
+
41
+ private
42
+
43
+ # The one-member audit of principle id's member, as a refusal naming a near miss leads its repair with (D56).
44
+ #: (String) -> ^(String) -> String
45
+ def one_member_audit(id)
46
+ ->(member) { invocation.in_project("start-audit", id, options: ["--member", member]) }
47
+ end
48
+
49
+ # The record with the acknowledgement in it, once its quotes are found at the baseline and now.
50
+ #: (String, Models::Acknowledgement, Models::Member, untyped) -> Models::AuditRecord
51
+ def accepted(id, acknowledgement, member, response)
52
+ _baseline, lines = since_baseline(id, acknowledgement, member)
53
+ acknowledgement.accepted(response, before: lines.at_baseline, now: lines.now, at: Time.now.utc.iso8601)
54
+ end
55
+
56
+ # The member as the request shows it, with where it was at its baseline when it has moved since (#547).
57
+ #: (Models::Acknowledgement, Models::Member, Models::Placement) -> Hash[String, untyped]
58
+ def shown(acknowledgement, member, baseline)
59
+ moved_from = acknowledgement.moved_from(baseline)
60
+ { "id" => member.id, "role" => member.role, "evidence" => [member.reference],
61
+ **(moved_from ? { "evidence_at_baseline" => [moved_from] } : {}) }
62
+ end
63
+
64
+ # The commits the request names: the baseline's, HEAD, and the audit's base when the baseline is later (#550).
65
+ #: (Models::Acknowledgement, Models::Placement, String?) -> Hash[String, String]
66
+ def commits(acknowledgement, baseline, base)
67
+ audit_base = acknowledgement.audit_base(baseline)
68
+ { "baseline" => baseline.commit, "base" => base.to_s, **(audit_base ? { "audit_base" => audit_base } : {}) }
69
+ end
70
+
71
+ # The member's baseline, the oldest commit from the one it was last judged at that holds the text the audit
72
+ # record vouches for, placed as that commit's map placed it or as it is placed now (#547, #550), and its own
73
+ # lines there and now, which the answer's quotes are checked against (#379).
74
+ #: (String, Models::Acknowledgement, Models::Member) -> [Models::Placement, Models::MemberLines]
75
+ def since_baseline(id, acknowledgement, member)
76
+ placements = map_history(id).placements_since(acknowledgement.last_judged_commit, member.key,
77
+ @member_maps.find(id).member(member.id))
78
+ baseline = acknowledgement.baseline(placements)
79
+ lines = @project.member_lines(member.locator, since: baseline.commit, then_locator: baseline.locator)
80
+ [baseline, acknowledgement.compared(lines)]
81
+ end
82
+
83
+ #: (String, String) -> [Models::Acknowledgement, Models::Member, String?]
84
+ def prepared(id, member_id)
85
+ member = Models::Member.new(@member_maps.find(id).member(member_id))
86
+ base = @history.base_commit(id, [@member_maps.file(id), member.path])
87
+ now = @project.locate(member.locator).fetch("sha256")
88
+ [Models::Acknowledgement.new(principle_id: id, member: member, record: @audit_records.find(id), base: base,
89
+ now_sha256: now), member, base]
90
+ end
91
+
92
+ #: (String, String) -> bot
93
+ def stale(id, member_id)
94
+ raise Error.new("stale_request", "This request no longer matches #{id}: the member, its baseline or the " \
95
+ "commit it was based on has changed since it was printed",
96
+ "Start it again and answer the new request: " \
97
+ "#{invocation.in_project("start-audit", id, options: ["--member", member_id])}")
98
+ end
99
+
100
+ #: () -> Views::AcknowledgementResult
101
+ def view
102
+ Views::AcknowledgementResult.new(invocation)
103
+ end
104
+ end
105
+ end
106
+ end