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.
- checksums.yaml +7 -0
- data/exe/principles +10 -0
- data/lib/principles/after_commit_hook_options.rb +44 -0
- data/lib/principles/cli.rb +149 -0
- data/lib/principles/command_line.rb +188 -0
- data/lib/principles/command_routes.rb +60 -0
- data/lib/principles/controllers/acknowledgement.rb +106 -0
- data/lib/principles/controllers/answer.rb +172 -0
- data/lib/principles/controllers/audit.rb +203 -0
- data/lib/principles/controllers/audit_completion.rb +113 -0
- data/lib/principles/controllers/command.rb +201 -0
- data/lib/principles/controllers/correction.rb +99 -0
- data/lib/principles/controllers/help.rb +107 -0
- data/lib/principles/controllers/inventory.rb +181 -0
- data/lib/principles/controllers/mapping.rb +56 -0
- data/lib/principles/controllers/member_entries.rb +149 -0
- data/lib/principles/controllers/members_a_commit_changed.rb +43 -0
- data/lib/principles/controllers/members_to_assess.rb +160 -0
- data/lib/principles/controllers/meta_principle_setup.rb +92 -0
- data/lib/principles/controllers/repoint.rb +68 -0
- data/lib/principles/controllers/running_tool.rb +41 -0
- data/lib/principles/data/audit_files.rb +96 -0
- data/lib/principles/data/bounded_file.rb +45 -0
- data/lib/principles/data/candidate_files_found.rb +105 -0
- data/lib/principles/data/command-steps.json +305 -0
- data/lib/principles/data/command_steps_file.rb +50 -0
- data/lib/principles/data/expected_type.rb +71 -0
- data/lib/principles/data/ignored_by_git.rb +54 -0
- data/lib/principles/data/json_text.rb +57 -0
- data/lib/principles/data/known_fields.rb +42 -0
- data/lib/principles/data/linked_path.rb +102 -0
- data/lib/principles/data/map_files.rb +104 -0
- data/lib/principles/data/principle_id.rb +32 -0
- data/lib/principles/data/principles_subfolder.rb +93 -0
- data/lib/principles/data/project_files.rb +109 -0
- data/lib/principles/data/repository.rb +165 -0
- data/lib/principles/data/running_version.rb +44 -0
- data/lib/principles/data/schema_check.rb +139 -0
- data/lib/principles/data/schemas/acknowledgement-answer.schema.json +25 -0
- data/lib/principles/data/schemas/answer-guide.schema.json +99 -0
- data/lib/principles/data/schemas/assessment-answer.schema.json +332 -0
- data/lib/principles/data/schemas/audit-record.schema.json +277 -0
- data/lib/principles/data/schemas/command-steps.schema.json +83 -0
- data/lib/principles/data/schemas/correction-answer-guide.schema.json +68 -0
- data/lib/principles/data/schemas/correction-answer.schema.json +306 -0
- data/lib/principles/data/schemas/member-map.schema.json +39 -0
- data/lib/principles/data/schemas/repointing-answer.schema.json +44 -0
- data/lib/principles/error.rb +102 -0
- data/lib/principles/invocation_options.rb +77 -0
- data/lib/principles/models/accepted_loading.rb +36 -0
- data/lib/principles/models/acknowledgement.rb +203 -0
- data/lib/principles/models/activation.rb +64 -0
- data/lib/principles/models/agent_loads.rb +64 -0
- data/lib/principles/models/answer_fields.rb +67 -0
- data/lib/principles/models/answer_place.rb +53 -0
- data/lib/principles/models/answer_text.rb +37 -0
- data/lib/principles/models/answered_time.rb +65 -0
- data/lib/principles/models/assessment_answer.rb +131 -0
- data/lib/principles/models/assessment_basis.rb +47 -0
- data/lib/principles/models/assessment_report.rb +62 -0
- data/lib/principles/models/audit.rb +200 -0
- data/lib/principles/models/audit_record.rb +249 -0
- data/lib/principles/models/audit_records.rb +103 -0
- data/lib/principles/models/audit_start.rb +96 -0
- data/lib/principles/models/audited_member_lines.rb +45 -0
- data/lib/principles/models/candidate_files.rb +66 -0
- data/lib/principles/models/candidate_inventory.rb +67 -0
- data/lib/principles/models/candidate_listing.rb +32 -0
- data/lib/principles/models/carried_assessments.rb +76 -0
- data/lib/principles/models/check_since_last_audit.rb +61 -0
- data/lib/principles/models/cited_files.rb +39 -0
- data/lib/principles/models/command_steps.rb +270 -0
- data/lib/principles/models/commits_brought_in.rb +27 -0
- data/lib/principles/models/commits_since_base.rb +32 -0
- data/lib/principles/models/correction_answer.rb +66 -0
- data/lib/principles/models/correction_basis.rb +70 -0
- data/lib/principles/models/dated_line.rb +29 -0
- data/lib/principles/models/decided_prior_findings.rb +106 -0
- data/lib/principles/models/declared_map_changes.rb +156 -0
- data/lib/principles/models/disposition.rb +233 -0
- data/lib/principles/models/entry_file.rb +65 -0
- data/lib/principles/models/entry_files.rb +46 -0
- data/lib/principles/models/evidence_item.rb +48 -0
- data/lib/principles/models/evidence_lines_since.rb +57 -0
- data/lib/principles/models/evidence_replacements.rb +99 -0
- data/lib/principles/models/finding_write_ups.rb +108 -0
- data/lib/principles/models/findings_left_open.rb +34 -0
- data/lib/principles/models/first_raised_claim.rb +128 -0
- data/lib/principles/models/frontmatter_fields.rb +128 -0
- data/lib/principles/models/hook_commands.rb +100 -0
- data/lib/principles/models/ignored_candidates.rb +46 -0
- data/lib/principles/models/json_digest.rb +35 -0
- data/lib/principles/models/json_path_query.rb +131 -0
- data/lib/principles/models/json_path_selectors.rb +64 -0
- data/lib/principles/models/json_path_string.rb +76 -0
- data/lib/principles/models/kept_prior_finding.rb +70 -0
- data/lib/principles/models/last_completed_revision.rb +70 -0
- data/lib/principles/models/line_span.rb +49 -0
- data/lib/principles/models/loaded_files.rb +60 -0
- data/lib/principles/models/loading_decision.rb +45 -0
- data/lib/principles/models/loading_facts.rb +45 -0
- data/lib/principles/models/loads_to_check.rb +93 -0
- data/lib/principles/models/locate_refusal.rb +23 -0
- data/lib/principles/models/located_text.rb +43 -0
- data/lib/principles/models/locator.rb +52 -0
- data/lib/principles/models/map_members.rb +171 -0
- data/lib/principles/models/markdown_file.rb +92 -0
- data/lib/principles/models/member.rb +171 -0
- data/lib/principles/models/member_assessments.rb +144 -0
- data/lib/principles/models/member_elsewhere.rb +74 -0
- data/lib/principles/models/member_file.rb +156 -0
- data/lib/principles/models/member_grounds.rb +32 -0
- data/lib/principles/models/member_history.rb +68 -0
- data/lib/principles/models/member_lines.rb +95 -0
- data/lib/principles/models/member_loads.rb +197 -0
- data/lib/principles/models/member_map.rb +219 -0
- data/lib/principles/models/member_map_history.rb +143 -0
- data/lib/principles/models/member_maps.rb +185 -0
- data/lib/principles/models/member_moves.rb +95 -0
- data/lib/principles/models/member_problems.rb +253 -0
- data/lib/principles/models/member_sighting.rb +176 -0
- data/lib/principles/models/member_snapshots.rb +36 -0
- data/lib/principles/models/member_state.rb +35 -0
- data/lib/principles/models/members_elsewhere.rb +46 -0
- data/lib/principles/models/members_sharing_lines.rb +116 -0
- data/lib/principles/models/meta_principle.rb +51 -0
- data/lib/principles/models/missing_files.rb +85 -0
- data/lib/principles/models/misspelt_member_id.rb +45 -0
- data/lib/principles/models/open_finding.rb +221 -0
- data/lib/principles/models/open_findings_elsewhere.rb +45 -0
- data/lib/principles/models/other_principles.rb +47 -0
- data/lib/principles/models/placement.rb +42 -0
- data/lib/principles/models/plain_text_file.rb +32 -0
- data/lib/principles/models/principles_folder_move.rb +59 -0
- data/lib/principles/models/principles_package.rb +53 -0
- data/lib/principles/models/prior_finding_decision.rb +162 -0
- data/lib/principles/models/prior_finding_outcomes.rb +60 -0
- data/lib/principles/models/prior_findings.rb +109 -0
- data/lib/principles/models/problem_report.rb +110 -0
- data/lib/principles/models/project.rb +214 -0
- data/lib/principles/models/project_history.rb +203 -0
- data/lib/principles/models/record_judged_against.rb +42 -0
- data/lib/principles/models/recorded_entry.rb +93 -0
- data/lib/principles/models/recorded_finding.rb +97 -0
- data/lib/principles/models/recorded_findings.rb +58 -0
- data/lib/principles/models/recorded_members.rb +149 -0
- data/lib/principles/models/repoint.rb +112 -0
- data/lib/principles/models/reported_check.rb +130 -0
- data/lib/principles/models/request_id.rb +70 -0
- data/lib/principles/models/runs_from.rb +36 -0
- data/lib/principles/models/skill_loads.rb +123 -0
- data/lib/principles/models/still_holds_restatement.rb +87 -0
- data/lib/principles/models/text_size.rb +26 -0
- data/lib/principles/models/uncommitted_outside_the_map.rb +30 -0
- data/lib/principles/models/waits_on.rb +58 -0
- data/lib/principles/models/write_up_opening.rb +93 -0
- data/lib/principles/models/write_up_pointer.rb +50 -0
- data/lib/principles/models/write_up_repoint.rb +39 -0
- data/lib/principles/prose_list.rb +23 -0
- data/lib/principles/supported_ruby.rb +93 -0
- data/lib/principles/version.rb +11 -0
- data/lib/principles/views/acknowledged_since_last_audit.rb +34 -0
- data/lib/principles/views/acknowledgement_result.rb +44 -0
- data/lib/principles/views/activation_in_hand.rb +47 -0
- data/lib/principles/views/assessment_request.rb +131 -0
- data/lib/principles/views/audit_changes.rb +79 -0
- data/lib/principles/views/audit_commit.rb +55 -0
- data/lib/principles/views/audit_result.rb +147 -0
- data/lib/principles/views/candidate_files_result.rb +95 -0
- data/lib/principles/views/check_result_template.rb +75 -0
- data/lib/principles/views/diff_since_last_audit.rb +29 -0
- data/lib/principles/views/findings_result.rb +45 -0
- data/lib/principles/views/help.rb +119 -0
- data/lib/principles/views/invocation.rb +66 -0
- data/lib/principles/views/landed_since_base.rb +27 -0
- data/lib/principles/views/lines_shared_with_member.rb +111 -0
- data/lib/principles/views/loads_to_check_result.rb +45 -0
- data/lib/principles/views/map_result.rb +169 -0
- data/lib/principles/views/member_change_since_baseline.rb +58 -0
- data/lib/principles/views/member_repairs.rb +82 -0
- data/lib/principles/views/members_a_commit_changed.rb +66 -0
- data/lib/principles/views/members_with_no_baseline.rb +56 -0
- data/lib/principles/views/meta_principle_passage.rb +23 -0
- data/lib/principles/views/meta_principle_result.rb +170 -0
- data/lib/principles/views/open_findings_elsewhere_result.rb +32 -0
- data/lib/principles/views/open_findings_line.rb +125 -0
- data/lib/principles/views/other_principles_unjudged_result.rb +32 -0
- data/lib/principles/views/outside_the_map_result.rb +31 -0
- data/lib/principles/views/package_since_last_audit_result.rb +34 -0
- data/lib/principles/views/principle_read_result.rb +86 -0
- data/lib/principles/views/principle_with_changed_members.rb +26 -0
- data/lib/principles/views/principles_with_no_completed_audit.rb +30 -0
- data/lib/principles/views/printed_principle.rb +45 -0
- data/lib/principles/views/prior_finding_decisions_result.rb +35 -0
- data/lib/principles/views/prior_findings_result.rb +80 -0
- data/lib/principles/views/problem_lines.rb +84 -0
- data/lib/principles/views/problem_result.rb +107 -0
- data/lib/principles/views/prompts/acknowledge-a-change/say-whether-the-change-keeps-the-meaning.md +12 -0
- data/lib/principles/views/prompts/answer-any-judgement-request/what-a-self-audit-cannot-show.md +6 -0
- data/lib/principles/views/prompts/assess-every-member/answer-guide.json +63 -0
- data/lib/principles/views/prompts/assess-every-member/assess-each-member-and-the-set.md +12 -0
- data/lib/principles/views/prompts/assess-every-member/check-roles-other-principles-give-these-lines.md +7 -0
- data/lib/principles/views/prompts/assess-every-member/decide-each-member-loading.md +17 -0
- data/lib/principles/views/prompts/assess-every-member/decide-each-prior-finding.md +38 -0
- data/lib/principles/views/prompts/assess-every-member/give-problem-evidence-at-still-holds.md +2 -0
- data/lib/principles/views/prompts/assess-every-member/give-the-evidence-a-prior-finding-lacks.md +7 -0
- data/lib/principles/views/prompts/assess-every-member/have-a-changed-check-reviewed-before-running-it.md +4 -0
- data/lib/principles/views/prompts/assess-every-member/have-a-new-check-reviewed-before-running-it.md +3 -0
- data/lib/principles/views/prompts/assess-every-member/have-hidden-characters-in-a-check-explained.md +4 -0
- data/lib/principles/views/prompts/assess-every-member/judge-acknowledged-edits-together.md +6 -0
- data/lib/principles/views/prompts/assess-every-member/judge-the-keeping-against-the-other-principles.md +7 -0
- data/lib/principles/views/prompts/assess-every-member/judge-the-keeping-under-the-release-since-the-last-audit.md +10 -0
- data/lib/principles/views/prompts/assess-every-member/judge-the-set-with-carried-assessments.md +10 -0
- data/lib/principles/views/prompts/assess-every-member/keep-the-last-review-basis-unless-you-give-a-new-one.md +1 -0
- data/lib/principles/views/prompts/assess-every-member/name-another-principles-open-finding-rather-than-raise-it.md +9 -0
- data/lib/principles/views/prompts/assess-every-member/read-whether-each-prior-finding-s-evidence-changed.md +12 -0
- data/lib/principles/views/prompts/assess-every-member/run-a-named-check-at-every-full-audit.md +2 -0
- data/lib/principles/views/prompts/assess-every-member/run-the-check-then-report-it.md +2 -0
- data/lib/principles/views/prompts/assess-every-member/run-the-reviewed-check-and-say-what-its-review-rested-on.md +6 -0
- data/lib/principles/views/prompts/assess-every-member/uncommitted-files-outside-the-map.md +4 -0
- data/lib/principles/views/prompts/assess-every-member/why-each-member-is-judged-again.md +24 -0
- data/lib/principles/views/prompts/dispose-of-each-finding/answer-guide.json +34 -0
- data/lib/principles/views/prompts/dispose-of-each-finding/fix-or-record-each-finding.md +24 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/ask-the-owner-before-mapping.md +1 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/choose-one-principle.md +5 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-ignored-folders.md +2 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-ignored.md +3 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-possibly-test-data.md +2 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/owner-ask-untracked.md +2 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/what-each-command-writes.md +4 -0
- data/lib/principles/views/prompts/find-a-principle-to-map/where-the-inventory-looks.md +10 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/add-member.md +45 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/change-activation.md +21 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/change-description.md +2 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/change-member-check.md +17 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/change-member-role.md +9 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/create-principle.md +6 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/every-command-and-how-they-chain.md +28 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/list-candidate-files.md +17 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/list-member-problems.md +73 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/list-open-findings.md +71 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/list-principles.md +7 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/move-member.md +11 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/remove-member-check.md +10 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/remove-member.md +6 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/show-answer-check.md +16 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/show-audit.md +20 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/show-meta-principle-passage.md +52 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/show-principle.md +86 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/start-audit.md +68 -0
- data/lib/principles/views/prompts/learn-what-a-command-does/submit-answer.md +56 -0
- data/lib/principles/views/prompts/repoint-write-ups/point-each-moved-write-up-at-its-new-place.md +10 -0
- data/lib/principles/views/prompts/report-after-a-commit/it-refused-after-the-commits-touched-the-maps.md +2 -0
- data/lib/principles/views/prompts/report-after-a-commit/others-are-still-listed.md +1 -0
- data/lib/principles/views/prompts/report-after-a-commit/the-commits-changed-these-members.md +2 -0
- data/lib/principles/views/prompts/report-after-a-commit/this-command-shows-each-one.md +1 -0
- data/lib/principles/views/prompts/report-the-completed-audit/commit-only-what-the-audit-changed.md +2 -0
- data/lib/principles/views/prompts/report-the-completed-audit/replace-the-default-commit-message.md +4 -0
- data/lib/principles/views/prompts/report-the-completed-audit/what-completion-means.md +2 -0
- data/lib/principles/views/prompts/set-principles-up/entry-file-passage.md +3 -0
- data/lib/principles/views/prompts/set-principles-up/name-the-step-each-member-lacks.md +15 -0
- data/lib/principles/views/prompts/set-principles-up/place-the-passage-with-the-user.md +31 -0
- data/lib/principles/views/prompts/set-principles-up/set-up-first.md +2 -0
- data/lib/principles/views/prompts/set-principles-up/what-the-passage-presumes.md +4 -0
- data/lib/principles/views/prompts/start-a-session/act-on-the-open-findings.md +5 -0
- data/lib/principles/views/prompts/start-a-session/it-returns-after-each-clear-or-compaction-too.md +1 -0
- data/lib/principles/views/prompts/start-a-session/it-returns-at-each-session-start.md +1 -0
- data/lib/principles/views/prompts/start-a-session/raise-them-at-the-next-pause.md +1 -0
- data/lib/principles/views/prompts/start-a-session/raise-them-before-the-first-task.md +1 -0
- data/lib/principles/views/prompts.rb +71 -0
- data/lib/principles/views/repoint_result.rb +80 -0
- data/lib/principles/views/request_part.rb +26 -0
- data/lib/principles/views/request_parts.rb +26 -0
- data/lib/principles/views/shell_word.rb +29 -0
- data/lib/principles/views/visible_text.rb +26 -0
- data/lib/principles/views/workflow_diagram.rb +77 -0
- data/lib/principles.rb +5 -0
- 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
|