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,106 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "member_moves"
|
|
6
|
+
require_relative "recorded_finding"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# What the next full audit decided about each prior finding (D35), once this tool has checked the decisions:
|
|
12
|
+
# the findings that still hold, as the record keeps them; the ones closed, with their reasons; and, for each
|
|
13
|
+
# new finding that raises a prior one again, when that one was first raised. The correction's basis carries
|
|
14
|
+
# these fields, so an audit with findings completes with the same decisions.
|
|
15
|
+
class DecidedPriorFindings
|
|
16
|
+
#: (Hash[String, untyped]) -> void
|
|
17
|
+
def initialize(fields)
|
|
18
|
+
@fields = fields.freeze #: Hash[String, untyped]
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Whether there was no prior finding to decide.
|
|
23
|
+
#: () -> bool
|
|
24
|
+
def none?
|
|
25
|
+
kept.empty? && closed.empty? && @fields.fetch("inherited").empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Each finding that still holds, as the record keeps it.
|
|
29
|
+
#: () -> Array[Hash[String, untyped]]
|
|
30
|
+
def kept
|
|
31
|
+
@fields.fetch("kept")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: () -> Array[String]
|
|
35
|
+
def kept_ids = kept_findings.map(&:id)
|
|
36
|
+
|
|
37
|
+
# Refuses a new finding that reuses the id of a prior finding that still holds, since the record would then hold
|
|
38
|
+
# two findings under one id.
|
|
39
|
+
#: (Array[String]) -> void
|
|
40
|
+
def distinct_from(new_ids)
|
|
41
|
+
return unless kept_ids.intersect?(new_ids)
|
|
42
|
+
|
|
43
|
+
raise Error.new("duplicate_finding", "A new finding reuses the id of a prior finding that still holds",
|
|
44
|
+
"Give new findings ids other than the kept ones: #{kept_ids.join(", ")}.")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Each finding resolved or withdrawn, with its decision and reason.
|
|
48
|
+
#: () -> Array[Hash[String, untyped]]
|
|
49
|
+
def closed
|
|
50
|
+
@fields.fetch("closed")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The ids of the prior findings decided resolved; a withdrawn one was never a gap (#714).
|
|
54
|
+
#: () -> Array[String]
|
|
55
|
+
def resolved_ids = closed.select { |each| each["decision"] == "resolved" }.map { |each| each.fetch("finding_id") }
|
|
56
|
+
|
|
57
|
+
# Each kept finding whose pointer its decision dropped with null, with the pointer it had (#374). A
|
|
58
|
+
# correction basis printed before this was built carries none.
|
|
59
|
+
#: () -> Array[Hash[String, untyped]]
|
|
60
|
+
def dropped
|
|
61
|
+
@fields.fetch("dropped", [])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Each kept finding whose waits_on its decision dropped, with null or by accepting it, with the line and since
|
|
65
|
+
# it had (D52). A correction basis printed before this was built carries none.
|
|
66
|
+
#: () -> Array[Hash[String, untyped]]
|
|
67
|
+
def waits_on_dropped
|
|
68
|
+
@fields.fetch("waits_on_dropped", [])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Each kept evidence item a still_holds decision gave a new place, as it was and is now (D70). A correction
|
|
72
|
+
# basis printed before this was built carries none.
|
|
73
|
+
#: () -> Array[Hash[String, untyped]]
|
|
74
|
+
def evidence_replaced
|
|
75
|
+
@fields.fetch("evidence_replaced", [])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# When the prior finding that the new finding id raises again was first raised, or nothing.
|
|
79
|
+
#: (String) -> Hash[String, String]
|
|
80
|
+
def first_raised(id)
|
|
81
|
+
inherited = @fields.fetch("inherited")
|
|
82
|
+
inherited.key?(id) ? { "first_raised_at" => inherited.fetch(id) } : {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# These decisions with each kept finding filed under its members' ids after moves (D28).
|
|
86
|
+
#: (MemberMoves) -> DecidedPriorFindings
|
|
87
|
+
def moved(moves)
|
|
88
|
+
DecidedPriorFindings.new(@fields.merge("kept" => moves.findings(kept)))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The fields as the correction's basis carries them.
|
|
92
|
+
#: () -> Hash[String, untyped]
|
|
93
|
+
def to_h
|
|
94
|
+
@fields
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
NONE = new({ "kept" => [], "closed" => [], "inherited" => {} }) #: DecidedPriorFindings
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Each finding that still holds, read as the record keeps it.
|
|
102
|
+
#: () -> Array[RecordedFinding]
|
|
103
|
+
def kept_findings = kept.map { |finding| RecordedFinding.new(finding) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "member_map"
|
|
6
|
+
require_relative "member_moves"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain: maps, audits and command steps, with every rule a schema cannot express.
|
|
10
|
+
# models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# The changes a correction declares it made to the map after the audit began: the members it moved (D28),
|
|
13
|
+
# and the members whose role or check a fix changed, which each fixed disposition lists in
|
|
14
|
+
# changed_map_entries (D40). This tool checks them against the map at the base commit, which may also hold
|
|
15
|
+
# members added since, needing no declaration, as a fix maps a member a finding said was missing (T69); whether a
|
|
16
|
+
# change fixes its finding is the agent's judgement.
|
|
17
|
+
class DeclaredMapChanges
|
|
18
|
+
# The changes an answer declares: its moved_members and each disposition's changed_map_entries.
|
|
19
|
+
#: (untyped) -> DeclaredMapChanges
|
|
20
|
+
def self.declared_in(response)
|
|
21
|
+
new(MemberMoves.new(response["moved_members"]), response["dispositions"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#: (MemberMoves, untyped) -> void
|
|
25
|
+
def initialize(moves, dispositions)
|
|
26
|
+
@moves = moves
|
|
27
|
+
@entries = claimed(dispositions) #: Array[String]
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: () -> bool
|
|
32
|
+
def empty?
|
|
33
|
+
@moves.empty? && @entries.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The base commit, to read there the map the audit began from, or nil when nothing is declared and current is
|
|
37
|
+
# still at map_version, the version the audit began from. Outside git, a map that changed is refused, as are
|
|
38
|
+
# declared changes, since there is no base map to check them against.
|
|
39
|
+
#: (String?, MemberMap, map_version: String, map_file: String) -> String?
|
|
40
|
+
def base_to_read(base, current, map_version:, map_file:)
|
|
41
|
+
return check_in_git(base, principle_id: current.id) unless empty?
|
|
42
|
+
return if current.version == map_version
|
|
43
|
+
return base if base
|
|
44
|
+
|
|
45
|
+
raise stale_map(current.id, map_file)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Refuses declared changes outside git, where there is no base map to check them against.
|
|
49
|
+
#: (String?, principle_id: String) -> String
|
|
50
|
+
def check_in_git(base, principle_id:)
|
|
51
|
+
return @moves.check_in_git(base, principle_id: principle_id) unless @moves.empty?
|
|
52
|
+
return base if base
|
|
53
|
+
|
|
54
|
+
raise Error.new("map_entries_need_git",
|
|
55
|
+
"changed_map_entries needs git, to read the map #{principle_id} began from",
|
|
56
|
+
"Outside git, defer the finding, commit nothing, and start a fresh audit: " \
|
|
57
|
+
"#{Error::EXECUTABLE} start-audit #{principle_id}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Refuses unless base is the map the audit began from and current is base with exactly the declared
|
|
61
|
+
# members moved, the claimed members' role and check changed, and any members added (T69).
|
|
62
|
+
#: (MemberMap, MemberMap, map_version: String, map_file: String) -> void
|
|
63
|
+
def check(base, current, map_version:, map_file:)
|
|
64
|
+
raise unexplained(current.id, map_file) unless base.version == map_version
|
|
65
|
+
|
|
66
|
+
expected = @entries.reduce(@moves.applied(base, current)) { |map, id| claimed_entry(map, current, id) }
|
|
67
|
+
raise unexplained(current.id, map_file) unless with_added(expected, current).version == current.version
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Each claimed member's role and check, as was and now, where they differ between the base map with the
|
|
71
|
+
# declared moves applied, which check compares, and current. The block gives [base, current], and is
|
|
72
|
+
# called only when a fix claims an entry, so nothing else reads the base map (#462).
|
|
73
|
+
#: () { () -> [MemberMap, MemberMap] } -> Array[Hash[String, untyped]]
|
|
74
|
+
def entry_changes(&)
|
|
75
|
+
return [] if @entries.empty?
|
|
76
|
+
|
|
77
|
+
base, current = yield
|
|
78
|
+
moved = @moves.applied(base, current)
|
|
79
|
+
@entries.map { |id| changed_fields(id, moved.member(id), current.member(id)) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The map file, when a fix changed a member's role or check in it or the correction added members to it, so the
|
|
83
|
+
# agent commits it too (T69).
|
|
84
|
+
#: (String, ?added: Array[String]) -> Array[String]
|
|
85
|
+
def files_to_commit(map_file, added: [])
|
|
86
|
+
@entries.empty? && added.empty? ? [] : [map_file]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# expected with each member of current whose id it lacks added, as add-member adds it.
|
|
92
|
+
#: (MemberMap, MemberMap) -> MemberMap
|
|
93
|
+
def with_added(expected, current)
|
|
94
|
+
added = current.members.ids - expected.members.ids
|
|
95
|
+
added.reduce(expected) { |map, id| map.adding(current.member(id)) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#: (untyped) -> Array[String]
|
|
99
|
+
def claimed(dispositions)
|
|
100
|
+
Array(dispositions).flat_map { |each| each.is_a?(Hash) ? Array(each["changed_map_entries"]) : [] }.uniq
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The map with the claimed member's role and check as current holds them, refused when they are as
|
|
104
|
+
# they were at the base commit.
|
|
105
|
+
#: (MemberMap, MemberMap, String) -> MemberMap
|
|
106
|
+
def claimed_entry(map, current, id)
|
|
107
|
+
unless current.members.taken?(id)
|
|
108
|
+
raise Error.new("unknown_member", "#{id} is not a member of #{current.id}",
|
|
109
|
+
"Name in changed_map_entries an id that this command lists: " \
|
|
110
|
+
"#{Error::EXECUTABLE} show-principle #{current.id}")
|
|
111
|
+
end
|
|
112
|
+
return replayed(map, id, current.member(id)) unless as_at_base?(map, current, id)
|
|
113
|
+
|
|
114
|
+
raise Error.new("claimed_change_missing", "#{id}'s role and check did not change during this audit",
|
|
115
|
+
"The role and check of #{id} are as they were at the base commit; remove it from " \
|
|
116
|
+
"changed_map_entries.")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The map with the member's role and check changed to now's, as change-member-role, change-member-check
|
|
120
|
+
# and remove-member-check changed them.
|
|
121
|
+
#: (MemberMap, String, Hash[String, untyped]) -> MemberMap
|
|
122
|
+
def replayed(map, id, now)
|
|
123
|
+
roled = map.changing_role(id, now.fetch("role"))
|
|
124
|
+
return roled.changing_check(id, now.fetch("check")) if now.key?("check")
|
|
125
|
+
|
|
126
|
+
roled.member(id).key?("check") ? roled.removing_check(id) : roled
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
#: (String, Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
|
|
130
|
+
def changed_fields(id, was, now)
|
|
131
|
+
changed = %w[role check].reject { |field| was[field] == now[field] }
|
|
132
|
+
{ "member_id" => id, **changed.to_h { |field| [field, { "was" => was[field], "now" => now[field] }] } }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#: (MemberMap, MemberMap, String) -> bool
|
|
136
|
+
def as_at_base?(map, current, id)
|
|
137
|
+
map.member(id).slice("role", "check") == current.member(id).slice("role", "check")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: (String, String) -> Error
|
|
141
|
+
def unexplained(principle_id, map_file)
|
|
142
|
+
@moves.empty? ? stale_map(principle_id, map_file) : @moves.undeclared(principle_id, map_file)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#: (String, String) -> Error
|
|
146
|
+
def stale_map(principle_id, map_file)
|
|
147
|
+
Error.new("stale_map", "The map of #{principle_id} changed during this audit",
|
|
148
|
+
"If you moved a member to fix a finding, declare it in the answer's moved_members as " \
|
|
149
|
+
"{\"from\": OLD_ID, \"to\": NEW_ID}; if a fix changed a member's role or check, list the " \
|
|
150
|
+
"member's id in that fixed disposition's changed_map_entries. In git, a member you added with " \
|
|
151
|
+
"add-member needs no declaration, as long as nothing else changed.\n" \
|
|
152
|
+
"Otherwise, start a fresh audit: #{Error::EXECUTABLE} start-audit #{principle_id}", map_file)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "answer_fields"
|
|
6
|
+
require_relative "answer_place"
|
|
7
|
+
require_relative "answer_text"
|
|
8
|
+
require_relative "waits_on"
|
|
9
|
+
require_relative "write_up_pointer"
|
|
10
|
+
|
|
11
|
+
module Principles
|
|
12
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
13
|
+
module Models
|
|
14
|
+
# One disposition of a correction answer: the action the agent chose for one finding, why, its evidence and
|
|
15
|
+
# where it is written up, and the members a fix claims it changed. The agent's AnswerFields check each field
|
|
16
|
+
# and locate each piece of evidence in the project. Its readers read what the agent gave, so callers ask them
|
|
17
|
+
# only once CorrectionAnswer has checked every disposition, and found none of its fields unfinished, its action
|
|
18
|
+
# among them (#753, #791, #794).
|
|
19
|
+
class Disposition
|
|
20
|
+
ACTIONS = %w[fixed deferred accepted disagreed].freeze #: Array[String]
|
|
21
|
+
|
|
22
|
+
# A field the agent left unfinished: empty, the template's placeholder, the answer guide's sample (#789), or
|
|
23
|
+
# an action not one of the four. It says where it sits in the answer and what to write there, which one
|
|
24
|
+
# refusal lists beside every other (#791).
|
|
25
|
+
class UnfinishedField
|
|
26
|
+
#: String
|
|
27
|
+
attr_reader :repair
|
|
28
|
+
|
|
29
|
+
#: (AnswerPlace, String, problem: String, repair: String) -> void
|
|
30
|
+
def initialize(place, name, problem:, repair:)
|
|
31
|
+
@place = place
|
|
32
|
+
@name = name
|
|
33
|
+
@problem = problem
|
|
34
|
+
@repair = repair
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whose field it is, such as "Finding F1 reason".
|
|
39
|
+
#: () -> String
|
|
40
|
+
def to_s = "#{@place.subject} #{@name}"
|
|
41
|
+
|
|
42
|
+
# Its entry in the refusal's fields, as a malformed answer's are (D29).
|
|
43
|
+
#: () -> Hash[String, String]
|
|
44
|
+
def field = { "pointer" => @place.pointer, "problem" => @problem }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A text field the agent must write: the placeholder the correction template leaves in it, which an answer may
|
|
48
|
+
# not send back unchanged, and what to write instead; reassessment applies only to a fix (#725).
|
|
49
|
+
class TextField
|
|
50
|
+
#: String
|
|
51
|
+
attr_reader :placeholder
|
|
52
|
+
|
|
53
|
+
#: (placeholder: String, repair: String) -> void
|
|
54
|
+
def initialize(placeholder:, repair:)
|
|
55
|
+
@placeholder = placeholder
|
|
56
|
+
@repair = repair
|
|
57
|
+
freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The field named name, at place, when value leaves it empty, as the placeholder (#788, #725) or as sample,
|
|
61
|
+
# the answer guide's illustration of this field for the disposition's action, if it gives one (#789).
|
|
62
|
+
#: (untyped, AnswerPlace, String, sample: String?) -> UnfinishedField?
|
|
63
|
+
def left_in(value, place, name, sample:)
|
|
64
|
+
problem = if !AnswerText.new(value).written? then "must be a nonempty string"
|
|
65
|
+
elsif value == @placeholder then "is the template's placeholder, unchanged"
|
|
66
|
+
elsif value == sample then SAMPLE_UNCHANGED
|
|
67
|
+
end
|
|
68
|
+
UnfinishedField.new(place, name, problem:, repair: @repair) if problem
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The problem with a field the agent gave back as the answer guide's sample (#789).
|
|
73
|
+
SAMPLE_UNCHANGED = "is the answer guide's sample, unchanged" #: String
|
|
74
|
+
|
|
75
|
+
TEXT_FIELDS = {
|
|
76
|
+
"reason" => TextField.new(placeholder: "Why, and what remains",
|
|
77
|
+
repair: "Write why you chose this action and what remains."),
|
|
78
|
+
"reassessment" => TextField.new(placeholder: "When fixed, how the changed members now carry the principle",
|
|
79
|
+
repair: "Write how the changed members now carry the principle.")
|
|
80
|
+
}.freeze #: Hash[String, TextField]
|
|
81
|
+
# The placeholder in each field, which the template prints.
|
|
82
|
+
PLACEHOLDERS = TEXT_FIELDS.transform_values(&:placeholder).freeze #: Hash[String, String]
|
|
83
|
+
|
|
84
|
+
# at is the disposition's place in the answer, such as /dispositions/0, which a refusal about its pointers names.
|
|
85
|
+
# samples is the answer guide's disposition for each action, whose text illustrates each field (#789).
|
|
86
|
+
#: (untyped, AnswerFields, at: String, samples: Hash[String, untyped]) -> void
|
|
87
|
+
def initialize(disposition, fields, at:, samples:)
|
|
88
|
+
@disposition = disposition
|
|
89
|
+
@fields = fields
|
|
90
|
+
@samples = samples
|
|
91
|
+
@place = AnswerPlace.new(at, "Finding #{disposition["finding_id"]}") #: AnswerPlace
|
|
92
|
+
freeze
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#: () -> untyped
|
|
96
|
+
def finding_id = @disposition["finding_id"]
|
|
97
|
+
|
|
98
|
+
# The disposition as the agent gave it.
|
|
99
|
+
#: () -> untyped
|
|
100
|
+
def to_h = @disposition
|
|
101
|
+
|
|
102
|
+
#: () -> String
|
|
103
|
+
def action = @disposition.fetch("action")
|
|
104
|
+
|
|
105
|
+
#: () -> bool
|
|
106
|
+
def fixed? = action == "fixed"
|
|
107
|
+
|
|
108
|
+
# The members a fix says it changed.
|
|
109
|
+
#: () -> Array[String]
|
|
110
|
+
def changed_members = @disposition.fetch("changed_members")
|
|
111
|
+
|
|
112
|
+
# The file where the finding is written up, if anywhere.
|
|
113
|
+
#: () -> String?
|
|
114
|
+
def recorded_path = recorded_at&.path
|
|
115
|
+
|
|
116
|
+
# What the audit record keeps of this disposition beside its finding: the action, where the finding is
|
|
117
|
+
# written up and what a deferral waits on, when given (D52).
|
|
118
|
+
#: () -> Hash[String, untyped]
|
|
119
|
+
def recorded_outcome
|
|
120
|
+
{ "action" => action, "recorded_at" => recorded_at&.to_h, "waits_on" => @disposition["waits_on"] }.compact
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Where the finding is written up, if anywhere, as checked_fields located it.
|
|
124
|
+
#: () -> WriteUpPointer?
|
|
125
|
+
def recorded_at
|
|
126
|
+
fields = @disposition["recorded_at"]
|
|
127
|
+
WriteUpPointer.new(fields) if fields
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Each field this disposition leaves for the agent still: an action that is not one of the four, the reason
|
|
131
|
+
# left empty, as the template's placeholder or as the answer guide's sample, and so a fix's reassessment,
|
|
132
|
+
# which applies to no other action, and a waits_on left as the guide's sample (#788, #789, #791, #794).
|
|
133
|
+
#: () -> Array[UnfinishedField]
|
|
134
|
+
def unfinished = [*unchosen_action, *unwritten_text, *waits_on_left_as_sample]
|
|
135
|
+
|
|
136
|
+
# The members this disposition claims it changed, refusing it unless its other fields are sound: only a fix
|
|
137
|
+
# claims any, and each must be among the members that changed. CorrectionAnswer has checked its action and
|
|
138
|
+
# that it wrote each field.
|
|
139
|
+
#: (Array[String]) -> Array[untyped]
|
|
140
|
+
def claimed_members(changed)
|
|
141
|
+
action = @disposition["action"]
|
|
142
|
+
checked_fields
|
|
143
|
+
members = @fields.array(@disposition["changed_members"], "changed_members")
|
|
144
|
+
entries = @disposition.fetch("changed_map_entries", [])
|
|
145
|
+
action == "fixed" ? fixed_members(members, entries, changed) : unclaimed(members, entries)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# The template leaves each action null, since choosing it is the agent's call; a null or unknown one is
|
|
151
|
+
# left for the agent, with the four to choose from (#708).
|
|
152
|
+
#: () -> UnfinishedField?
|
|
153
|
+
def unchosen_action
|
|
154
|
+
action = @disposition["action"]
|
|
155
|
+
return if ACTIONS.include?(action)
|
|
156
|
+
|
|
157
|
+
problem = action.nil? ? "is not chosen" : "is #{action.inspect}, not one of the four"
|
|
158
|
+
UnfinishedField.new(@place.child("action"), "action",
|
|
159
|
+
problem:, repair: "Set its action to one of fixed, deferred, accepted or disagreed.")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The reason, and a fix's reassessment, when left empty, as the template's placeholder (#788, #725) or as the
|
|
163
|
+
# answer guide's sample for this action (#789).
|
|
164
|
+
#: () -> Array[UnfinishedField]
|
|
165
|
+
def unwritten_text
|
|
166
|
+
action = @disposition["action"]
|
|
167
|
+
required = action == "fixed" ? TEXT_FIELDS : TEXT_FIELDS.slice("reason")
|
|
168
|
+
required.filter_map do |name, field|
|
|
169
|
+
field.left_in(@disposition[name], @place.child(name), name, sample: @samples.dig(action, name))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A deferral's waits_on left as the answer guide's sample, which illustrates the line without saying whom the
|
|
174
|
+
# finding waits on. It is one line, so WaitsOn would keep it (#789).
|
|
175
|
+
#: () -> UnfinishedField?
|
|
176
|
+
def waits_on_left_as_sample
|
|
177
|
+
sample = @samples.dig(@disposition["action"], "waits_on")
|
|
178
|
+
return unless sample && @disposition["waits_on"] == sample
|
|
179
|
+
|
|
180
|
+
UnfinishedField.new(@place.child("waits_on"), "waits_on", problem: SAMPLE_UNCHANGED, repair: WaitsOn::REPAIR)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A fix's claimed members, each of which must have changed. A fix may claim no member's text when it
|
|
184
|
+
# claims a map entry instead, which DeclaredMapChanges has checked changed (D40).
|
|
185
|
+
#: (Array[untyped], Array[untyped], Array[String]) -> Array[untyped]
|
|
186
|
+
def fixed_members(members, entries, changed)
|
|
187
|
+
return members unless (members.empty? && entries.empty?) || (members - changed).any?
|
|
188
|
+
|
|
189
|
+
raise Error.new("claimed_change_missing",
|
|
190
|
+
"Finding #{finding_id} claims changes its members do not show: #{members.join(", ")}",
|
|
191
|
+
"Make the authorised change and reassess it, or record a deferral.")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The disposition's evidence and recorded_at, which a deferral must give, if only as null, and the
|
|
195
|
+
# one line a deferral may give saying what it waits on (D52).
|
|
196
|
+
#: () -> void
|
|
197
|
+
def checked_fields
|
|
198
|
+
locate_pointers
|
|
199
|
+
unrecorded_deferral if @disposition["action"] == "deferred" && !@disposition.key?("recorded_at")
|
|
200
|
+
WaitsOn.new(@disposition["waits_on"], action: @disposition["action"])
|
|
201
|
+
.check(code: "invalid_disposition", subject: "Finding #{finding_id}")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Each piece of evidence, and where the finding is written up, located in the project.
|
|
205
|
+
#: () -> void
|
|
206
|
+
def locate_pointers
|
|
207
|
+
@fields.evidence(@disposition["evidence"], @place.child("evidence"))
|
|
208
|
+
@fields.pointer(@disposition["recorded_at"], @place.child("recorded_at")) if @disposition["recorded_at"]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# A deferral must say where it is written up, or null, so an answer reused from before recorded_at existed
|
|
212
|
+
# cannot drop the pointer silently (#299).
|
|
213
|
+
#: () -> bot
|
|
214
|
+
def unrecorded_deferral
|
|
215
|
+
raise Error.new("invalid_disposition", "Deferred finding #{finding_id} has no recorded_at",
|
|
216
|
+
"Set recorded_at to where the finding is written up, as {\"path\": ..., \"heading\": ...}, " \
|
|
217
|
+
"or to null when it is written up nowhere.")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
#: (Array[untyped], Array[untyped]) -> Array[untyped]
|
|
221
|
+
def unclaimed(members, entries)
|
|
222
|
+
unless entries.empty?
|
|
223
|
+
raise Error.new("invalid_disposition", "Only a fixed disposition may list changed_map_entries",
|
|
224
|
+
"Mark it fixed with a reassessment, or omit changed_map_entries.")
|
|
225
|
+
end
|
|
226
|
+
return [] if members.empty?
|
|
227
|
+
|
|
228
|
+
raise Error.new("invalid_disposition", "Only a fixed disposition may claim changed members",
|
|
229
|
+
"Mark it fixed with a reassessment, or omit changed_members.")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../data/project_files"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# One root entry file as the project holds it: the path, the file its links lead to, and its text, or the code
|
|
11
|
+
# this tool refused it with when it cannot be read. It says whether it links the other entry file: as a symbolic
|
|
12
|
+
# link to it, or by an import word, @AGENTS.md or @CLAUDE.md, which Claude Code documents as loading the file it
|
|
13
|
+
# names. That is a fact about the string; a plain mention of the other's name is not a link, and an unread text
|
|
14
|
+
# shows no import. It says, too, when git does not track it, since such a file may be the owner's alone (#896).
|
|
15
|
+
class EntryFile
|
|
16
|
+
# The entry file at path as the project holds it, or nil when there is none; tracked says whether git tracks
|
|
17
|
+
# it, and is true outside git, where nothing is checked.
|
|
18
|
+
#: (Data::ProjectFiles, String, tracked: bool) -> EntryFile?
|
|
19
|
+
def self.read(files, path, tracked:)
|
|
20
|
+
return nil unless files.file?(path)
|
|
21
|
+
|
|
22
|
+
leads_to = files.leads_to(path)
|
|
23
|
+
begin
|
|
24
|
+
new(path, leads_to: leads_to, text: files.read_text(path), tracked: tracked)
|
|
25
|
+
rescue Error => e
|
|
26
|
+
new(path, leads_to: leads_to, not_read: e.payload.fetch("code"), tracked: tracked)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: (String, leads_to: String, ?text: String?, ?not_read: String?, ?tracked: bool) -> void
|
|
31
|
+
def initialize(path, leads_to:, text: nil, not_read: nil, tracked: true)
|
|
32
|
+
@path = path
|
|
33
|
+
@leads_to = leads_to
|
|
34
|
+
@text = text
|
|
35
|
+
@not_read = not_read
|
|
36
|
+
@tracked = tracked
|
|
37
|
+
freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What setup reports of the file: that it exists, that git does not track it when it does not, why its text
|
|
41
|
+
# could not be read when it could not, and how it links other, when it does.
|
|
42
|
+
#: (String) -> Hash[String, untyped]
|
|
43
|
+
def facts(other)
|
|
44
|
+
untracked = @tracked ? {} : { "tracked" => false }
|
|
45
|
+
unread = @not_read ? { "not_read" => @not_read } : {}
|
|
46
|
+
{ "path" => @path, "exists" => true, **untracked, **unread }.merge(link(other))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# How this file links other, or nothing when it does not.
|
|
52
|
+
#: (String) -> Hash[String, String]
|
|
53
|
+
def link(other)
|
|
54
|
+
return { "links_to" => other, "by" => "symbolic_link" } if @leads_to == other
|
|
55
|
+
return { "links_to" => other, "by" => "import" } if @text.to_s.match?(import_word(other))
|
|
56
|
+
|
|
57
|
+
{}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# An @ import of path, as one word of the text.
|
|
61
|
+
#: (String) -> Regexp
|
|
62
|
+
def import_word(path) = %r{(?:\A|\s)@(?:\./)?#{Regexp.escape(path)}(?=\s|\z)}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../data/project_files"
|
|
5
|
+
require_relative "entry_file"
|
|
6
|
+
require_relative "project_history"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The project's root entry files, which a host loads in every session: CLAUDE.md for Claude Code and AGENTS.md for
|
|
12
|
+
# Codex, by each host's documentation. For each it says whether it exists, and, as each EntryFile says, whether it
|
|
13
|
+
# links the other, and whether git tracks it.
|
|
14
|
+
class EntryFiles
|
|
15
|
+
PATHS = %w[CLAUDE.md AGENTS.md].freeze #: Array[String]
|
|
16
|
+
|
|
17
|
+
# Each entry file as the project holds it, or nil for one that does not exist. history says which git does not
|
|
18
|
+
# track: a path whose own file, or a file its links lead to, git does not hold, so a fresh clone would not find
|
|
19
|
+
# its text there (#695, #896).
|
|
20
|
+
#: (Data::ProjectFiles, ProjectHistory) -> EntryFiles
|
|
21
|
+
def self.read(files, history)
|
|
22
|
+
new(PATHS.to_h { |path| [path, EntryFile.read(files, path, tracked: history.tracks?(path))] })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# held maps each entry file's path to the file, or nil when it does not exist.
|
|
26
|
+
#: (Hash[String, EntryFile?]) -> void
|
|
27
|
+
def initialize(held)
|
|
28
|
+
@held = held.freeze #: Hash[String, EntryFile?]
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Each entry file's facts, in PATHS order.
|
|
33
|
+
#: () -> Array[Hash[String, untyped]]
|
|
34
|
+
def to_a = PATHS.map { |path| facts(path) }
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
#: (String) -> Hash[String, untyped]
|
|
39
|
+
def facts(path)
|
|
40
|
+
file = @held[path] or return { "path" => path, "exists" => false }
|
|
41
|
+
|
|
42
|
+
file.facts((PATHS - [path]).first.to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
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 item of the evidence a finding was raised with, as its record keeps it: a path, with a heading, passage or
|
|
10
|
+
# query inside it, or none to name the whole file (D64). It holds that place as the Locator its record's hash
|
|
11
|
+
# gives, which keeps the hash's keys and order, since the record's bytes depend on it.
|
|
12
|
+
class EvidenceItem
|
|
13
|
+
# Where in the project the item points, as MemberFile reads it.
|
|
14
|
+
#: Locator
|
|
15
|
+
attr_reader :locator
|
|
16
|
+
|
|
17
|
+
#: (Hash[String, untyped]) -> void
|
|
18
|
+
def initialize(fields)
|
|
19
|
+
@locator = Locator.new(fields) #: Locator
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The file it points into.
|
|
24
|
+
#: () -> String
|
|
25
|
+
def path = @locator.path
|
|
26
|
+
|
|
27
|
+
# Whether it names its whole file, with no heading, passage or query inside it.
|
|
28
|
+
#: () -> bool
|
|
29
|
+
def whole_file? = @locator.whole_file?
|
|
30
|
+
|
|
31
|
+
# The lines it names in the project's file at commit, or nil when that commit lacks the file or its place there.
|
|
32
|
+
#: (ProjectHistory, String) -> Array[String]?
|
|
33
|
+
def lines_at(history, commit) = history.lines_at(commit, @locator)
|
|
34
|
+
|
|
35
|
+
# The lines it names in the project's file now; refused when it no longer resolves.
|
|
36
|
+
#: (Project) -> Array[String]
|
|
37
|
+
def lines_now(project) = project.member_file(path).lines_of(@locator)
|
|
38
|
+
|
|
39
|
+
# The item as printed, with facts about it beside its fields.
|
|
40
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
41
|
+
def with_facts(facts) = to_h.merge(facts)
|
|
42
|
+
|
|
43
|
+
# The item as its record holds it.
|
|
44
|
+
#: () -> Hash[String, untyped]
|
|
45
|
+
def to_h = @locator.to_h
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|