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,46 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The candidate files git ignores, which this tool names but never reads. A folder git ignores can hold many
|
|
8
|
+
# copies of instruction files written there by specs or builds, and listing each buries a file ignored on
|
|
9
|
+
# its own, such as personal settings (#406). So a file under a folder git ignores is counted under the
|
|
10
|
+
# shallowest such folder, and only the rest are listed.
|
|
11
|
+
class IgnoredCandidates
|
|
12
|
+
# paths are the ignored candidates; folders are every folder above one that git ignores, each ending in "/".
|
|
13
|
+
#: (paths: Array[String], folders: Array[String]) -> void
|
|
14
|
+
def initialize(paths:, folders:)
|
|
15
|
+
@paths = paths #: Array[String]
|
|
16
|
+
@folders = folders #: Array[String]
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The inventory's ignored files and ignored folders.
|
|
21
|
+
#: () -> Hash[String, untyped]
|
|
22
|
+
def listing = { "ignored" => on_their_own, "ignored_folders" => by_folder }
|
|
23
|
+
|
|
24
|
+
# Each ignored candidate with no folder above it that git ignores.
|
|
25
|
+
#: () -> Array[String]
|
|
26
|
+
def on_their_own
|
|
27
|
+
@paths.reject { |path| shallowest_folder(path) }.sort
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Each shallowest folder git ignores, and how many ignored candidates it holds.
|
|
31
|
+
#: () -> Array[Hash[String, untyped]]
|
|
32
|
+
def by_folder
|
|
33
|
+
@paths.filter_map { |path| shallowest_folder(path) }.tally.sort.map do |folder, count|
|
|
34
|
+
{ "folder" => folder, "candidates" => count }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
#: (String) -> String?
|
|
41
|
+
def shallowest_folder(path)
|
|
42
|
+
@folders.select { |folder| path.start_with?(folder) }.min_by(&:length)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The digest of what a JSON value says: its objects' keys in sorted order at every depth and its lists in their
|
|
11
|
+
# own, so a formatter or a hand edit that only reorders a file's keys leaves it unchanged (#656). A map's
|
|
12
|
+
# version and an audit record's revision are each one (D51).
|
|
13
|
+
class JsonDigest
|
|
14
|
+
#: (untyped) -> void
|
|
15
|
+
def initialize(value)
|
|
16
|
+
@text = JSON.generate(in_key_order(value)) #: String
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: () -> String
|
|
21
|
+
def to_s = Digest::SHA256.hexdigest(@text)
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
#: (untyped) -> untyped
|
|
26
|
+
def in_key_order(value)
|
|
27
|
+
case value
|
|
28
|
+
when Hash then value.sort.to_h { |key, inner| [key, in_key_order(inner)] }
|
|
29
|
+
when Array then value.map { |inner| in_key_order(inner) }
|
|
30
|
+
else value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "strscan"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
require_relative "json_path_string"
|
|
7
|
+
require_relative "json_path_selectors"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# A query in this tool's own profile of RFC 9535 JSONPath (D32): from $, name selectors in dot or bracket
|
|
13
|
+
# form, wildcards, and filters that keep the children whose relative paths equal strings, joined by &&.
|
|
14
|
+
# A query outside the profile is refused whole, never half-read, so an id always holds standard JSONPath
|
|
15
|
+
# and a later full implementation changes no map. It only evaluates over values already parsed as JSON.
|
|
16
|
+
class JsonPathQuery
|
|
17
|
+
# A member-name shorthand, as RFC 9535 defines it for ASCII.
|
|
18
|
+
SHORTHAND = /[A-Za-z_][A-Za-z0-9_]*/ #: Regexp
|
|
19
|
+
|
|
20
|
+
# The repair this tool gives when it has no exact rewrite to offer.
|
|
21
|
+
GRAMMAR = "Use $ followed by .name, ['name'], .* or [*], and [?@.field=='text' && ...] filters." #: String
|
|
22
|
+
|
|
23
|
+
#: (String) -> void
|
|
24
|
+
def initialize(query)
|
|
25
|
+
@query = query
|
|
26
|
+
@segments = parsed(StringScanner.new(query)) #: Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The nodes this query selects from value, in document order.
|
|
31
|
+
#: (untyped) -> Array[untyped]
|
|
32
|
+
def select(value)
|
|
33
|
+
@segments.reduce([value]) { |nodes, selector| nodes.flat_map { |node| selector.select(node) } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The name of the one top-level member this query selects, or nil when it selects anything else.
|
|
37
|
+
#: () -> String?
|
|
38
|
+
def top_level_name
|
|
39
|
+
only = @segments.first
|
|
40
|
+
only.name if @segments.length == 1 && only.is_a?(JsonPathName)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
#: (StringScanner) -> Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
|
|
46
|
+
def parsed(scanner)
|
|
47
|
+
refuse("it does not start with $") unless scanner.scan("$")
|
|
48
|
+
segments = [] #: Array[(JsonPathName | JsonPathWildcard | JsonPathFilter)]
|
|
49
|
+
segments << segment(scanner) until scanner.eos?
|
|
50
|
+
segments
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
|
|
54
|
+
def segment(scanner)
|
|
55
|
+
refuse("descendant segments (..) are outside the profile") if scanner.check("..")
|
|
56
|
+
return dotted(scanner) if scanner.scan(".")
|
|
57
|
+
return bracketed(scanner) if scanner.scan("[")
|
|
58
|
+
|
|
59
|
+
refuse("it has #{scanner.rest.inspect} where a segment should start")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
|
|
63
|
+
def dotted(scanner)
|
|
64
|
+
return JsonPathWildcard.new if scanner.scan("*")
|
|
65
|
+
|
|
66
|
+
start = scanner.pos - 1
|
|
67
|
+
name = scanner.scan(SHORTHAND) or refuse("a . must be followed by a name or *")
|
|
68
|
+
refuse_hyphenated(scanner, start, name) if scanner.check(/-/)
|
|
69
|
+
JsonPathName.new(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A name with a hyphen cannot follow a dot in RFC 9535, so the repair gives the query with it bracketed.
|
|
73
|
+
#: (StringScanner, Integer, String) -> bot
|
|
74
|
+
def refuse_hyphenated(scanner, start, name)
|
|
75
|
+
name += scanner.scan(/[A-Za-z0-9_-]+/).to_s
|
|
76
|
+
rewritten = "#{@query[0...start]}['#{name}']#{scanner.rest}"
|
|
77
|
+
refuse("a name with a hyphen cannot follow a dot", "Put the name in brackets: write it #{rewritten}.")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#: (StringScanner) -> (JsonPathName | JsonPathWildcard | JsonPathFilter)
|
|
81
|
+
def bracketed(scanner)
|
|
82
|
+
selector = if scanner.scan("*") then JsonPathWildcard.new
|
|
83
|
+
elsif scanner.scan("?") then JsonPathFilter.new(conditions(scanner))
|
|
84
|
+
elsif scanner.check(/['"]/) then JsonPathName.new(JsonPathString.new(@query).read(scanner))
|
|
85
|
+
else refuse("this selector is outside the profile, which takes names, * and ?filters")
|
|
86
|
+
end
|
|
87
|
+
refuse("this selector is outside the profile, or not closed") unless scanner.scan("]")
|
|
88
|
+
selector
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The filter's conditions: @-relative paths each equal to a string, joined by &&.
|
|
92
|
+
#: (StringScanner) -> Array[[Array[String], String]]
|
|
93
|
+
def conditions(scanner)
|
|
94
|
+
found = [condition(scanner)]
|
|
95
|
+
found << condition(scanner) while scanner.scan(/\s*&&\s*/)
|
|
96
|
+
scanner.skip(/\s*/)
|
|
97
|
+
found
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#: (StringScanner) -> [Array[String], String]
|
|
101
|
+
def condition(scanner)
|
|
102
|
+
scanner.skip(/\s*/)
|
|
103
|
+
refuse("a filter must compare an @ path") unless scanner.scan("@")
|
|
104
|
+
path = [] #: Array[String]
|
|
105
|
+
path << relative_name(scanner) while scanner.check(/\.|\[/)
|
|
106
|
+
refuse("a filter compares with == only") unless scanner.scan(/\s*==\s*/)
|
|
107
|
+
refuse("a filter compares with a quoted string only") unless scanner.check(/['"]/)
|
|
108
|
+
[path, JsonPathString.new(@query).read(scanner)]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
#: (StringScanner) -> String
|
|
112
|
+
def relative_name(scanner)
|
|
113
|
+
if scanner.scan(".")
|
|
114
|
+
scanner.scan(SHORTHAND) or refuse("a . in a filter path must be followed by a name")
|
|
115
|
+
else
|
|
116
|
+
scanner.scan("[")
|
|
117
|
+
refuse("a [ in a filter path must hold a quoted name") unless scanner.check(/['"]/)
|
|
118
|
+
name = JsonPathString.new(@query).read(scanner)
|
|
119
|
+
refuse("a [ in a filter path is not closed") unless scanner.scan("]")
|
|
120
|
+
name
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#: (String, ?String) -> bot
|
|
125
|
+
def refuse(why, repair = GRAMMAR)
|
|
126
|
+
raise Error.new("unsupported_query", "#{@query.inspect} is outside the supported JSONPath profile: #{why}",
|
|
127
|
+
repair)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The selector * in a JSONPath query: every member value of an object, or every element of an array.
|
|
8
|
+
class JsonPathWildcard
|
|
9
|
+
#: () -> void
|
|
10
|
+
def initialize
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: (untyped) -> Array[untyped]
|
|
15
|
+
def select(node)
|
|
16
|
+
return node.values if node.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
node.is_a?(Array) ? node : []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A name selector in a JSONPath query: the member of an object with that name, if it has one.
|
|
23
|
+
class JsonPathName
|
|
24
|
+
#: String
|
|
25
|
+
attr_reader :name
|
|
26
|
+
|
|
27
|
+
#: (String) -> void
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: (untyped) -> Array[untyped]
|
|
34
|
+
def select(node)
|
|
35
|
+
node.is_a?(Hash) && node.key?(@name) ? [node.fetch(@name)] : []
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A filter selector in this tool's profile: the children for which every condition holds, each condition an
|
|
40
|
+
# @-relative path of names leading to a string equal to the condition's string.
|
|
41
|
+
class JsonPathFilter
|
|
42
|
+
#: (Array[[Array[String], String]]) -> void
|
|
43
|
+
def initialize(conditions)
|
|
44
|
+
@conditions = conditions
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#: (untyped) -> Array[untyped]
|
|
49
|
+
def select(node)
|
|
50
|
+
JsonPathWildcard.new.select(node).select { |child| holds_for?(child) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
#: (untyped) -> bool
|
|
56
|
+
def holds_for?(child)
|
|
57
|
+
@conditions.all? do |path, expected|
|
|
58
|
+
found = path.reduce(child) { |value, name| value.is_a?(Hash) ? value[name] : nil }
|
|
59
|
+
found.is_a?(String) && found == expected
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "strscan"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# One quoted name or string in a JSONPath query, read as RFC 9535 writes it: single or double quotes, the
|
|
11
|
+
# escapes it defines, and no raw control character. Anything else refuses the whole query.
|
|
12
|
+
class JsonPathString
|
|
13
|
+
# The escapes a quoted name or string may hold, and what each stands for.
|
|
14
|
+
ESCAPES = { "b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t", "/" => "/", "\\" => "\\",
|
|
15
|
+
"'" => "'", "\"" => "\"" }.freeze #: Hash[String, String]
|
|
16
|
+
|
|
17
|
+
# query is the whole query, which a refusal names.
|
|
18
|
+
#: (String) -> void
|
|
19
|
+
def initialize(query)
|
|
20
|
+
@query = query
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The string that starts at scanner, its quotes and escapes read; scanner is left after its closing quote.
|
|
25
|
+
#: (StringScanner) -> String
|
|
26
|
+
def read(scanner)
|
|
27
|
+
string(scanner)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# A quoted string, single or double, with its escapes read.
|
|
33
|
+
#: (StringScanner) -> String
|
|
34
|
+
def string(scanner)
|
|
35
|
+
quote = scanner.getch.to_s
|
|
36
|
+
text = +""
|
|
37
|
+
until scanner.scan(quote)
|
|
38
|
+
refuse("a quoted string is not closed") if scanner.eos?
|
|
39
|
+
text << (scanner.scan("\\") ? escaped(scanner, quote) : unescaped(scanner))
|
|
40
|
+
end
|
|
41
|
+
text
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# One escape after a backslash. A quote may be escaped only inside quotes of its own kind, and a \u escape
|
|
45
|
+
# naming half of a surrogate pair is outside the profile.
|
|
46
|
+
#: (StringScanner, String) -> String
|
|
47
|
+
def escaped(scanner, quote)
|
|
48
|
+
if scanner.scan(/u\h{4}/)
|
|
49
|
+
point = scanner.matched.to_s[1, 4].to_s.hex
|
|
50
|
+
refuse("a \\u escape for a surrogate is outside the profile") if point.between?(0xD800, 0xDFFF)
|
|
51
|
+
return [point].pack("U")
|
|
52
|
+
end
|
|
53
|
+
char = scanner.getch.to_s
|
|
54
|
+
other_quote = quote == "'" ? '"' : "'"
|
|
55
|
+
refuse("a string holds an escape JSONPath does not define") if char == other_quote
|
|
56
|
+
|
|
57
|
+
ESCAPES.fetch(char) { refuse("a string holds an escape JSONPath does not define") }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# One character as written; a control character must be escaped.
|
|
61
|
+
#: (StringScanner) -> String
|
|
62
|
+
def unescaped(scanner)
|
|
63
|
+
char = scanner.getch.to_s
|
|
64
|
+
refuse("a string holds an unescaped control character") if char.ord < 0x20
|
|
65
|
+
|
|
66
|
+
char
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (String) -> bot
|
|
70
|
+
def refuse(why)
|
|
71
|
+
raise Error.new("unsupported_query", "#{@query.inspect} is outside the supported JSONPath profile: #{why}",
|
|
72
|
+
"Quote names with ' or \", escaping only what RFC 9535 defines.")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "first_raised_claim"
|
|
5
|
+
require_relative "map_members"
|
|
6
|
+
require_relative "open_finding"
|
|
7
|
+
require_relative "waits_on"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# A prior finding a still_holds decision kept (D35): the finding as the next record keeps it, and what the decision
|
|
13
|
+
# dropped or replaced of it, which the completed result names so a change not meant can be seen and put back.
|
|
14
|
+
class KeptPriorFinding
|
|
15
|
+
# decision is the checked still_holds decision on finding, the prior finding as its record left it open; members
|
|
16
|
+
# are the map's members now, under whose ids it is recorded.
|
|
17
|
+
#: (Hash[String, untyped], OpenFinding, MapMembers) -> void
|
|
18
|
+
def initialize(decision, finding, members)
|
|
19
|
+
@decision = decision
|
|
20
|
+
@finding = finding
|
|
21
|
+
@record = record(members) #: Hash[String, untyped]
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The finding as the record keeps it, with any problem, action or pointer its decision gave, and any earlier
|
|
26
|
+
# first_raised_at it claimed with the commit it cited (D53); a null pointer drops the one it had. Its evidence is
|
|
27
|
+
# as OpenFinding#evidence_after says, from its own state rather than as the request printed it (D64, D70).
|
|
28
|
+
#: () -> Hash[String, untyped]
|
|
29
|
+
def to_h = @record
|
|
30
|
+
|
|
31
|
+
# The pointer it had that its decision dropped with a null recorded_at, or nil when it dropped none (#374).
|
|
32
|
+
#: () -> Hash[String, untyped]?
|
|
33
|
+
def dropped_write_up
|
|
34
|
+
had = @finding.recorded_at
|
|
35
|
+
return unless had && @decision.key?("recorded_at") && @decision["recorded_at"].nil?
|
|
36
|
+
|
|
37
|
+
{ "finding_id" => @finding.id, "recorded_at" => had.to_h }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What it waited on that its decision dropped, with null or by accepting it, or nil when it dropped nothing (D52).
|
|
41
|
+
#: () -> Hash[String, untyped]?
|
|
42
|
+
def waits_on_dropped
|
|
43
|
+
return if @finding.without_waits_on? || @record.key?("waits_on")
|
|
44
|
+
|
|
45
|
+
{ "finding_id" => @finding.id, **@finding.waiting }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Each kept evidence item its decision gave a new place, as it was and is now (D70).
|
|
49
|
+
#: () -> Array[Hash[String, untyped]]
|
|
50
|
+
def swaps = @finding.replacements(@decision["problem_evidence"]).swaps(@finding.id)
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
#: (MapMembers) -> Hash[String, untyped]
|
|
55
|
+
def record(members)
|
|
56
|
+
kept = @finding.kept_against(members)
|
|
57
|
+
.merge(@decision.slice("problem", "action", "recorded_at", *FirstRaisedClaim::FIELDS),
|
|
58
|
+
{ "evidence" => @finding.evidence_after(@decision["problem_evidence"]) }).compact
|
|
59
|
+
kept.merge(waiting_after(kept))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# What it waits on, and since when, after its decision (D52).
|
|
63
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
64
|
+
def waiting_after(kept)
|
|
65
|
+
WaitsOn.new(@decision.fetch("waits_on", @finding.waits_on), action: kept.fetch("action"))
|
|
66
|
+
.kept_over(@finding.waiting)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "audit_record"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The revision of a principle's last completed audit record when a request was printed, or nil when there was
|
|
11
|
+
# none. An assessment's basis and a correction's carry it, so either answer is refused once that record changed,
|
|
12
|
+
# by another audit completing or a member's acknowledgement, rather than replacing it and what it left open
|
|
13
|
+
# (#646, #653).
|
|
14
|
+
class LastCompletedRevision
|
|
15
|
+
#: (String?) -> void
|
|
16
|
+
def initialize(revision)
|
|
17
|
+
@revision = revision
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Refuses an answer of principle_id that would write changes, when record, the record now, changed since the
|
|
22
|
+
# request was printed. An answer that writes nothing, as an identical retry does, is accepted (D24).
|
|
23
|
+
#: (String, AuditRecord?, Array[untyped]) -> void
|
|
24
|
+
def confirm_writable(principle_id, record, changes)
|
|
25
|
+
confirm_unchanged(principle_id, record) unless changes.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# What the block gives, judging an answer of principle_id; but when the block refuses the answer while record,
|
|
29
|
+
# the record now, changed since the request was printed, the refusal is that the request is stale, so the agent
|
|
30
|
+
# does not repair an answer it must start again (#669). An answer the block refuses is not D24's identical
|
|
31
|
+
# retry, which passed every check, so the stale refusal always wins over it.
|
|
32
|
+
#: [T] (String, AuditRecord?) { () -> T } -> T
|
|
33
|
+
def refusing_stale_first(principle_id, record, &)
|
|
34
|
+
yield
|
|
35
|
+
rescue Error
|
|
36
|
+
confirm_unchanged(principle_id, record)
|
|
37
|
+
raise
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Refuses any answer of principle_id when record, the record now, changed since the request was printed.
|
|
41
|
+
#: (String, AuditRecord?) -> void
|
|
42
|
+
def confirm_unchanged(principle_id, record)
|
|
43
|
+
last = record&.last_completed
|
|
44
|
+
return if @revision == last&.revision
|
|
45
|
+
|
|
46
|
+
raise Error.new("audit_completed_since_request",
|
|
47
|
+
"The record of #{principle_id}'s last audit changed after this request was printed: " \
|
|
48
|
+
"#{what_changed(last)}, and answering it would replace that record and what it left open",
|
|
49
|
+
"To see what the record now leaves open: " \
|
|
50
|
+
"#{Error::EXECUTABLE} list-open-findings #{principle_id}\n" \
|
|
51
|
+
"Then start the audit again and answer the new request: " \
|
|
52
|
+
"#{Error::EXECUTABLE} start-audit #{principle_id}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# What changed the record last: its removal, the newest acknowledgement since its audit, or else the audit.
|
|
58
|
+
# Acknowledgements are dropped when a full audit completes, so any kept is newer than the audit (D33).
|
|
59
|
+
#: (AuditRecord?) -> String
|
|
60
|
+
def what_changed(last)
|
|
61
|
+
return "the record was removed" unless last
|
|
62
|
+
|
|
63
|
+
id, at = last.members.latest_acknowledgement
|
|
64
|
+
return "#{id} was acknowledged at #{at}" if id
|
|
65
|
+
|
|
66
|
+
"an audit completed at #{last.to_h.fetch("audited_at")}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The lines a member spans in its file, first to last, as a LocatedText gives them, and whether the member is
|
|
8
|
+
# its whole file, so two members' spans are compared in one place.
|
|
9
|
+
class LineSpan
|
|
10
|
+
#: Integer
|
|
11
|
+
attr_reader :first_line, :last_line
|
|
12
|
+
|
|
13
|
+
#: (Integer, Integer, ?whole_file: bool) -> void
|
|
14
|
+
def initialize(first_line, last_line, whole_file: false)
|
|
15
|
+
@first_line = first_line
|
|
16
|
+
@last_line = last_line
|
|
17
|
+
@whole_file = whole_file
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Whether the two spans share a line.
|
|
22
|
+
#: (LineSpan) -> bool
|
|
23
|
+
def shares_a_line_with?(other)
|
|
24
|
+
first_line <= other.last_line && other.first_line <= last_line
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whether every line of other is one of this span's lines. Lines are not text: where the two share a first or
|
|
28
|
+
# last line, a passage can start or end partway through it, so this span may not hold all of other's text.
|
|
29
|
+
#: (LineSpan) -> bool
|
|
30
|
+
def holds?(other)
|
|
31
|
+
first_line <= other.first_line && other.last_line <= last_line
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether this span surely holds all of other's text: it holds other's lines and is a whole file, which holds
|
|
35
|
+
# every line's text, where a passage may start or end partway through a line (#756).
|
|
36
|
+
#: (LineSpan) -> bool
|
|
37
|
+
def holds_all_text_of?(other)
|
|
38
|
+
@whole_file && holds?(other)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether the two spans begin and end on the same lines. Equal lines are not equal text: a passage can end
|
|
42
|
+
# within its last line.
|
|
43
|
+
#: (LineSpan) -> bool
|
|
44
|
+
def same_lines_as?(other)
|
|
45
|
+
first_line == other.first_line && last_line == other.last_line
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "member_loads"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# The files a principle's members load, each once however many of its members it holds, since every member's
|
|
10
|
+
# loads gives its whole file's size and summing members counts a shared file once per member (second alpha
|
|
11
|
+
# user on #481, #483). Only session start loads its files together, so only those are totalled; a file that
|
|
12
|
+
# loads on files, on invocation or on delegation loads apart from the rest. A file that cannot be read has no
|
|
13
|
+
# size and is not totalled.
|
|
14
|
+
class LoadedFiles
|
|
15
|
+
# The files members load, each read whole once, as files hold it.
|
|
16
|
+
#: (Data::ProjectFiles, MapMembers, hooks: HookCommands) -> LoadedFiles
|
|
17
|
+
def self.read(files, members, hooks:)
|
|
18
|
+
new(members.paths.to_h { |path| [path, MemberLoads.of_file(files, path, hooks: hooks)] })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# loads holds each file's whole-file MemberLoads by path.
|
|
22
|
+
#: (Hash[String, MemberLoads]) -> void
|
|
23
|
+
def initialize(loads)
|
|
24
|
+
@loads = loads.select { |_path, file| file.carried? } #: Hash[String, MemberLoads]
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# files_loaded and session_start_total, each left out when it would be empty.
|
|
29
|
+
#: () -> Hash[String, untyped]
|
|
30
|
+
def to_h
|
|
31
|
+
return {} if @loads.empty?
|
|
32
|
+
|
|
33
|
+
total = session_start_total
|
|
34
|
+
{ "files_loaded" => @loads.map { |path, file| { "path" => path }.merge(file.claude_code) },
|
|
35
|
+
**(total ? { "session_start_total" => total } : {}) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# The files that load whole at session start and their summed size, beside the descriptions that load at start
|
|
41
|
+
# while their bodies wait; each left out when there is none, and nil when there is neither.
|
|
42
|
+
#: () -> Hash[String, untyped]?
|
|
43
|
+
def session_start_total
|
|
44
|
+
files = summed(@loads.values.select(&:at_session_start?).map(&:size), "files", MemberLoads::FILE_SIZE_FIELDS)
|
|
45
|
+
descriptions = summed(@loads.values.map(&:description_at_start), "count", MemberLoads::DESCRIPTION_SIZE_FIELDS)
|
|
46
|
+
total = files.merge(descriptions.empty? ? {} : { "descriptions" => descriptions })
|
|
47
|
+
total unless total.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# How many of sizes were measured, under counted, and their sum under each of fields; empty when none was.
|
|
51
|
+
#: (Array[Hash[String, Integer]], String, [String, String]) -> Hash[String, Integer]
|
|
52
|
+
def summed(sizes, counted, fields)
|
|
53
|
+
measured = sizes.reject(&:empty?)
|
|
54
|
+
return {} if measured.empty?
|
|
55
|
+
|
|
56
|
+
{ counted => measured.size }.merge(fields.to_h { |field| [field, measured.sum { it.fetch(field) }] })
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "answer_text"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# One decision in an assessment's loading_decisions on a member the audit listed to check against the
|
|
11
|
+
# activation (#355). Whether the member should load as it does is the agent's judgement; this tool checks the
|
|
12
|
+
# decision's shape.
|
|
13
|
+
class LoadingDecision
|
|
14
|
+
# decision is as the answer gave it; index is its place in loading_decisions.
|
|
15
|
+
#: (untyped, Integer) -> void
|
|
16
|
+
def initialize(decision, index)
|
|
17
|
+
@decision = decision.is_a?(Hash) ? decision : {} #: Hash[String, untyped]
|
|
18
|
+
@index = index
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The member this decides, or "" when the decision names none.
|
|
23
|
+
#: () -> String
|
|
24
|
+
def member_id = @decision["member_id"].to_s
|
|
25
|
+
|
|
26
|
+
# What the record keeps of this decision once checked: its reason and the loads it accepted, from its
|
|
27
|
+
# member's row in the listing.
|
|
28
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
29
|
+
def kept(row) = { "reason" => @decision["reason"], "loads" => row.fetch("loads") }
|
|
30
|
+
|
|
31
|
+
# Refuses the decision unless it is accepted with a one-line reason.
|
|
32
|
+
#: () -> void
|
|
33
|
+
def check
|
|
34
|
+
return if @decision["decision"] == "accepted" && AnswerText.new(@decision["reason"]).written?
|
|
35
|
+
|
|
36
|
+
raise Error.new("invalid_loading_decision",
|
|
37
|
+
"loading_decisions[#{@index}] for #{@decision["member_id"]} needs decision accepted with a " \
|
|
38
|
+
"reason",
|
|
39
|
+
"Give decision accepted with a one-line reason why the member loads as it does, or, if the " \
|
|
40
|
+
"activation is what is wrong, change it with change-activation and run start-audit again, " \
|
|
41
|
+
"as the request's loads_to_check_instruction says; a finding decision is not built yet.")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|