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,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ module Principles
7
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
8
+ module Data
9
+ # JSON text that an agent or a person wrote, parsed so that an object naming one key twice is refused on
10
+ # every json version. Ruby 3.4's own json keeps the last of two duplicates without a word, so a reviewer
11
+ # reading a diff could see one value while this tool used the other.
12
+ class JsonText
13
+ # An object under construction, which refuses a key it already holds.
14
+ class UniqueKeys
15
+ #: () -> void
16
+ def initialize
17
+ @pairs = {} #: Hash[untyped, untyped]
18
+ end
19
+
20
+ #: (untyped, untyped) -> untyped
21
+ def []=(key, value)
22
+ raise JSON::ParserError, "duplicate key #{key.inspect}" if @pairs.key?(key)
23
+
24
+ @pairs[key] = value
25
+ end
26
+
27
+ #: () -> Hash[untyped, untyped]
28
+ def to_h
29
+ @pairs
30
+ end
31
+ end
32
+
33
+ #: (String) -> void
34
+ def initialize(text)
35
+ @text = text
36
+ freeze
37
+ end
38
+
39
+ # The parsed value, made only of plain hashes and arrays; raises JSON::ParserError on a duplicate key.
40
+ #: () -> untyped
41
+ def parsed
42
+ plain(JSON.parse(@text, object_class: UniqueKeys))
43
+ end
44
+
45
+ private
46
+
47
+ #: (untyped) -> untyped
48
+ def plain(value)
49
+ case value
50
+ when UniqueKeys then value.to_h.transform_values { |each| plain(each) }
51
+ when Array then value.map { |each| plain(each) }
52
+ else value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
6
+ module Data
7
+ # The fields a schema names, kept from data at every depth. This tool's schemas allow fields they do not
8
+ # name, so an older version can read a newer file (D29), but a field a person or agent added must never
9
+ # be printed back as if this tool had said it.
10
+ class KnownFields
11
+ #: (Hash[String, untyped]) -> void
12
+ def initialize(schema)
13
+ @schema = schema
14
+ freeze
15
+ end
16
+
17
+ # data with only the named fields; data is assumed to fit the schema.
18
+ #: (untyped, ?Hash[String, untyped]) -> untyped
19
+ def of(data, schema = @schema)
20
+ case data
21
+ when Hash then named(data, schema)
22
+ when Array then schema["items"] ? data.map { |each| of(each, schema.fetch("items")) } : data
23
+ else data
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # An object's named fields, and every field when the schema gives one schema for all its values.
30
+ #: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
31
+ def named(data, schema)
32
+ properties = schema.fetch("properties", {})
33
+ each_value = schema["additionalProperties"]
34
+ data.each_with_object({}) do |(key, value), kept|
35
+ if properties.key?(key) then kept[key] = of(value, properties.fetch(key))
36
+ elsif each_value.is_a?(Hash) then kept[key] = of(value, each_value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Principles
5
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
6
+ module Data
7
+ # A path in the project, as its symbolic links lead it. A host may load an instruction file through a link, and
8
+ # git holds a link as the name of its target, so the text at path depends on every link on the way, a linked
9
+ # folder's included, and on the file they lead to (#695).
10
+ class LinkedPath
11
+ # The most links followed, as an operating system caps them, so a cycle ends.
12
+ MOST_LINKS = 40
13
+
14
+ # One link on the way to a path: where it is, the target it names, and the parts of the path beyond it.
15
+ class Hop
16
+ #: String
17
+ attr_reader :link, :target
18
+
19
+ #: Array[String]
20
+ attr_reader :rest
21
+
22
+ #: (String, String, Array[String]) -> void
23
+ def initialize(link, target, rest)
24
+ @link = link
25
+ @target = target
26
+ @rest = rest
27
+ freeze
28
+ end
29
+ end
30
+
31
+ # read_link gives the target the link at a path relative to root names, as the disk holds it, or nil for a
32
+ # path that is not a link.
33
+ #: (path: String, root: String, read_link: ^(String) -> String?) -> void
34
+ def initialize(path:, root:, read_link:)
35
+ @path = path
36
+ @root = root
37
+ @read_link = read_link
38
+ freeze
39
+ end
40
+
41
+ # The paths, relative to root, that the text at path depends on: each link on the way, in order, then the file
42
+ # they lead to. A path with no link gives itself. So does one whose links leave the project or never end, which
43
+ # reading it refuses.
44
+ #: () -> Array[String]
45
+ def text_paths
46
+ links = [] #: Array[String]
47
+ current = @path #: String
48
+ MOST_LINKS.times do
49
+ hop = first_link(current) or return [*links, current]
50
+ links << hop.link
51
+ followed = followed(hop) or return [@path]
52
+ current = followed
53
+ end
54
+ [@path]
55
+ end
56
+
57
+ # The file the links lead to, the last of text_paths: path itself when it passes through no link.
58
+ #: () -> String
59
+ def leads_to = text_paths.fetch(-1)
60
+
61
+ private
62
+
63
+ # The first link on the way to path; nil when there is none.
64
+ #: (String) -> Hop?
65
+ def first_link(path)
66
+ parts = path.split("/")
67
+ (1..parts.length).each do |count|
68
+ link = parts.take(count).join("/")
69
+ target = @read_link.call(link) or next
70
+ return Hop.new(link, target, parts.drop(count))
71
+ end
72
+ nil
73
+ end
74
+
75
+ # The path the hop's target and the parts beyond it name, relative to root; nil when it leaves the project.
76
+ #: (Hop) -> String?
77
+ def followed(hop)
78
+ target = hop.target
79
+ if target.start_with?("/")
80
+ return unless target.start_with?("#{@root}/")
81
+
82
+ return [target.delete_prefix("#{@root}/"), *hop.rest].join("/")
83
+ end
84
+ normal([*hop.link.split("/")[0...-1], *target.split("/"), *hop.rest])
85
+ end
86
+
87
+ # parts joined, with each "." dropped and each ".." taking away the part before it; nil when one has none to
88
+ # take, so the path would leave the project.
89
+ #: (Array[String]) -> String?
90
+ def normal(parts)
91
+ kept = [] #: Array[String]
92
+ parts.each do |part|
93
+ next if part.empty? || part == "."
94
+ return nil if part == ".." && kept.empty?
95
+
96
+ part == ".." ? kept.pop : kept.push(part)
97
+ end
98
+ kept.join("/")
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,104 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require_relative "../error"
6
+ require_relative "schema_check"
7
+ require_relative "principles_subfolder"
8
+ require_relative "json_text"
9
+
10
+ module Principles
11
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
12
+ module Data
13
+ # The member maps this tool writes from the agent's answers: one JSON file per principle under
14
+ # .principles/maps/, committed in the audited repository. A map holds a description and its members,
15
+ # each a project file, optionally narrowed to one Markdown section, with the role it plays.
16
+ class MapFiles
17
+ # The newest map format this version reads. Models::MemberMap says which member fields need which
18
+ # format, and a spec ties the two together.
19
+ NEWEST_FORMAT = 2
20
+
21
+ #: (PrinciplesSubfolder) -> void
22
+ def initialize(directory)
23
+ @directory = directory
24
+ end
25
+
26
+ #: () -> Array[String]
27
+ def ids
28
+ @directory.ids
29
+ end
30
+
31
+ # Which of the folder and its earlier name hold this subfolder (#536).
32
+ #: () -> Array[String]
33
+ def held_in
34
+ @directory.held_in
35
+ end
36
+
37
+ #: (String) -> bool
38
+ def exist?(id)
39
+ @directory.exist?(id)
40
+ end
41
+
42
+ # The map id, which exists: Models::MemberMaps refuses one that does not, since which command creates it is
43
+ # a decision.
44
+ #: (String) -> Hash[String, untyped]
45
+ def read(id)
46
+ parse(id, @directory.read(id, shown_as: relative(id)))
47
+ end
48
+
49
+ # The map id from text read elsewhere, such as the map's file at commit, checked against its schema.
50
+ #: (String, String, ?commit: String?) -> Hash[String, untyped]
51
+ def parse(id, text, commit: nil)
52
+ map = JsonText.new(text).parsed
53
+ refuse_newer(id, map, commit)
54
+ problems = SchemaCheck.new("member-map").problems(map)
55
+ return map if problems.empty?
56
+
57
+ raise Error.new("invalid_map", "#{where(id, commit)} is not in the shape this tool writes",
58
+ "Repair each field listed, or restore the map from version control.", relative(id),
59
+ fields: problems)
60
+ rescue JSON::ParserError => e
61
+ raise Error.new("unparseable_map", e.message, "Repair this map file or restore it from version control.",
62
+ relative(id))
63
+ end
64
+
65
+ #: (String, Hash[String, untyped]) -> void
66
+ def write(id, map)
67
+ @directory.write(id, "#{JSON.pretty_generate(map)}\n")
68
+ end
69
+
70
+ #: (String) -> String
71
+ def relative(id)
72
+ File.join(PrinciplesSubfolder::FOLDER, "maps", "#{id}.json")
73
+ end
74
+
75
+ # Where map id was kept before the folder took its name now, so history reads it at earlier commits (#536).
76
+ #: (String) -> String
77
+ def earlier_relative(id)
78
+ File.join(PrinciplesSubfolder::EARLIER_FOLDER, "maps", "#{id}.json")
79
+ end
80
+
81
+ private
82
+
83
+ # Refuses a map a newer version wrote, before its schema is checked: its fields may be ones this
84
+ # version cannot read without losing what they say, and the agent must not be told to repair or
85
+ # restore a map that is not broken.
86
+ #: (String, untyped, String?) -> void
87
+ def refuse_newer(id, map, commit)
88
+ format = map.is_a?(Hash) ? map["format"] : nil
89
+ return unless format.is_a?(Integer) && format > NEWEST_FORMAT
90
+
91
+ raise Error.new("newer_format", "#{where(id, commit)} is format #{format}, written by a newer version " \
92
+ "than this one; this one reads up to format #{NEWEST_FORMAT}",
93
+ "Update the installed principles plugin or gem, then run the command again. " \
94
+ "Leave the map as it is: it is not broken.",
95
+ relative(id))
96
+ end
97
+
98
+ #: (String, String?) -> String
99
+ def where(id, commit)
100
+ commit ? "#{relative(id)} at commit #{commit[0, 12]}" : relative(id)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../error"
5
+
6
+ module Principles
7
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
8
+ module Data
9
+ # A principle id as a file name under .principles: lower-kebab-case, so it names one file in its directory
10
+ # and never a path outside it. Every map and audit file is reached through one, whichever command asked.
11
+ class PrincipleId
12
+ FORMAT = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/ #: Regexp
13
+
14
+ #: (String) -> void
15
+ def initialize(id)
16
+ unless id.match?(FORMAT)
17
+ raise Error.new("invalid_id", "#{id.inspect} is not a principle id, which is lower-kebab-case",
18
+ "To see the mapped ids: #{Error::EXECUTABLE} list-principles")
19
+ end
20
+
21
+ @id = id
22
+ freeze
23
+ end
24
+
25
+ # The file this id names in directory.
26
+ #: (String) -> String
27
+ def file_in(directory)
28
+ File.join(directory, "#{@id}.json")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,93 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "tempfile"
6
+ require_relative "../error"
7
+ require_relative "principle_id"
8
+ require_relative "bounded_file"
9
+
10
+ module Principles
11
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
12
+ module Data
13
+ # One of this tool's own directories, .principles/maps or .principles/audits, holding one JSON file per id
14
+ # as bytes; MapFiles and AuditFiles keep the rules for what those bytes say. A cloned project may hold
15
+ # a committed symbolic link at .principles or at the directory, which would send this tool's reads and
16
+ # writes outside the project, so each use refuses one.
17
+ class PrinciplesSubfolder
18
+ # The folder a project keeps its maps and audit records in.
19
+ FOLDER = ".principles"
20
+ # Its name before the rename to principles; a project an older version mapped still holds it (#536).
21
+ EARLIER_FOLDER = ".memeplex"
22
+
23
+ #: (String, String) -> void
24
+ def initialize(root, name)
25
+ @root = root
26
+ @name = name
27
+ freeze
28
+ end
29
+
30
+ # The directory's path, once neither it nor FOLDER is a symbolic link.
31
+ #: () -> String
32
+ def path
33
+ [FOLDER, File.join(FOLDER, @name)].each { |relative| refuse_link(relative) }
34
+ File.join(@root, FOLDER, @name)
35
+ end
36
+
37
+ # Which of FOLDER and EARLIER_FOLDER hold this subfolder, in that order, so a model can refuse a project
38
+ # whose maps or records have not moved. A file an older version left beside no subfolder holds none.
39
+ #: () -> Array[String]
40
+ def held_in
41
+ [FOLDER, EARLIER_FOLDER].select { |folder| File.directory?(File.join(@root, folder, @name)) }
42
+ end
43
+
44
+ # The file id names in this directory.
45
+ #: (String) -> String
46
+ def file(id)
47
+ PrincipleId.new(id).file_in(path)
48
+ end
49
+
50
+ # The ids of the JSON files in this directory, sorted.
51
+ #: () -> Array[String]
52
+ def ids
53
+ Dir.glob(File.join(path, "*.json")).map { |each| File.basename(each, ".json") }.sort
54
+ end
55
+
56
+ #: (String) -> bool
57
+ def exist?(id)
58
+ File.exist?(file(id))
59
+ end
60
+
61
+ # The text of id's file, read within BoundedFile's limit and named as shown_as in a refusal.
62
+ #: (String, shown_as: String) -> String
63
+ def read(id, shown_as:)
64
+ BoundedFile.new(file(id), shown_as: shown_as).read
65
+ end
66
+
67
+ # Writes text as id's file, whole or not at all: it is written beside the file, then renamed over it.
68
+ #: (String, String) -> void
69
+ def write(id, text)
70
+ directory = path
71
+ FileUtils.mkdir_p(directory)
72
+ Tempfile.create([id, ".json"], directory) do |temporary|
73
+ temporary.write(text)
74
+ temporary.flush
75
+ File.rename(temporary.path, file(id))
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ #: (String) -> void
82
+ def refuse_link(relative)
83
+ return unless File.symlink?(File.join(@root, relative))
84
+
85
+ raise Error.new("symlinked_directory",
86
+ "#{relative} is a symbolic link, so a read or write there would leave the " \
87
+ "project",
88
+ "Replace the link with a real directory, then restore its files from version control.",
89
+ relative)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,109 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "open3"
6
+ require_relative "../error"
7
+ require_relative "json_text"
8
+ require_relative "bounded_file"
9
+ require_relative "candidate_files_found"
10
+ require_relative "linked_path"
11
+ module Principles
12
+ # Every file and process this tool reads or writes. data/CLAUDE.md owns the boundary.
13
+ module Data
14
+ # Reads the selected project's files: resolves paths inside it, reads members' text and lists candidates.
15
+ class ProjectFiles
16
+ #: String
17
+ attr_reader :root
18
+
19
+ #: (String) -> void
20
+ def initialize(root)
21
+ @root = File.realpath(root) #: String
22
+ end
23
+
24
+ # A project file's text, refused unless it is a readable UTF-8 file inside the project.
25
+ # Models::MemberFile finds a member's place in it.
26
+ #: (String) -> String
27
+ def read_text(path)
28
+ text = BoundedFile.new(resolve(path), shown_as: path).read
29
+ unless text.valid_encoding?
30
+ raise Error.new("invalid_encoding", "Cannot read UTF-8 text", "Choose a text member.",
31
+ path)
32
+ end
33
+
34
+ text
35
+ rescue SystemCallError => e
36
+ raise Error.new("unreadable_file", e.message, "Restore a readable project-local file, then retry.", path)
37
+ end
38
+
39
+ # The paths the text at path depends on, each symbolic link on the way and then the file they lead to, as
40
+ # LinkedPath gives them; path alone when it passes through no link (#695).
41
+ #: (String) -> Array[String]
42
+ def text_paths(path) = linked(path).text_paths
43
+
44
+ # The file path's links lead to, read in place of path; path itself when it passes through no link (#695).
45
+ #: (String) -> String
46
+ def leads_to(path) = linked(path).leads_to
47
+
48
+ # path as the links on the disk lead it.
49
+ #: (String) -> LinkedPath
50
+ def linked(path)
51
+ LinkedPath.new(path: path, root: @root, read_link: lambda { |relative|
52
+ absolute = File.join(@root, relative)
53
+ File.readlink(absolute) if File.symlink?(absolute)
54
+ })
55
+ end
56
+
57
+ # Whether path names a file inside the project; its contents are not read.
58
+ #: (String) -> bool
59
+ def file?(path)
60
+ absolute = File.expand_path(path, @root)
61
+ absolute.start_with?("#{@root}/") && File.file?(absolute)
62
+ end
63
+
64
+ #: (String) -> String
65
+ def resolve(path)
66
+ absolute = File.expand_path(path, @root)
67
+ check_inside(absolute, path)
68
+ unless File.file?(absolute)
69
+ raise Error.new("not_a_file", "Reference does not name a file", "Select a regular text file.",
70
+ path)
71
+ end
72
+
73
+ absolute
74
+ rescue SystemCallError => e
75
+ raise Error.new("unreadable_file", e.message, "Restore the referenced file or correct its path.", path)
76
+ end
77
+
78
+ # Refuses a path that leaves the project, directly or through a link.
79
+ #: (String, String) -> void
80
+ def check_inside(absolute, path)
81
+ return if absolute.start_with?("#{@root}/") && File.realpath(absolute).start_with?("#{@root}/")
82
+
83
+ raise Error.new("outside_project", "Reference is outside the selected project",
84
+ "Choose a file inside this project; external scopes are not supported in this alpha.", path)
85
+ end
86
+
87
+ # The instruction files hosts load by convention, as CandidateFilesFound lists them.
88
+ #: () -> Hash[String, untyped]
89
+ def inventory = CandidateFilesFound.new(self).to_h
90
+
91
+ #: (String) -> untyped
92
+ def read_json(path)
93
+ parse_json(BoundedFile.new(path).read, path)
94
+ rescue SystemCallError => e
95
+ raise Error.new("unreadable_file", e.message, "Supply a readable JSON file.", path)
96
+ end
97
+
98
+ # JSON from text read elsewhere, such as stdin, named by source in a refusal.
99
+ #: (String, String) -> untyped
100
+ def parse_json(text, source)
101
+ BoundedFile.new(source).refuse_larger(text.bytesize)
102
+ JsonText.new(text).parsed
103
+ rescue JSON::ParserError => e
104
+ raise Error.new("unparseable_json", e.message, "Repair this JSON and retry; no response was accepted.",
105
+ source)
106
+ end
107
+ end
108
+ end
109
+ end