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,77 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+
6
+ module Principles
7
+ # The options one invocation gives: each value or flag, and which the agent typed, which CommandLine checks
8
+ # against what its command takes.
9
+ class InvocationOptions
10
+ # The options that take a value; --help, -h and --version take none.
11
+ VALUE_OPTIONS = %w[project file description path section starting-at ending-at query role
12
+ check member session-start commits-after hook-event].freeze #: Array[String]
13
+ # The options that take no value, as list-member-problems --short (#291).
14
+ FLAG_OPTIONS = %w[short when-invoked always write-ups].freeze #: Array[String]
15
+ # The options that may be given more than once, each value kept in order, as change-activation --when-files.
16
+ LIST_OPTIONS = %w[when-files].freeze #: Array[String]
17
+ # The words that name an option taking the next word as its value.
18
+ VALUE_WORDS = (VALUE_OPTIONS + LIST_OPTIONS).map { |name| "--#{name}" }.freeze #: Array[String]
19
+ # Every word the parser reads as an option, which an option taking a value refuses as its value (#750).
20
+ WORDS = [*VALUE_WORDS, *FLAG_OPTIONS.map { |name| "--#{name}" }, "--help", "-h",
21
+ "--version"].freeze #: Array[String]
22
+
23
+ # Whether word is the name of an option that takes the next word as its value.
24
+ #: (String) -> bool
25
+ def self.takes_a_value?(word) = VALUE_WORDS.include?(word)
26
+
27
+ #: () -> void
28
+ def initialize
29
+ @values = { "project" => Dir.pwd } #: Hash[String, untyped]
30
+ @given = [] #: Array[String]
31
+ end
32
+
33
+ # Takes the options out of words, leaving the command and its positional arguments; an option no command
34
+ # defines is refused with OptionParser::InvalidOption.
35
+ #: (Array[String]) -> void
36
+ def parse!(words)
37
+ parser.parse!(words)
38
+ end
39
+
40
+ # An option's value, or true for a flag that was given; its name is written with underscores.
41
+ #: (String) -> untyped
42
+ def value(name)
43
+ @values[name]
44
+ end
45
+
46
+ # Each option the agent typed, once, by its name on the command line.
47
+ #: () -> Array[String]
48
+ def given = @given.uniq
49
+
50
+ # Every option's value, under its name with underscores.
51
+ #: () -> Hash[String, untyped]
52
+ def to_h = @values.dup
53
+
54
+ private
55
+
56
+ # Records that the agent typed name, and keeps its value: appended for an option that may repeat, else set.
57
+ #: (String, untyped) -> void
58
+ def store(name, value)
59
+ @given << name
60
+ key = name.tr("-", "_")
61
+ @values[key] = LIST_OPTIONS.include?(name) ? [*@values[key], value] : value
62
+ end
63
+
64
+ # A parser that fills the options, and records each value or flag option the agent typed.
65
+ #: () -> OptionParser
66
+ def parser
67
+ OptionParser.new do |opts|
68
+ (VALUE_OPTIONS + FLAG_OPTIONS + LIST_OPTIONS).each do |name|
69
+ # A flag yields true; a value option yields its value.
70
+ opts.on(FLAG_OPTIONS.include?(name) ? "--#{name}" : "--#{name} VALUE") { |value| store(name, value) }
71
+ end
72
+ opts.on("--help", "-h") { @values["help"] = true }
73
+ opts.on("--version") { @values["version"] = true }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "loading_facts"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # The decision a completed audit accepted on a member's loading, kept in the member's record entry with the
10
+ # member's text hash, as the next full audit shows it beside the member's row (#502 step 5). It changed since when
11
+ # what it weighed changed: when the member loads, or the member's own text. A file's size alone moves with every
12
+ # edit elsewhere in a shared file, so it does not count (second alpha user on 5bedec4, #515).
13
+ class AcceptedLoading
14
+ # entry is the member's record entry, holding loading_accepted and sha256.
15
+ #: (Hash[String, untyped]) -> void
16
+ def initialize(entry)
17
+ decision = entry.fetch("loading_accepted")
18
+ @reason = decision.fetch("reason") #: String
19
+ @loads = decision.fetch("loads") #: Hash[String, untyped]
20
+ @sha256 = entry.fetch("sha256") #: String
21
+ freeze
22
+ end
23
+
24
+ # The reason, and whether what the decision weighed changed since, given the member's loads and hash now; when
25
+ # it did, which of the two changed and the loads accepted, for the agent to see what moved.
26
+ #: (Hash[String, untyped], String?) -> Hash[String, untyped]
27
+ def last_accepted(loads_now, hash_now)
28
+ changed = [("loading" if LoadingFacts.new(@loads) != LoadingFacts.new(loads_now)),
29
+ ("member_text" if @sha256 != hash_now)].compact
30
+ return { "reason" => @reason, "since_accepted" => "unchanged" } if changed.empty?
31
+
32
+ { "reason" => @reason, "since_accepted" => "changed", "changed" => changed, "loads" => @loads }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,203 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "request_id"
6
+ require_relative "audit_record"
7
+ require_relative "member"
8
+ require_relative "placement"
9
+
10
+ module Principles
11
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
12
+ module Models
13
+ # A one-member audit (D33): the agent says a committed change to one member keeps its meaning, quoting a line
14
+ # from before and after it, so the record's baseline for that member moves on and --short stops repeating it.
15
+ # Refused outside git, for a member no completed audit assessed, and for an unchanged or checked member.
16
+ class Acknowledgement
17
+ # One side of the change a quote is checked against: the member's lines then, and when they read.
18
+ class SideOfTheChange
19
+ #: String
20
+ attr_reader :reads_at
21
+
22
+ #: (Array[String], String) -> void
23
+ def initialize(lines, reads_at)
24
+ @lines = lines.map(&:chomp) #: Array[String]
25
+ @reads_at = reads_at
26
+ freeze
27
+ end
28
+
29
+ #: (String) -> bool
30
+ def whole_line?(quote) = @lines.include?(quote)
31
+
32
+ #: (String) -> bool
33
+ def holds?(text) = @lines.any? { |line| line.include?(text) }
34
+ end
35
+
36
+ PHASE = "acknowledgement"
37
+ # The fewest characters a quoted fragment of a line may have, so it shows which edit was read (#352).
38
+ FRAGMENT_LENGTH = 30
39
+
40
+ # before holds the member's lines at its baseline commit, now its lines and hash at base, HEAD.
41
+ #: (principle_id: String, member: Member, record: AuditRecord?, base: String?, now_sha256: String) -> void
42
+ def initialize(principle_id:, member:, record:, base:, now_sha256:)
43
+ @principle_id = principle_id
44
+ @member = member
45
+ @record = record
46
+ @base = base
47
+ @now_sha256 = now_sha256
48
+ freeze
49
+ end
50
+
51
+ # The member's lines at its baseline and now, which the answer quotes; refused when the member cannot be found
52
+ # where its baseline was read, since there is no line there to quote (#459); #baseline reads a moved member
53
+ # where the map then placed it (#547).
54
+ #: (MemberLines?) -> MemberLines
55
+ def compared(lines)
56
+ lines or refuse("it cannot be found in its file at its baseline commit #{last_judged_commit}")
57
+ end
58
+
59
+ # The member's baseline: the oldest of placements, from the commit it was last judged at on, that holds the
60
+ # text the record vouches for, which is later than that commit when its full audit fixed it (#550), and as the
61
+ # map then placed it when it has moved since (#547). Refused when none does, since that text is not the
62
+ # audited one.
63
+ #: (Enumerable[Placement]) -> Placement
64
+ def baseline(placements)
65
+ recorded = acknowledgeable.members.hash_for(@member.key)
66
+ placements.find { |placement| placement.holds?(recorded) } ||
67
+ refuse("its text does not match the audit record at #{last_judged_commit} or any later commit")
68
+ end
69
+
70
+ # Where the member was at baseline, when that is not where it is now, as after a move (#547).
71
+ #: (Placement) -> Hash[String, untyped]?
72
+ def moved_from(baseline)
73
+ baseline.reference unless baseline.reference == @member.reference
74
+ end
75
+
76
+ # The paths the change since baseline spans, which the baseline's placement owns (#551).
77
+ #: (Placement) -> Array[String]
78
+ def paths_since(baseline) = baseline.paths_to(@member.path)
79
+
80
+ # The commit the member was last judged at, when baseline is later than it, as when the member's own full
81
+ # audit fixed it after its base (#550).
82
+ #: (Placement) -> String?
83
+ def audit_base(baseline)
84
+ judged = last_judged_commit
85
+ judged unless baseline.commit == judged
86
+ end
87
+
88
+ # The commit the member was last judged at: its last acknowledgement's, or the audit's base commit. Refused
89
+ # unless the member can be acknowledged now.
90
+ #: () -> String
91
+ def last_judged_commit
92
+ record = acknowledgeable
93
+ record.members.acknowledgements_for(@member.key).last&.fetch("commit") || record.base_commit.to_s
94
+ end
95
+
96
+ # The request's id: the member, its hash at the baseline and now, and the commit it is judged at.
97
+ #: () -> RequestId
98
+ def request_id
99
+ RequestId.for(principle_id: @principle_id, phase: PHASE,
100
+ parts: [@member.key, acknowledgeable.members.hash_for(@member.key), @now_sha256, @base])
101
+ end
102
+
103
+ # The ids of the deferred and the accepted findings the last audit left on this member, which the request
104
+ # and the acknowledgement name (D33), the accepted ones since an edit may bear on whether they still hold
105
+ # (D35 brief, finding 11).
106
+ #: () -> Hash[String, Array[String]]
107
+ def open_findings
108
+ { "deferred_findings" => findings_on_member("deferred"), "accepted_findings" => findings_on_member("accepted") }
109
+ end
110
+
111
+ # The record with this member's baseline moved to its text now, once the answer is checked against the
112
+ # member's lines at the baseline commit and now.
113
+ #: (untyped, before: Array[String], now: Array[String], at: String) -> AuditRecord
114
+ def accepted(answer, before:, now:, at:)
115
+ checked(answer, before, now)
116
+ acknowledgeable.acknowledged(@member.key, { "from_sha256" => acknowledgeable.members.hash_for(@member.key),
117
+ "to_sha256" => @now_sha256, "commit" => @base,
118
+ "reason" => answer.fetch("reason"), "acknowledged_at" => at,
119
+ **open_findings })
120
+ end
121
+
122
+ private
123
+
124
+ # The ids of the findings the last audit left open on this member with action.
125
+ #: (String) -> Array[String]
126
+ def findings_on_member(action)
127
+ acknowledgeable.open_findings
128
+ .select { |finding| finding.fetch("action") == action }
129
+ .select { |finding| finding.fetch("member_ids").any? { |id| on_member?(id) } }
130
+ .map { |finding| finding.fetch("finding_id") }
131
+ end
132
+
133
+ # Whether the record's id names this member, by key, so a member moved since keeps its findings (#547).
134
+ #: (String) -> bool
135
+ def on_member?(id) = acknowledgeable.members.key_of(id) == @member.key
136
+
137
+ # The completed record, once the member can be acknowledged.
138
+ #: () -> AuditRecord
139
+ def acknowledgeable
140
+ refuse("an acknowledgement is judged against committed text, so it needs git") unless @base
141
+ record = @record or no_completed_audit
142
+ refused_for(record.acknowledgement_refusal(@member, now_sha256: @now_sha256))
143
+ record
144
+ end
145
+
146
+ # Raises the refusal the record gives for this member, if any.
147
+ #: (Symbol?) -> void
148
+ def refused_for(reason)
149
+ case reason
150
+ when :not_completed then no_completed_audit
151
+ when :not_assessed then refuse("no completed audit assessed #{@member.id}")
152
+ when :unchanged then nothing_to_acknowledge
153
+ when :checked then refuse("#{@member.id} has a check, and acknowledging a checked member is not built yet")
154
+ when nil then nil
155
+ else raise ArgumentError, "unknown acknowledgement refusal #{reason}"
156
+ end
157
+ end
158
+
159
+ #: (untyped, Array[String], Array[String]) -> void
160
+ def checked(answer, before, now)
161
+ refuse("the answer says the change does not keep #{@member.id}'s meaning") unless answer["meaning_kept"] == true
162
+ reason = answer.fetch("reason").to_s
163
+ refuse("the reason must be one line") if reason.include?("\n")
164
+ then_side = SideOfTheChange.new(before, "before the change")
165
+ now_side = SideOfTheChange.new(now, "at HEAD")
166
+ quoted(answer.fetch("old_line"), then_side, now_side)
167
+ quoted(answer.fetch("new_line"), now_side, then_side)
168
+ end
169
+
170
+ # A quote is a whole line of this side, or a fragment long enough to show the edit that only this side reads.
171
+ #: (String, SideOfTheChange, SideOfTheChange) -> void
172
+ def quoted(line, side, other)
173
+ return if side.whole_line?(line) || (line.length >= FRAGMENT_LENGTH && side.holds?(line) && !other.holds?(line))
174
+
175
+ raise Error.new("quote_not_found", "#{line.inspect} is not a line of #{@member.id} #{side.reads_at}",
176
+ "Quote a whole line of the member as it reads #{side.reads_at}, or at least " \
177
+ "#{FRAGMENT_LENGTH} characters of one that the member #{other.reads_at} does not hold, " \
178
+ "from the change the request names.")
179
+ end
180
+
181
+ #: () -> bot
182
+ def no_completed_audit = refuse("#{@principle_id} has no completed audit, so no baseline has been judged")
183
+
184
+ # A member whose text is the one last judged, as after a pure move, needs nothing: not an acknowledgement, and
185
+ # not the full audit the other refusals send the agent to (#553).
186
+ #: () -> bot
187
+ def nothing_to_acknowledge
188
+ raise Error.new("cannot_acknowledge",
189
+ "#{@member.id}'s text is the one last judged, so there is nothing to acknowledge",
190
+ "Nothing needs doing.")
191
+ .ending_with([Error::RepairCommand.new(purpose: "After a committed edit changes it, acknowledge it",
192
+ command: "start-audit", values: [@principle_id],
193
+ options: ["--member", @member.id])])
194
+ end
195
+
196
+ #: (String) -> bot
197
+ def refuse(why)
198
+ raise Error.new("cannot_acknowledge", "#{@member.id} cannot be acknowledged: #{why}",
199
+ "For a full audit instead: #{Error::EXECUTABLE} start-audit #{@principle_id}")
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # When a principle's work is in hand, as the agent judged it (D38): when files matching its globs are read or
10
+ # changed, when its work is invoked, or always. This tool stores what the agent declared and does not choose it.
11
+ class Activation
12
+ FORMS = "Give exactly one of --when-files GLOB (repeat it for each glob), --when-invoked, or --always."
13
+
14
+ # The activation change-activation's options declare, refused unless they name exactly one form.
15
+ #: (when_files: Array[String]?, when_invoked: bool?, always: bool?) -> Activation
16
+ def self.declared(when_files:, when_invoked:, always:)
17
+ given = { "files" => when_files, "invoked" => when_invoked, "always" => always }.compact
18
+ unless given.length == 1
19
+ raise Error.new("invalid_activation", "change-activation needs one activation, not #{given.length}", FORMS)
20
+ end
21
+
22
+ globs = when_files
23
+ return new({ "when" => given.keys.first }) unless globs
24
+
25
+ new({ "when" => "files", "files" => globs })
26
+ end
27
+
28
+ #: (Hash[String, untyped]) -> void
29
+ def initialize(fields)
30
+ @fields = fields.freeze #: Hash[String, untyped]
31
+ check_globs
32
+ freeze
33
+ end
34
+
35
+ # The activation as the map stores it.
36
+ #: () -> Hash[String, untyped]
37
+ def to_h
38
+ @fields
39
+ end
40
+
41
+ # What a full audit lists against this activation for the agent to weigh: the members that load at every
42
+ # session start under a files activation; none under always, where the work is in hand in every session; and,
43
+ # under invoked, a listing not built yet (#355, #523).
44
+ #: () -> String
45
+ def full_audit_lists
46
+ case @fields.fetch("when")
47
+ when "files" then "session_start_members"
48
+ when "invoked" then "not_built"
49
+ else "none"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ #: () -> void
56
+ def check_globs
57
+ return unless @fields.fetch("files", []).any? { |glob| glob.strip.empty? }
58
+
59
+ raise Error.new("invalid_activation", "an empty glob matches no file",
60
+ "Give each --when-files a glob such as 'spec/**'.")
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "frontmatter_fields"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # When Claude Code loads a project subagent's definition. Its description, which Claude reads to decide when
11
+ # to delegate, loads at session start in the main session; its body is the subagent's own system prompt, loaded
12
+ # in the subagent's context when Claude delegates to it. Claude Code reads .claude/agents recursively, from
13
+ # the start folder up to the repository root, as its subagents documentation says; the #354 experiment saw
14
+ # a project subagent's description listed at start.
15
+ class AgentLoads
16
+ # An agent's definition: a Markdown file at any depth in a .claude/agents folder, at the root or below it.
17
+ AGENT_FILE = %r{\A(?<folder>(?:.*/)?)\.claude/agents/(?:[^/]+/)*[^/]+\.md\z} #: Regexp
18
+
19
+ # text is the agent's whole file, or nil when it cannot be read.
20
+ #: (String, String?) -> void
21
+ def initialize(path, text)
22
+ @path = path
23
+ @text = text
24
+ freeze
25
+ end
26
+
27
+ #: () -> Hash[String, untyped]
28
+ def to_h
29
+ lines = @text&.lines or return { "when" => "not_determined" }
30
+ return not_derived("an agents folder below the project root") unless folder.empty?
31
+
32
+ description = FrontmatterFields.new(path: @path, lines: lines).field_lines("$.description")
33
+ return not_derived("an agent file without a description field") unless description
34
+
35
+ { "when" => "on_delegation", "context" => "subagent", "description" => { "when" => "session_start" } }
36
+ rescue Error => e
37
+ { "when" => "not_determined", "frontmatter_refused" => e.payload.fetch("code") }
38
+ end
39
+
40
+ # The description lines Claude reads at start to decide when to delegate, as written; nil without the field, or
41
+ # when the file cannot be read. The subagents page states no cap on one description.
42
+ #: () -> String?
43
+ def description_text
44
+ lines = @text&.lines or return nil
45
+ FrontmatterFields.new(path: @path, lines: lines).field_lines("$.description")&.join
46
+ rescue Error
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ # The folder whose .claude/agents holds the agent: empty at the project root, else ending in a slash.
53
+ #: () -> String
54
+ def folder
55
+ AGENT_FILE.match(@path)&.[](:folder).to_s
56
+ end
57
+
58
+ #: (String) -> Hash[String, untyped]
59
+ def not_derived(why)
60
+ { "when" => "not_determined", "not_derived" => why }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "locator"
6
+ require_relative "answer_place"
7
+ require_relative "answer_text"
8
+ require_relative "write_up_pointer"
9
+
10
+ module Principles
11
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
12
+ module Models
13
+ # The checks every answer's fields share: a field is a nonempty string or an array, and each piece of
14
+ # evidence names a path that the caller's block locates in the project.
15
+ class AnswerFields
16
+ #: (^(Locator) -> void) -> void
17
+ def initialize(locate)
18
+ @locate = locate
19
+ end
20
+
21
+ #: (untyped, String) -> String
22
+ def text(value, field) = AnswerText.new(value).required(field)
23
+
24
+ # value as text, refused as field at its place in the answer, so one of many entries is found (#787).
25
+ #: (untyped, String, AnswerPlace) -> String
26
+ def text_at(value, field, place)
27
+ text(value, field)
28
+ rescue Error => e
29
+ raise place.refusal(e)
30
+ end
31
+
32
+ #: (untyped, String) -> untyped
33
+ def array(value, field)
34
+ unless value.is_a?(Array)
35
+ raise Error.new("invalid_answer", "#{field} must be an array",
36
+ "Supply #{field} as the question asks.")
37
+ end
38
+
39
+ value
40
+ end
41
+
42
+ # Each piece of evidence, located where place says the answer holds it; a refusal locating one names its place.
43
+ #: (untyped, AnswerPlace) -> void
44
+ def evidence(references, place)
45
+ array(references, "evidence").each_with_index { |reference, index| pointer(reference, place.child(index)) }
46
+ end
47
+
48
+ # One pointer into the project, such as where a finding is written up, located at its place in the answer.
49
+ #: (untyped, AnswerPlace) -> void
50
+ def pointer(reference, place)
51
+ text(reference["path"], "evidence.path")
52
+ begin
53
+ @locate.call(Locator.new(reference))
54
+ rescue Error => e
55
+ raise place.refusal(e)
56
+ end
57
+ end
58
+
59
+ # Where a finding is written up, located at its place in the answer, as the value its record keeps (#768).
60
+ #: (untyped, AnswerPlace) -> WriteUpPointer
61
+ def write_up_pointer(reference, place)
62
+ pointer(reference, place)
63
+ WriteUpPointer.new(reference)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+ require_relative "member_file"
6
+
7
+ module Principles
8
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
9
+ module Models
10
+ # A place in an agent's answer: its JSON pointer, and whose part of the answer it is, such as "Finding F1".
11
+ # A refusal about what sits there names both, so the agent can find the one pointer to repair (#702).
12
+ class AnswerPlace
13
+ # What a pointer in an answer may do instead of quoting exactly a passage its file does not hold (#742).
14
+ ELSEWHERE = " Or point it at text the file holds."
15
+
16
+ #: String
17
+ attr_reader :pointer
18
+
19
+ # Whose part of the answer it is, such as "Finding F1".
20
+ #: String
21
+ attr_reader :subject
22
+
23
+ #: (String, String) -> void
24
+ def initialize(pointer, subject)
25
+ @pointer = pointer
26
+ @subject = subject
27
+ freeze
28
+ end
29
+
30
+ # The place of name, a key or an index, inside this one, with the same subject.
31
+ #: (String | Integer) -> AnswerPlace
32
+ def child(name) = AnswerPlace.new("#{@pointer}/#{name}", @subject)
33
+
34
+ # error, a refusal about what sits here, saying whose it is and where, as a malformed answer's fields do (D29).
35
+ #: (Error) -> Error
36
+ def refusal(error)
37
+ payload = error.payload
38
+ message = payload.fetch("message")
39
+ Error.new(payload.fetch("code"), "#{@subject}: #{message}", repair(payload))
40
+ .including(payload.except("message", "repair"))
41
+ .including("fields" => [{ "pointer" => @pointer, "problem" => message }])
42
+ end
43
+
44
+ private
45
+
46
+ #: (Hash[String, untyped]) -> String
47
+ def repair(payload)
48
+ repair = payload.fetch("repair")
49
+ MemberFile::QUOTE_NOT_FOUND.include?(payload.fetch("code")) ? repair + ELSEWHERE : repair
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # A value an agent gave where its answer asks for text, the one owner of whether it wrote any (#796).
10
+ class AnswerText
11
+ #: (untyped) -> void
12
+ def initialize(value)
13
+ @value = value
14
+ freeze
15
+ end
16
+
17
+ # Whether the agent wrote it: a string with more than whitespace in it.
18
+ #: () -> bool
19
+ def written? = @value.is_a?(String) && !@value.strip.empty? ? true : false
20
+
21
+ # Whether the agent wrote it as one line: written, with no line break in it.
22
+ #: () -> bool
23
+ def one_line? = written? && !@value.include?("\n")
24
+
25
+ # The text where field requires it: as written, or refused naming field (#798).
26
+ #: (String) -> String
27
+ def required(field)
28
+ unless written?
29
+ raise Error.new("invalid_answer", "#{field} must be a nonempty string",
30
+ "Supply #{field} as the question asks.")
31
+ end
32
+
33
+ @value
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+
6
+ module Principles
7
+ # The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
8
+ module Models
9
+ # A time as an agent wrote it in an answer, such as a sighting's observed_at, which the record compares with
10
+ # audited_at only in audited_at's own form: a UTC time to the second (#699).
11
+ class AnsweredTime
12
+ #: (untyped) -> void
13
+ def initialize(value)
14
+ @value = value
15
+ freeze
16
+ end
17
+
18
+ # Whether it is a UTC time in audited_at's form. Only a time that reads back as written is: Time.iso8601 takes an
19
+ # offset or a fraction, and rolls 30 February over to March.
20
+ #: () -> bool
21
+ def utc?
22
+ return false unless @value.is_a?(String)
23
+
24
+ Time.iso8601(@value).utc.iso8601 == @value
25
+ rescue ArgumentError
26
+ false
27
+ end
28
+
29
+ # Whether it is written as a date alone, such as 2026-11-01, whether or not a calendar has that date.
30
+ #: () -> bool
31
+ def date_alone?
32
+ return false unless @value.is_a?(String)
33
+
34
+ @value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
35
+ end
36
+
37
+ # The UTC time in audited_at's form that it names exactly, or nil when it names none: a date alone at its
38
+ # midnight, and a time to the second with an offset as the same instant (#801). A fraction, or a date no
39
+ # calendar has, names none.
40
+ #: () -> String?
41
+ def exact_utc
42
+ return midnight if date_alone?
43
+ return unless @value.is_a?(String)
44
+
45
+ time = Time.iso8601(@value)
46
+ time.utc.iso8601 if time.iso8601 == @value
47
+ rescue ArgumentError
48
+ nil
49
+ end
50
+
51
+ # Whether it is later than time, both UTC times in audited_at's form: a sighting is made before the answer that
52
+ # reports it (#699), so a repair never prints one later (#801).
53
+ #: (String) -> bool
54
+ def after?(time) = Time.iso8601(@value) > Time.iso8601(time)
55
+
56
+ private
57
+
58
+ #: () -> String?
59
+ def midnight
60
+ at_midnight = "#{@value}T00:00:00Z"
61
+ at_midnight if AnsweredTime.new(at_midnight).utc?
62
+ end
63
+ end
64
+ end
65
+ end