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,203 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../data/repository"
|
|
6
|
+
require_relative "../data/project_files"
|
|
7
|
+
require_relative "../data/principles_subfolder"
|
|
8
|
+
require_relative "map_members"
|
|
9
|
+
require_relative "member"
|
|
10
|
+
require_relative "member_file"
|
|
11
|
+
require_relative "commits_brought_in"
|
|
12
|
+
|
|
13
|
+
module Principles
|
|
14
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
15
|
+
module Models
|
|
16
|
+
# What an audit needs from the project's git history, read through Data::Repository: the commit an audit
|
|
17
|
+
# is based on, each member as that commit holds it, and what changed since. Outside git there is no
|
|
18
|
+
# history, and an audit has no base commit. A path that is a symbolic link, or lies below one, is read at
|
|
19
|
+
# each commit in the file its links lead to, and whether it is committed, and which commits touched it, cover
|
|
20
|
+
# every link on the way, since git holds a link as its target's name (#695). uncommitted_outside alone asks by
|
|
21
|
+
# the paths given: start-audit asks it only once base_commit found every file they lead to committed.
|
|
22
|
+
class ProjectHistory
|
|
23
|
+
# This tool's folder as git names a folder, now and under its earlier name.
|
|
24
|
+
OWN_FOLDERS = [Data::PrinciplesSubfolder::FOLDER, Data::PrinciplesSubfolder::EARLIER_FOLDER]
|
|
25
|
+
.map { |folder| "#{folder}/" }.freeze #: Array[String]
|
|
26
|
+
|
|
27
|
+
#: (Data::Repository, Data::ProjectFiles) -> void
|
|
28
|
+
def initialize(repository, files)
|
|
29
|
+
@repository = repository
|
|
30
|
+
@files = files
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Which of paths, and of the links and files they lead through, git does not track, which an audit refuses as
|
|
34
|
+
# members until committed (#386, #695). nil outside git, where nothing is checked.
|
|
35
|
+
#: (Array[String]) -> Array[String]?
|
|
36
|
+
def untracked(paths) = (@repository.untracked(through_links(paths)) if @repository.inside?)
|
|
37
|
+
|
|
38
|
+
# Whether git tracks path and every file its links lead to, so a fresh clone finds its text; an ignored file is
|
|
39
|
+
# not tracked either (#896). True outside git, where nothing is checked.
|
|
40
|
+
#: (String) -> bool
|
|
41
|
+
def tracks?(path) = !@repository.inside? || @repository.not_tracked(through_links([path])).empty?
|
|
42
|
+
|
|
43
|
+
# Whether the project is in git, so it has a history at all.
|
|
44
|
+
#: () -> bool
|
|
45
|
+
def inside_git? = @repository.inside?
|
|
46
|
+
|
|
47
|
+
# The project's files, other than paths, that differ from HEAD or are untracked, ignored ones aside; nil
|
|
48
|
+
# outside git or when there are none. A check and an answer's evidence read them as they are now (#409).
|
|
49
|
+
#: (Array[String]) -> Array[String]?
|
|
50
|
+
def uncommitted_outside(paths)
|
|
51
|
+
return unless @repository.inside?
|
|
52
|
+
|
|
53
|
+
outside = @repository.changed_files - paths
|
|
54
|
+
outside unless outside.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Refuses a write-up's path while it, or a file its links lead to, differs from HEAD: the record is committed
|
|
58
|
+
# pointing at it, so a fresh clone would find nothing there (#760). Nothing outside git.
|
|
59
|
+
#: (String) -> void
|
|
60
|
+
def refuse_uncommitted_write_up(path)
|
|
61
|
+
return unless @repository.inside?
|
|
62
|
+
|
|
63
|
+
dirty = uncommitted([path])
|
|
64
|
+
return if dirty.empty?
|
|
65
|
+
|
|
66
|
+
linked = dirty - [path]
|
|
67
|
+
raise Error.new("uncommitted_files",
|
|
68
|
+
"The write-up at #{path} differs from the last commit" +
|
|
69
|
+
(linked.empty? ? "" : ", through #{linked.join(", ")}"),
|
|
70
|
+
"The record will point at the write-up as committed.\n" \
|
|
71
|
+
"Commit the write-up, then submit the same answer again.")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The commit an audit of id is based on: HEAD, refused while any of files, or a file their links lead to,
|
|
75
|
+
# differs from it. nil outside git.
|
|
76
|
+
#: (String, Array[String]) -> String?
|
|
77
|
+
def base_commit(id, files)
|
|
78
|
+
return unless @repository.inside?
|
|
79
|
+
|
|
80
|
+
dirty = uncommitted(files)
|
|
81
|
+
unless dirty.empty?
|
|
82
|
+
raise Error.new("uncommitted_files", "The map or members of #{id} differ from the last commit: " \
|
|
83
|
+
"#{dirty.join(", ")}",
|
|
84
|
+
"An audit assesses the map and members as committed, so its record can name the commit " \
|
|
85
|
+
"it began from.\nCommit them, then start it again: #{Error::EXECUTABLE} start-audit #{id}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@repository.head
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# path's text at commit, or nil when commit does not hold it.
|
|
92
|
+
#: (String, String) -> String?
|
|
93
|
+
def text_at(commit, path)
|
|
94
|
+
@repository.text_at(commit, @files.leads_to(path))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The lines of the member at locator in its file at commit, or nil when commit lacks the file or the
|
|
98
|
+
# locator finds nothing in its text there, as after a move; any other refusal still refuses (#457).
|
|
99
|
+
#: (String, Locator) -> Array[String]?
|
|
100
|
+
def lines_at(commit, locator)
|
|
101
|
+
path = locator.path
|
|
102
|
+
text = text_at(commit, path) or return nil
|
|
103
|
+
MemberFile.for(path: path, text: text).lines_of(locator)
|
|
104
|
+
rescue Error => e
|
|
105
|
+
raise unless MemberFile::PLACEMENT_REFUSALS.include?(e.payload.fetch("code"))
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Each member's id and the hash of its text at commit, or nil when commit lacks a member's file or the
|
|
111
|
+
# member's place in it.
|
|
112
|
+
#: (String, MapMembers) -> Hash[String, String]?
|
|
113
|
+
def hashes_at(commit, members)
|
|
114
|
+
members.to_h do |member|
|
|
115
|
+
path = member.path
|
|
116
|
+
text = text_at(commit, path)
|
|
117
|
+
return nil unless text
|
|
118
|
+
|
|
119
|
+
[member.id, MemberFile.for(path: path, text: text).locate(member.locator).fetch("sha256")]
|
|
120
|
+
end
|
|
121
|
+
rescue Error
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The rename git detects for path, a file absent from the working tree, as {"to", "similarity",
|
|
126
|
+
# "deleted_in"}: the last commit that touched path, or nil while the move is not yet committed. It is
|
|
127
|
+
# git's similarity match, which can pair a deleted file with an unrelated similar one, so the agent checks
|
|
128
|
+
# it (#276). nil outside git, for a file that exists, or when git detects none.
|
|
129
|
+
#: (String) -> Hash[String, untyped]?
|
|
130
|
+
def rename_of(path)
|
|
131
|
+
return unless @repository.inside? && @repository.absent?(path)
|
|
132
|
+
|
|
133
|
+
deleted_in = @repository.text_at("HEAD", path) ? nil : @repository.last_commit(path)
|
|
134
|
+
base = deleted_in ? "#{deleted_in}^" : "HEAD"
|
|
135
|
+
found = @repository.renames_since(base).find { |each| each["from"] == path } or return nil
|
|
136
|
+
found.slice("to", "similarity").merge("deleted_in" => deleted_in)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Each member's key and the hash of its text at commit, leaving out a member that commit cannot find.
|
|
140
|
+
#: (String, Array[Hash[String, untyped]]) -> Hash[String, String]
|
|
141
|
+
def hashes_by_key_at(commit, members)
|
|
142
|
+
members.map { |fields| Member.new(fields) }.each_with_object({}) do |member, found|
|
|
143
|
+
path = member.path
|
|
144
|
+
text = text_at(commit, path) or next
|
|
145
|
+
found[member.key] = MemberFile.for(path: path, text: text).locate(member.locator).fetch("sha256")
|
|
146
|
+
rescue Error
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Which of paths, and of the links and files they lead through, differ from HEAD, or are not yet committed.
|
|
152
|
+
#: (Array[String]) -> Array[String]
|
|
153
|
+
def uncommitted(paths)
|
|
154
|
+
@repository.uncommitted(through_links(paths))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Which of paths name a file in the project now, so a file deleted since an audit cited it is not committed as a
|
|
158
|
+
# deletion (#842).
|
|
159
|
+
#: (Array[String]) -> Array[String]
|
|
160
|
+
def existing(paths) = paths.select { |path| @files.file?(path) }
|
|
161
|
+
|
|
162
|
+
# The commit at HEAD now; nil outside git.
|
|
163
|
+
#: () -> String?
|
|
164
|
+
def head = (@repository.head if @repository.inside?)
|
|
165
|
+
|
|
166
|
+
# The commits after commit that changed path, a link on the way to it or the file they lead to, newest
|
|
167
|
+
# first.
|
|
168
|
+
#: (String, String) -> Array[Hash[String, String]]
|
|
169
|
+
def commits_since(commit, path)
|
|
170
|
+
@repository.commits_since(commit, through_links([path]))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Every commit on HEAD's first-parent line that changed any of paths, newest first; with since, only those
|
|
174
|
+
# after it.
|
|
175
|
+
#: (Array[String], ?since: String?) -> Array[Hash[String, String]]
|
|
176
|
+
def commits_touching(paths, since: nil)
|
|
177
|
+
@repository.commits_touching(through_links(paths), since: since)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The commits after commit, up to HEAD, that one move of HEAD brought in, whatever they changed (D73).
|
|
181
|
+
#: (String) -> CommitsBroughtIn
|
|
182
|
+
def commits_brought_in(commit)
|
|
183
|
+
CommitsBroughtIn.new(@repository.commits_since(commit, []).map { |each| each.fetch("commit") })
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether any commit after commit changed this tool's own folder, under its name now or its earlier one, or one
|
|
187
|
+
# of paths, a link on the way to one or the file they lead to: the only changes that could make a command
|
|
188
|
+
# refuse that did not refuse before (review R8).
|
|
189
|
+
#: (String, Array[String]) -> bool
|
|
190
|
+
def touched_since?(commit, paths)
|
|
191
|
+
!@repository.commits_since(commit, [*OWN_FOLDERS, *through_links(paths)]).empty?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
# paths, each with the links on the way to it and the file they lead to, once each.
|
|
197
|
+
#: (Array[String]) -> Array[String]
|
|
198
|
+
def through_links(paths)
|
|
199
|
+
paths.flat_map { |path| @files.text_paths(path) }.uniq
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "audit_record"
|
|
5
|
+
require_relative "carried_assessments"
|
|
6
|
+
require_relative "loads_to_check"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The principle's audit record as it stands when an assessment answer is judged against it, each part read from
|
|
12
|
+
# that one record: the members whose loads the answer must decide (#355), the assessments it may carry
|
|
13
|
+
# forward (D49), and the command it ran for each member's check (D63).
|
|
14
|
+
class RecordJudgedAgainst
|
|
15
|
+
# record is the principle's stored record, open or completed, nil when it has none.
|
|
16
|
+
#: (loads: LoadsToCheck, carried: CarriedAssessments, record: AuditRecord?) -> void
|
|
17
|
+
def initialize(loads:, carried:, record:)
|
|
18
|
+
@loads = loads
|
|
19
|
+
@carried = carried
|
|
20
|
+
@record = record
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The members whose loading the answer decides, and the decisions the record accepted.
|
|
25
|
+
#: LoadsToCheck
|
|
26
|
+
attr_reader :loads
|
|
27
|
+
|
|
28
|
+
# Which members' last assessments the answer may carry forward.
|
|
29
|
+
#: CarriedAssessments
|
|
30
|
+
attr_reader :carried
|
|
31
|
+
|
|
32
|
+
# The command the stored record was given an exit status for, for the member known by key, as
|
|
33
|
+
# check_since_last_audit reads it: nil when it ran none for that member.
|
|
34
|
+
#: (String) -> String?
|
|
35
|
+
def check_run_for(key) = @record&.check_run_for(key)
|
|
36
|
+
|
|
37
|
+
# The line saying what the review of that run rested on, and since when, as check_since_last_audit shows it.
|
|
38
|
+
#: (String) -> Hash[String, untyped]
|
|
39
|
+
def check_review_for(key) = @record&.check_review_for(key) || {}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "locator"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# One member's entry in a completed audit record, filed under the id the member had then: its hash, any
|
|
10
|
+
# acknowledgements since (D33), and the assessment that audit kept for it, with what it was judged on (D49).
|
|
11
|
+
class RecordedEntry
|
|
12
|
+
#: (String, Hash[String, untyped]) -> void
|
|
13
|
+
def initialize(filed_id, entry)
|
|
14
|
+
@filed_id = filed_id
|
|
15
|
+
@entry = entry
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The assessment the audit kept, with when it was made and what it was judged on; nil when it kept none.
|
|
20
|
+
#: () -> Hash[String, untyped]?
|
|
21
|
+
def assessment = @entry["assessment"]
|
|
22
|
+
|
|
23
|
+
# The path of each file the kept assessment cites as evidence; none when it kept none (#724).
|
|
24
|
+
#: () -> Array[String]
|
|
25
|
+
def cited_paths = (assessment&.fetch("evidence", nil) || []).map { it.fetch("path") }
|
|
26
|
+
|
|
27
|
+
# The kept assessment as the next full audit shows it carried forward, an observed delivery's sighting dated
|
|
28
|
+
# beside it (#680), with whether any evidence it cited changed since, which the block says from each evidence
|
|
29
|
+
# item's hash then (D49).
|
|
30
|
+
#: () { (Locator, String) -> bool } -> Hash[String, untyped]
|
|
31
|
+
def carried_forward(&changed)
|
|
32
|
+
kept = assessment || {}
|
|
33
|
+
evidence = kept.fetch("evidence").zip(kept.fetch("evidence_sha256"))
|
|
34
|
+
kept.slice("assessment", "delivery").merge(sighting(kept), kept.slice("observation", "evidence", "assessed_at"))
|
|
35
|
+
.merge("evidence_changed" => evidence.any? { |each, sha| changed.call(Locator.new(each), sha) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# When the sighting the kept assessment observed was, if observation repeats its observation word for word:
|
|
39
|
+
# its observed_at, or for a record from before sightings were dated, its assessed_at (#680). Nil otherwise.
|
|
40
|
+
#: (String) -> String?
|
|
41
|
+
def repeated_sighting(observation)
|
|
42
|
+
kept = assessment or return nil
|
|
43
|
+
sighted_at(kept) if kept["observation"] == observation
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The id the member had when the audit recorded it, when that differs from id, its id now.
|
|
47
|
+
#: (String) -> String?
|
|
48
|
+
def moved_from(id) = (@filed_id unless @filed_id == id)
|
|
49
|
+
|
|
50
|
+
# Why the member, now id with its text hashing to sha256 and judged on grounds, must be judged again, or nil
|
|
51
|
+
# when its kept assessment still describes it: the same text the audit judged, under the same id, map entry
|
|
52
|
+
# and loads (D49). An acknowledged edit is judged again, unless it was undone.
|
|
53
|
+
#: (id: String, sha256: String?, grounds: Hash[String, String]) -> String?
|
|
54
|
+
def judge_because(id:, sha256:, grounds:)
|
|
55
|
+
return "changed_during_last_audit" if @entry["changed_during_last_audit"]
|
|
56
|
+
|
|
57
|
+
kept = assessment or return "not_assessed_before"
|
|
58
|
+
return text_changed if audited_sha256 != sha256
|
|
59
|
+
return "moved" if moved_from(id)
|
|
60
|
+
return "role_or_check_changed" if kept["entry_sha256"] != grounds["entry_sha256"]
|
|
61
|
+
|
|
62
|
+
"loads_changed" if kept["loads_sha256"] != grounds["loads_sha256"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# When kept's observed delivery was seen, as shown beside it: its observed_at, or for a record from before
|
|
68
|
+
# sightings were dated, its assessed_at, saying so. Empty for a delivery not observed (#680).
|
|
69
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
70
|
+
def sighting(kept)
|
|
71
|
+
return {} unless kept["delivery"] == "observed"
|
|
72
|
+
|
|
73
|
+
{ "observed_at" => sighted_at(kept), "observed_at_from_assessed_at" => (true unless kept["observed_at"]) }
|
|
74
|
+
.compact
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# When the sighting kept was: its observed_at, or its assessed_at in a record from before sightings were dated.
|
|
78
|
+
#: (Hash[String, untyped]) -> String?
|
|
79
|
+
def sighted_at(kept) = kept["observed_at"] || kept["assessed_at"]
|
|
80
|
+
|
|
81
|
+
# The hash of the text the audit judged: the first acknowledgement's starting point when the member has been
|
|
82
|
+
# acknowledged since, since an acknowledgement moves the recorded hash, or else the recorded hash.
|
|
83
|
+
#: () -> String
|
|
84
|
+
def audited_sha256 = acknowledgements.first&.fetch("from_sha256") || @entry.fetch("sha256")
|
|
85
|
+
|
|
86
|
+
#: () -> String
|
|
87
|
+
def text_changed = acknowledgements.empty? ? "text_changed" : "acknowledged_since_last_audit"
|
|
88
|
+
|
|
89
|
+
#: () -> Array[Hash[String, untyped]]
|
|
90
|
+
def acknowledgements = @entry.fetch("acknowledgements", [])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "write_up_pointer"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# One finding as an audit record holds it: its id, what the audit did about it and where it is written up.
|
|
10
|
+
# AuditRecord reads its findings only through this value (#768).
|
|
11
|
+
class RecordedFinding
|
|
12
|
+
# What a finding an assessment raised carries to its record, through the correction's basis: the one list of
|
|
13
|
+
# those fields, so the basis and the record cannot carry different ones.
|
|
14
|
+
RAISED_FIELDS = %w[id member_ids problem evidence].freeze #: Array[String]
|
|
15
|
+
|
|
16
|
+
#: (Hash[String, untyped]) -> void
|
|
17
|
+
def initialize(fields)
|
|
18
|
+
@fields = fields.freeze #: Hash[String, untyped]
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: () -> String
|
|
23
|
+
def id = @fields.fetch("id")
|
|
24
|
+
|
|
25
|
+
# The ids of the members it is on, as its audit recorded them.
|
|
26
|
+
#: () -> Array[String]
|
|
27
|
+
def member_ids = @fields.fetch("member_ids")
|
|
28
|
+
|
|
29
|
+
# What it says is wrong, or nil when the record kept no text for it.
|
|
30
|
+
#: () -> String?
|
|
31
|
+
def problem = @fields["problem"]
|
|
32
|
+
|
|
33
|
+
# What its audit did about it: fixed, deferred or accepted.
|
|
34
|
+
#: () -> String?
|
|
35
|
+
def action = @fields["action"]
|
|
36
|
+
|
|
37
|
+
# Where its write-up is, or nil when it was never written up.
|
|
38
|
+
#: () -> WriteUpPointer?
|
|
39
|
+
def recorded_at
|
|
40
|
+
fields = @fields["recorded_at"]
|
|
41
|
+
WriteUpPointer.new(fields) if fields
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# When it was first raised, when an earlier audit raised it; nil when its own audit did.
|
|
45
|
+
#: () -> String?
|
|
46
|
+
def first_raised_at = @fields["first_raised_at"]
|
|
47
|
+
|
|
48
|
+
# The locators of the evidence it was raised with, or nil when its record keeps none (D64).
|
|
49
|
+
#: () -> Array[Hash[String, untyped]]?
|
|
50
|
+
def evidence = @fields["evidence"]
|
|
51
|
+
|
|
52
|
+
# Whether its record keeps the evidence it was raised with; one written before this tool kept evidence, or kept
|
|
53
|
+
# by a still_holds decision that gave none, keeps none (D64, #856).
|
|
54
|
+
#: () -> bool
|
|
55
|
+
def evidence_kept? = !evidence.nil?
|
|
56
|
+
|
|
57
|
+
# The path of each file it points at: its evidence and its write-up (#724).
|
|
58
|
+
#: () -> Array[String]
|
|
59
|
+
def cited_paths = [*evidence&.map { it.fetch("path") }, recorded_at&.path].compact.uniq
|
|
60
|
+
|
|
61
|
+
# The commit whose record an audit cited for an earlier first_raised_at, when one did (D53).
|
|
62
|
+
#: () -> String?
|
|
63
|
+
def raised_in = @fields["raised_in"]
|
|
64
|
+
|
|
65
|
+
# The one line saying whom it waits on and to decide what, when its deferral says (D52).
|
|
66
|
+
#: () -> String?
|
|
67
|
+
def waits_on = @fields["waits_on"]
|
|
68
|
+
|
|
69
|
+
# Since when it has waited on that, when an earlier audit asked it; nil when its own audit did.
|
|
70
|
+
#: () -> String?
|
|
71
|
+
def waits_on_since = @fields["waits_on_since"]
|
|
72
|
+
|
|
73
|
+
# Whether its audit left it open, deferred or accepted, for the next full audit to decide (D35).
|
|
74
|
+
#: () -> bool
|
|
75
|
+
def open? = %w[deferred accepted].include?(action)
|
|
76
|
+
|
|
77
|
+
# Whether its audit fixed it.
|
|
78
|
+
#: () -> bool
|
|
79
|
+
def fixed? = action == "fixed"
|
|
80
|
+
|
|
81
|
+
# This finding pointed at recorded_at, the move from where it pointed appended to its repointed with when and
|
|
82
|
+
# why (D59), or itself when it already points there, so an identical retry writes nothing (D24).
|
|
83
|
+
#: (WriteUpPointer, at: String, reason: String) -> RecordedFinding
|
|
84
|
+
def moved_to(pointer, at:, reason:)
|
|
85
|
+
return self if recorded_at == pointer
|
|
86
|
+
|
|
87
|
+
moved = { "from" => @fields["recorded_at"], "to" => pointer.to_h, "at" => at, "reason" => reason }
|
|
88
|
+
RecordedFinding.new(@fields.merge("recorded_at" => pointer.to_h,
|
|
89
|
+
"repointed" => [*@fields["repointed"], moved]))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The finding as its record's file holds it.
|
|
93
|
+
#: () -> Hash[String, untyped]
|
|
94
|
+
def to_h = @fields
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "open_finding"
|
|
5
|
+
require_relative "recorded_finding"
|
|
6
|
+
require_relative "recorded_members"
|
|
7
|
+
require_relative "write_up_repoint"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# The findings an audit record's own audit recorded, whatever it did about each, with the principle, audit time
|
|
13
|
+
# and members that audit recorded them under. AuditRecord#left_open decides whose findings are open; these are
|
|
14
|
+
# only its own. Callers reach them through AuditRecord.
|
|
15
|
+
class RecordedFindings
|
|
16
|
+
#: (Array[Hash[String, untyped]], principle_id: String, audited_at: String?, members: RecordedMembers) -> void
|
|
17
|
+
def initialize(entries, principle_id:, audited_at:, members:)
|
|
18
|
+
@findings = entries.map { |finding| RecordedFinding.new(finding) } #: Array[RecordedFinding]
|
|
19
|
+
@principle_id = principle_id
|
|
20
|
+
@audited_at = audited_at
|
|
21
|
+
@members = members
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The findings as the record's file holds them.
|
|
26
|
+
#: () -> Array[Hash[String, untyped]]
|
|
27
|
+
def to_a = @findings.map(&:to_h)
|
|
28
|
+
|
|
29
|
+
# Those its audit deferred or accepted, each with the members as it recorded them.
|
|
30
|
+
#: () -> Array[OpenFinding]
|
|
31
|
+
def left_open
|
|
32
|
+
@findings.select(&:open?).map do |finding|
|
|
33
|
+
OpenFinding.new(finding, principle_id: @principle_id, audited_at: @audited_at, members: @members)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The ids of those its audit fixed.
|
|
38
|
+
#: () -> Array[String]
|
|
39
|
+
def fixed_ids = @findings.select(&:fixed?).map(&:id)
|
|
40
|
+
|
|
41
|
+
# These findings with each one a repoint names moved by it, dated at (D59); the last repoint naming one wins.
|
|
42
|
+
#: (Array[WriteUpRepoint], at: String) -> RecordedFindings
|
|
43
|
+
def repointed(repoints, at:)
|
|
44
|
+
by_finding = repoints.to_h { |repoint| [repoint.finding_id, repoint] }
|
|
45
|
+
moved = @findings.map do |finding|
|
|
46
|
+
repoint = by_finding[finding.id]
|
|
47
|
+
repoint ? repoint.applied_to(finding, at: at) : finding
|
|
48
|
+
end
|
|
49
|
+
RecordedFindings.new(moved.map(&:to_h), principle_id: @principle_id, audited_at: @audited_at,
|
|
50
|
+
members: @members)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The path of each file they point at: their evidence and write-ups (#724).
|
|
54
|
+
#: () -> Array[String]
|
|
55
|
+
def cited_paths = @findings.flat_map(&:cited_paths).uniq
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "accepted_loading"
|
|
5
|
+
require_relative "member"
|
|
6
|
+
require_relative "recorded_entry"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The members an audit record assessed: each filed under the id it had then, with its key beside it when the
|
|
12
|
+
# two differ, holding its hash then, the result reported for its check (D30), its accepted loading decision
|
|
13
|
+
# and the acknowledgements made for it since (D33). Callers reach them through AuditRecord#members.
|
|
14
|
+
class RecordedMembers
|
|
15
|
+
#: (Hash[String, untyped]) -> void
|
|
16
|
+
def initialize(entries)
|
|
17
|
+
@entries = entries
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The entries as the record's file holds them.
|
|
22
|
+
#: () -> Hash[String, untyped]
|
|
23
|
+
def to_h = @entries
|
|
24
|
+
|
|
25
|
+
# The entries without when each assessment was made, or when a sighting it gave no date for was dated, which a
|
|
26
|
+
# record made again from the same answer dates anew (#680).
|
|
27
|
+
#: () -> Hash[String, untyped]
|
|
28
|
+
def undated
|
|
29
|
+
@entries.transform_values do |entry|
|
|
30
|
+
kept = entry["assessment"]
|
|
31
|
+
kept ? entry.merge("assessment" => kept.except("assessed_at", "observed_at")) : entry
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The path of each file an assessment kept here cites as evidence, carried forward or made now (#724).
|
|
36
|
+
#: () -> Array[String]
|
|
37
|
+
def cited_paths
|
|
38
|
+
@entries.flat_map { |id, entry| RecordedEntry.new(id, entry).cited_paths }.uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether the text of the member known by key, whose hash is now sha, is the text the record assessed.
|
|
42
|
+
#: (String, String) -> String
|
|
43
|
+
def freshness(key, sha)
|
|
44
|
+
recorded = hash_for(key)
|
|
45
|
+
return "unassessed" unless recorded
|
|
46
|
+
|
|
47
|
+
recorded == sha ? "current" : "changed"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Each hash the record assessed, by the key of the member it belongs to.
|
|
51
|
+
#: () -> Hash[String, String]
|
|
52
|
+
def hashes_by_key
|
|
53
|
+
@entries.to_h { |id, entry| [entry["key"] || id, entry.fetch("sha256")] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The hash the record assessed for the member known by key, or nil when it assessed no such member.
|
|
57
|
+
#: (String) -> String?
|
|
58
|
+
def hash_for(key)
|
|
59
|
+
entry_for(key)&.fetch("sha256")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The check the record's audit was given an exit status for, for the member known by key: nil when the
|
|
63
|
+
# member had no check then, or its check was reported as not run, so no audit has run it (D30, #227).
|
|
64
|
+
#: (String) -> String?
|
|
65
|
+
def check_run_for(key)
|
|
66
|
+
result = entry_for(key)&.fetch("check_result", nil) or return nil
|
|
67
|
+
result.key?("exit_status") ? result.fetch("command") : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# What the review of that run rested on, for the member known by key, as the record keeps it: its review_basis
|
|
71
|
+
# and any review_basis_since, or nothing when the record ran no check for it or kept no line (D63).
|
|
72
|
+
#: (String) -> Hash[String, untyped]
|
|
73
|
+
def check_review_as_kept(key)
|
|
74
|
+
return {} unless check_run_for(key)
|
|
75
|
+
|
|
76
|
+
entry_for(key)&.fetch("check_result")&.slice("review_basis", "review_basis_since") || {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Each member's accepted loading decision, by its id now, found by the key it keeps through moves (#502).
|
|
80
|
+
#: (MapMembers) -> Hash[String, AcceptedLoading]
|
|
81
|
+
def loading_accepted(members)
|
|
82
|
+
members.to_h do |member|
|
|
83
|
+
entry = entry_for(member.key)
|
|
84
|
+
[member.id, entry&.key?("loading_accepted") ? AcceptedLoading.new(entry) : nil]
|
|
85
|
+
end.compact
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The acknowledgements made for the member known by key since the record's audit, oldest first (D33).
|
|
89
|
+
#: (String) -> Array[Hash[String, untyped]]
|
|
90
|
+
def acknowledgements_for(key)
|
|
91
|
+
entry_for(key)&.fetch("acknowledgements", nil) || []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The member acknowledged most recently and when, as [id, acknowledged_at], or nil when none was since the
|
|
95
|
+
# record's audit (#668). The times compare as text, which orders them only because every writer gives UTC in
|
|
96
|
+
# ISO 8601.
|
|
97
|
+
#: () -> [String, String]?
|
|
98
|
+
def latest_acknowledgement
|
|
99
|
+
@entries.flat_map do |id, entry|
|
|
100
|
+
(entry["acknowledgements"] || []).map { |change| [id, change.fetch("acknowledged_at")] }
|
|
101
|
+
end.max_by(&:last)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# These entries with the member known by key moved to change's to_sha256, and change kept beside it (D33).
|
|
105
|
+
#: (String, Hash[String, untyped]) -> RecordedMembers
|
|
106
|
+
def acknowledged(key, change)
|
|
107
|
+
RecordedMembers.new(@entries.to_h do |id, entry|
|
|
108
|
+
next [id, entry] unless (entry["key"] || id) == key
|
|
109
|
+
|
|
110
|
+
[id, entry.merge("sha256" => change.fetch("to_sha256"),
|
|
111
|
+
"acknowledgements" => [*entry["acknowledgements"], change])]
|
|
112
|
+
end)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The key of the member filed under id: its stored key when it had moved, or else id.
|
|
116
|
+
#: (String) -> String
|
|
117
|
+
def key_of(id)
|
|
118
|
+
@entries.dig(id, "key") || id
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The entry of the member known by key, under the id it was filed under, or nil when the record has none.
|
|
122
|
+
#: (String) -> RecordedEntry?
|
|
123
|
+
def recorded(key)
|
|
124
|
+
id, entry = @entries.find { |filed, each| (each["key"] || filed) == key }
|
|
125
|
+
RecordedEntry.new(id, entry) if id && entry
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# These entries with each assessment an answer carried forward, marked {"carried_forward": true}, replaced by
|
|
129
|
+
# the one last kept for the same member, so the record keeps it with its first date (D49).
|
|
130
|
+
#: (RecordedMembers?) -> RecordedMembers
|
|
131
|
+
def carrying(last)
|
|
132
|
+
RecordedMembers.new(@entries.to_h do |id, entry|
|
|
133
|
+
next [id, entry] unless entry.dig("assessment", "carried_forward")
|
|
134
|
+
|
|
135
|
+
[id, entry.merge("assessment" => last&.recorded(entry["key"] || id)&.assessment).compact]
|
|
136
|
+
end)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# The entry for the member known by key: filed under the id it had then, with its key beside it when the
|
|
142
|
+
# two differ.
|
|
143
|
+
#: (String) -> Hash[String, untyped]?
|
|
144
|
+
def entry_for(key)
|
|
145
|
+
@entries.find { |id, entry| (entry["key"] || id) == key }&.last
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|