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,76 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "audit_record"
|
|
5
|
+
require_relative "member"
|
|
6
|
+
require_relative "member_grounds"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# Which members a full audit carries forward the last full audit's assessment of, and why it asks the agent to
|
|
12
|
+
# judge each other one (D48, D49). A member is carried only when the last completed record kept its assessment
|
|
13
|
+
# and judged it against the same description and question, and its text, id, map entry and loads are as that
|
|
14
|
+
# audit judged them. Each test compares stored values, so it is this tool's to make; whether the set still
|
|
15
|
+
# holds, and whether to re-judge a carried member, is the agent's.
|
|
16
|
+
class CarriedAssessments
|
|
17
|
+
# record is the principle's audit record now, if any; members are the map's members now, with their hashes in
|
|
18
|
+
# now and their grounds; judged_against digests the description and question this audit asks.
|
|
19
|
+
#: (AuditRecord?, members: MapMembers, judged_against: String, now: Hash[String, String],
|
|
20
|
+
#| grounds: MemberGrounds) -> void
|
|
21
|
+
def initialize(record, members:, judged_against:, now:, grounds:)
|
|
22
|
+
@last = record&.last_completed #: AuditRecord?
|
|
23
|
+
@members = members
|
|
24
|
+
@judged_against = judged_against
|
|
25
|
+
@now = now
|
|
26
|
+
@grounds = grounds
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Why the member id must be judged again, or nil when its last assessment is carried forward. A record from
|
|
31
|
+
# before assessments were kept assessed each member it recorded, but kept none to carry (#701).
|
|
32
|
+
#: (String) -> String?
|
|
33
|
+
def judge_because(id)
|
|
34
|
+
last = @last or return "not_assessed_before"
|
|
35
|
+
entry = recorded(id)
|
|
36
|
+
return reason_in_record_before_assessments_were_kept(entry) unless last.kept_assessments?
|
|
37
|
+
return "description_or_question_changed" unless last.judged_against?(@judged_against)
|
|
38
|
+
|
|
39
|
+
entry ? entry.judge_because(id: id, sha256: @now[id], grounds: @grounds.of(id)) : "not_assessed_before"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Whether the member id's last assessment is carried forward.
|
|
43
|
+
#: (String) -> bool
|
|
44
|
+
def carried?(id) = judge_because(id).nil?
|
|
45
|
+
|
|
46
|
+
# What the request shows beside the member id: the assessment carried forward, with whether any evidence it
|
|
47
|
+
# cited changed since, which the block says from each evidence item's hash then; or why it is judged again, and
|
|
48
|
+
# where it was if it moved.
|
|
49
|
+
#: (String) { (Locator, String) -> bool } -> Hash[String, untyped]
|
|
50
|
+
def shown(id, &)
|
|
51
|
+
reason = judge_because(id)
|
|
52
|
+
return { "judge_because" => reason, "moved_from" => recorded(id)&.moved_from(id) }.compact if reason
|
|
53
|
+
|
|
54
|
+
entry = recorded(id) or return {}
|
|
55
|
+
{ "carried_forward" => entry.carried_forward(&) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# When the last full audit's sighting of the member id was, if a fresh observation repeats that audit's word for
|
|
59
|
+
# word; nil when it does not, or there was none. Whether the sighting is the same is the agent's to say (#680).
|
|
60
|
+
#: (String, String) -> String?
|
|
61
|
+
def repeated_sighting(id, observation) = recorded(id)&.repeated_sighting(observation)
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Why a member of a record from before assessments were kept is judged again: that audit assessed it, if the
|
|
66
|
+
# record holds its entry, but kept no assessment to carry.
|
|
67
|
+
#: (RecordedEntry?) -> String
|
|
68
|
+
def reason_in_record_before_assessments_were_kept(entry)
|
|
69
|
+
entry ? "assessed_before_assessments_were_kept" : "not_assessed_before"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: (String) -> RecordedEntry?
|
|
73
|
+
def recorded(id) = @last&.members&.recorded(@members.found(id).key)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "audit_record"
|
|
6
|
+
require_relative "member"
|
|
7
|
+
require_relative "member_map_history"
|
|
8
|
+
require_relative "member_maps"
|
|
9
|
+
require_relative "project_history"
|
|
10
|
+
|
|
11
|
+
module Principles
|
|
12
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
13
|
+
module Models
|
|
14
|
+
# Whether a member's check is the one the principle's last audit was given an exit status for, and, when
|
|
15
|
+
# it is not, the commit that made it what it is now. A check is a command any collaborator can change in the
|
|
16
|
+
# committed map, and the audit asks the agent to run it, so a check no audit has seen needs a review
|
|
17
|
+
# first (#227). This tool states the fact; the review is the agent's step.
|
|
18
|
+
class CheckSinceLastAudit
|
|
19
|
+
#: (id: String, history: ProjectHistory, member_maps: MemberMaps, record: AuditRecord?) -> void
|
|
20
|
+
def initialize(id:, history:, member_maps:, record:)
|
|
21
|
+
@id = id
|
|
22
|
+
@history = history
|
|
23
|
+
@member_maps = member_maps
|
|
24
|
+
@record = record
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# "unchanged" when the last audit was given an exit status for this same check; otherwise "new" or
|
|
29
|
+
# "changed", with changed_in the oldest commit of the unbroken run, back from HEAD, whose map holds it,
|
|
30
|
+
# and, when changed, last_run, the command the last audit ran. Beside an unchanged check, the line saying what its
|
|
31
|
+
# last review rested on and since when; beside a changed one, last_run_review_basis, that line for the command
|
|
32
|
+
# the last audit ran, not this one (D63).
|
|
33
|
+
# changed_in is nil outside git, or when no commit holds it. The walk stops at a map it cannot read, so
|
|
34
|
+
# changed_in may then name a later commit than the one that set the check: the review is still asked for.
|
|
35
|
+
#: (Member, String) -> Hash[String, untyped]
|
|
36
|
+
def of(member, check)
|
|
37
|
+
ran = @record&.check_run_for(member.key)
|
|
38
|
+
review = review_of(member.key)
|
|
39
|
+
return { "status" => "unchanged", **review } if ran == check
|
|
40
|
+
|
|
41
|
+
changed_in = commit_that_set(member.key, check)
|
|
42
|
+
return { "status" => "new", "changed_in" => changed_in } unless ran
|
|
43
|
+
|
|
44
|
+
last_run_review = review.slice("review_basis").transform_keys { "last_run_review_basis" }
|
|
45
|
+
{ "status" => "changed", "last_run" => ran, **last_run_review, "changed_in" => changed_in }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The line the last audit's record keeps for what the review of its run rested on, and since when (D63).
|
|
51
|
+
#: (String) -> Hash[String, untyped]
|
|
52
|
+
def review_of(key) = @record&.check_review_for(key) || {}
|
|
53
|
+
|
|
54
|
+
#: (String, String) -> String?
|
|
55
|
+
def commit_that_set(key, check)
|
|
56
|
+
run = MemberMapHistory.new(id: @id, history: @history, member_maps: @member_maps).runs([key]).fetch(key)
|
|
57
|
+
run.take_while { |_sha, fields| Member.new(fields).check == check }.last&.first
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "assessment_report"
|
|
5
|
+
require_relative "project_history"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The files an audit record cites, through its assessments' evidence and its findings' evidence and write-ups, as
|
|
11
|
+
# the project holds them now: which go with the record when its completed audit is committed, since a clone needs
|
|
12
|
+
# every file the record points at (#724), and which no longer exist, though the record still points at them (#861).
|
|
13
|
+
class CitedFiles
|
|
14
|
+
# paths are the files the record cites; in_git says whether its audit had a base commit to compare with.
|
|
15
|
+
#: (Array[String], ProjectHistory, in_git: bool) -> void
|
|
16
|
+
def initialize(paths, history, in_git:)
|
|
17
|
+
@paths = paths
|
|
18
|
+
@history = history
|
|
19
|
+
@in_git = in_git
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The files to commit with the record, sorted once each: each file the audit changed; in git, with each cited
|
|
24
|
+
# file, only those that still differ from HEAD, since a fix the agent already committed needs nothing. A cited
|
|
25
|
+
# file deleted since is left out, since adding it would stage a deletion the audit never made (#842); a file its
|
|
26
|
+
# fixes deleted stays. What reported cites that the record does not keep, as the answer's recent decision's
|
|
27
|
+
# paths, is cited too (#860).
|
|
28
|
+
#: (Array[String], ?AssessmentReport) -> Array[String]
|
|
29
|
+
def committed_with(changed, reported = AssessmentReport::NONE)
|
|
30
|
+
changed = @history.uncommitted(changed + @history.existing(@paths + reported.recent_decision_paths)) if @in_git
|
|
31
|
+
changed.uniq.sort
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Each cited file that no longer exists, in git or outside it (#861).
|
|
35
|
+
#: () -> Array[String]
|
|
36
|
+
def missing = @paths - @history.existing(@paths)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../data/command_steps_file"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# Values built from validated data, and the rules a schema cannot express. models/CLAUDE.md owns the
|
|
9
|
+
# boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The steps each command runs: which are this tool's and which are the agent's, the refusals each of
|
|
12
|
+
# this tool's steps can raise, and where each step leads. Each command's --help and the workflow diagram
|
|
13
|
+
# are drawn from it (ticket 001 D15). It is built from data Data::CommandStepsFile has already checked
|
|
14
|
+
# against its schema, and checks what a schema cannot: every step a branch names exists, every refusal
|
|
15
|
+
# group exists, and every command a step hands over to is real. The prompts it names are checked by
|
|
16
|
+
# spec/unit/principles/views/prompts_spec.rb, because the domain reads no files.
|
|
17
|
+
class CommandSteps
|
|
18
|
+
# Where a step leads when it branches.
|
|
19
|
+
class Branch
|
|
20
|
+
#: String
|
|
21
|
+
attr_reader :to
|
|
22
|
+
#: String
|
|
23
|
+
attr_reader :condition
|
|
24
|
+
|
|
25
|
+
# A step's branches, from its "next" list.
|
|
26
|
+
#: (Array[Hash[String, String]]) -> Array[Branch]
|
|
27
|
+
def self.all(edges)
|
|
28
|
+
edges.map { |edge| new(edge.fetch("to"), edge.fetch("when")) }.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: (String, String) -> void
|
|
32
|
+
def initialize(to, condition)
|
|
33
|
+
@to = to
|
|
34
|
+
@condition = condition
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# What an agent's step hands back: what this tool then checks, what it cannot, and the command the agent
|
|
40
|
+
# answers with.
|
|
41
|
+
class Handover
|
|
42
|
+
#: String?
|
|
43
|
+
attr_reader :checks
|
|
44
|
+
#: String?
|
|
45
|
+
attr_reader :cannot_check
|
|
46
|
+
#: String?
|
|
47
|
+
attr_reader :answer_with
|
|
48
|
+
# When the agent runs answer_with, where that is the agent's to time, as a first full audit is (#627).
|
|
49
|
+
#: String?
|
|
50
|
+
attr_reader :answer_when
|
|
51
|
+
# The form of answer_with the step hands to, where its whole usage would mislead, as a first full audit
|
|
52
|
+
# that --member would make a refused one-member audit (#635).
|
|
53
|
+
#: String?
|
|
54
|
+
attr_reader :answer_usage
|
|
55
|
+
|
|
56
|
+
#: (Hash[String, untyped]) -> void
|
|
57
|
+
def initialize(data)
|
|
58
|
+
@checks = data["checks"] #: String?
|
|
59
|
+
@cannot_check = data["cannot_check"] #: String?
|
|
60
|
+
@answer_with = data["answer_with"] #: String?
|
|
61
|
+
@answer_when = data["answer_when"] #: String?
|
|
62
|
+
@answer_usage = data["answer_usage"] #: String?
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether answer_usage, if given, is a form of the command answer_with names, so it cannot drift to another.
|
|
67
|
+
#: () -> bool
|
|
68
|
+
def usage_names_its_command?
|
|
69
|
+
usage = @answer_usage
|
|
70
|
+
usage.nil? || usage == @answer_with || usage.start_with?("#{@answer_with} ")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# One step: this tool's ("cli") or the agent's ("agent").
|
|
75
|
+
class Step
|
|
76
|
+
#: String
|
|
77
|
+
attr_reader :id
|
|
78
|
+
#: String
|
|
79
|
+
attr_reader :does
|
|
80
|
+
#: Array[String]
|
|
81
|
+
attr_reader :refusals
|
|
82
|
+
#: String?
|
|
83
|
+
attr_reader :prompt
|
|
84
|
+
#: Handover
|
|
85
|
+
attr_reader :handover
|
|
86
|
+
#: Array[Branch]
|
|
87
|
+
attr_reader :branches
|
|
88
|
+
|
|
89
|
+
#: (Hash[String, untyped], Array[String]) -> void
|
|
90
|
+
def initialize(data, refusals)
|
|
91
|
+
@id = data.fetch("id") #: String
|
|
92
|
+
@agent = data.fetch("actor") == "agent" #: bool
|
|
93
|
+
@does = data.fetch("does") #: String
|
|
94
|
+
@refusals = refusals.freeze #: Array[String]
|
|
95
|
+
@prompt = data["prompt"] #: String?
|
|
96
|
+
@handover = Handover.new(data) #: Handover
|
|
97
|
+
@branches = Branch.all(data.fetch("next", [])) #: Array[Branch]
|
|
98
|
+
freeze
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#: () -> bool
|
|
102
|
+
def agent?
|
|
103
|
+
@agent
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A heading the overview lists commands under, and what every command under it writes.
|
|
108
|
+
class Group
|
|
109
|
+
#: String
|
|
110
|
+
attr_reader :id
|
|
111
|
+
#: String
|
|
112
|
+
attr_reader :heading
|
|
113
|
+
#: String
|
|
114
|
+
attr_reader :writes
|
|
115
|
+
|
|
116
|
+
#: (Hash[String, untyped]) -> void
|
|
117
|
+
def initialize(data)
|
|
118
|
+
@id = data.fetch("id") #: String
|
|
119
|
+
@heading = data.fetch("heading") #: String
|
|
120
|
+
@writes = data.fetch("writes") #: String
|
|
121
|
+
freeze
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# One command, with its steps in the order they run.
|
|
126
|
+
class Command
|
|
127
|
+
#: String
|
|
128
|
+
attr_reader :name
|
|
129
|
+
#: String
|
|
130
|
+
attr_reader :usage
|
|
131
|
+
#: String
|
|
132
|
+
attr_reader :group
|
|
133
|
+
# The named options it takes, without their dashes, besides --project and --help.
|
|
134
|
+
#: Array[String]
|
|
135
|
+
attr_reader :options
|
|
136
|
+
#: String
|
|
137
|
+
attr_reader :summary
|
|
138
|
+
#: String
|
|
139
|
+
attr_reader :help
|
|
140
|
+
#: Array[Step]
|
|
141
|
+
attr_reader :steps
|
|
142
|
+
|
|
143
|
+
#: (Hash[String, untyped], Array[Step]) -> void
|
|
144
|
+
def initialize(data, steps)
|
|
145
|
+
@name = data.fetch("name") #: String
|
|
146
|
+
@usage = data.fetch("usage") #: String
|
|
147
|
+
@group = data.fetch("group") #: String
|
|
148
|
+
@options = data.fetch("options").freeze #: Array[String]
|
|
149
|
+
@summary = data.fetch("summary") #: String
|
|
150
|
+
@help = data.fetch("help") #: String
|
|
151
|
+
@steps = steps.freeze #: Array[Step]
|
|
152
|
+
freeze
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The steps a step leads to: its branches, or else the next step unless it hands over to a command.
|
|
156
|
+
#: (Step) -> Array[Step]
|
|
157
|
+
def successors(step)
|
|
158
|
+
return step.branches.map { |branch| find(branch.to) } if step.branches.any?
|
|
159
|
+
return [] if step.handover.answer_with
|
|
160
|
+
|
|
161
|
+
following = @steps[@steps.index(step).to_i + 1]
|
|
162
|
+
following ? [following] : []
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#: (String) -> Step
|
|
166
|
+
def find(id)
|
|
167
|
+
@steps.find { |step| step.id == id } || raise(KeyError, "#{@name} has no step #{id}")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The overview's headings, in the order it lists them.
|
|
172
|
+
#: Array[Group]
|
|
173
|
+
attr_reader :groups
|
|
174
|
+
#: Array[String]
|
|
175
|
+
attr_reader :every_command_refuses
|
|
176
|
+
#: Array[Command]
|
|
177
|
+
attr_reader :commands
|
|
178
|
+
|
|
179
|
+
# Every command's steps, read from the command-steps.json the gem ships.
|
|
180
|
+
#: () -> CommandSteps
|
|
181
|
+
def self.load
|
|
182
|
+
new(Data::CommandStepsFile.new.read, Data::CommandStepsFile::PATH)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# data is the validated content of command-steps.json; source names it in a refusal.
|
|
186
|
+
#: (untyped, String) -> void
|
|
187
|
+
def initialize(data, source)
|
|
188
|
+
@source = source
|
|
189
|
+
@groups = data.fetch("groups").map { |group| Group.new(group) }.freeze #: Array[Group]
|
|
190
|
+
@refusal_groups = data.fetch("refusal_groups") #: Hash[String, Array[String]]
|
|
191
|
+
@every_command_refuses = @refusal_groups.fetch("any-command") #: Array[String]
|
|
192
|
+
@commands = data.fetch("commands").map { |command| build(command) }.freeze #: Array[Command]
|
|
193
|
+
check_references
|
|
194
|
+
freeze
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
#: (String) -> Command?
|
|
198
|
+
def command(name)
|
|
199
|
+
@commands.find { |command| command.name == name }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# What a command writes, which every command in its group shares.
|
|
203
|
+
#: (Command) -> String
|
|
204
|
+
def writes(command)
|
|
205
|
+
@groups.find { |group| group.id == command.group }&.writes || refuse("#{command.name} is in no group")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Every refusal code some step, or every command, can raise.
|
|
209
|
+
#: () -> Array[String]
|
|
210
|
+
def refusal_codes
|
|
211
|
+
(@every_command_refuses + @commands.flat_map { |command| command.steps.flat_map(&:refusals) }).uniq.sort
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Every prompt this file names: each command's help and each step's prompt.
|
|
215
|
+
#: () -> Array[String]
|
|
216
|
+
def prompt_names
|
|
217
|
+
@commands.flat_map { |command| [command.help, *command.steps.filter_map(&:prompt)] }.uniq.sort
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
#: (Hash[String, untyped]) -> Command
|
|
223
|
+
def build(data)
|
|
224
|
+
steps = data.fetch("steps").map do |step|
|
|
225
|
+
Step.new(step, (step.fetch("refuses", []) + step.fetch("refuses_like", []).flat_map do |group|
|
|
226
|
+
refusal_group(group)
|
|
227
|
+
end).uniq)
|
|
228
|
+
end
|
|
229
|
+
Command.new(data, steps)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
#: (String) -> Array[String]
|
|
233
|
+
def refusal_group(name)
|
|
234
|
+
@refusal_groups.fetch(name) { refuse("No refusal group #{name}") }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
#: () -> void
|
|
238
|
+
def check_references
|
|
239
|
+
names = @commands.map(&:name)
|
|
240
|
+
refuse("Command names repeat") unless names.uniq == names
|
|
241
|
+
@commands.each { |command| check_command(command, names) }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
#: (Command, Array[String]) -> void
|
|
245
|
+
def check_command(command, names)
|
|
246
|
+
writes(command)
|
|
247
|
+
ids = command.steps.map(&:id)
|
|
248
|
+
refuse("#{command.name} repeats a step id") unless ids.uniq == ids
|
|
249
|
+
command.steps.each { |step| check_step("#{command.name}/#{step.id}", step, names, ids) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
#: (String, Step, Array[String], Array[String]) -> void
|
|
253
|
+
def check_step(where, step, names, ids)
|
|
254
|
+
target = step.handover.answer_with
|
|
255
|
+
refuse("#{where} answers with unknown command #{target}") if target && !names.include?(target)
|
|
256
|
+
unless step.handover.usage_names_its_command?
|
|
257
|
+
refuse("#{where}'s handover form #{step.handover.answer_usage} does not name #{target}")
|
|
258
|
+
end
|
|
259
|
+
unknown = step.branches.map(&:to) - ids
|
|
260
|
+
refuse("#{where} branches to unknown step #{unknown.join(", ")}") if unknown.any?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
#: (String) -> bot
|
|
264
|
+
def refuse(message)
|
|
265
|
+
raise Error.new("invalid_command_steps", message,
|
|
266
|
+
"Repair the file so every step it names exists.", @source)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The commits one move of HEAD brought in, by abbreviated id, as the after-commit hook asks about them (D73): one
|
|
8
|
+
# for a commit or an amend, a lane for a fast-forward, the rewritten commits for a rebase. git chooses how long an
|
|
9
|
+
# abbreviated id is, so two name the same commit when one starts with the other (review R4).
|
|
10
|
+
class CommitsBroughtIn
|
|
11
|
+
#: (Array[String]) -> void
|
|
12
|
+
def initialize(commits)
|
|
13
|
+
@commits = commits
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The members, as list-member-problems lists them, whose newest changing commit the move brought in, in order.
|
|
18
|
+
#: (Array[Hash[String, untyped]]) -> Array[Hash[String, untyped]]
|
|
19
|
+
def changed_by(members)
|
|
20
|
+
members.select do |member|
|
|
21
|
+
newest = member.dig("history", "commits", 0, "commit")
|
|
22
|
+
newest && @commits.any? { |commit| commit.start_with?(newest) || newest.start_with?(commit) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# In git, what landed after a completed audit's base commit: for each member the audit changed, the commits
|
|
8
|
+
# since the base that touched it, and HEAD now. When HEAD differs from the base and none touched one, an empty
|
|
9
|
+
# list alone left the agent unsure why an answer on an older base was accepted, so it says so (#740).
|
|
10
|
+
class CommitsSinceBase
|
|
11
|
+
#: (base: String, head: String?, by_member: Hash[String, Array[Hash[String, String]]]) -> void
|
|
12
|
+
def initialize(base:, head:, by_member:)
|
|
13
|
+
@base = base
|
|
14
|
+
@head = head
|
|
15
|
+
@by_member = by_member
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The commits since the base that touched each member the audit changed, by member id.
|
|
20
|
+
#: Hash[String, Array[Hash[String, String]]]
|
|
21
|
+
attr_reader :by_member
|
|
22
|
+
|
|
23
|
+
# The base and HEAD, when HEAD differs from the base and no commit since the base touched a member the audit
|
|
24
|
+
# changed; nil otherwise.
|
|
25
|
+
#: () -> [String, String]?
|
|
26
|
+
def head_past_base
|
|
27
|
+
head = @head
|
|
28
|
+
[@base, head] unless head.nil? || head == @base || @by_member.values.any?(&:any?)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../prose_list"
|
|
6
|
+
require_relative "answer_fields"
|
|
7
|
+
require_relative "disposition"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# An agent's answer to a correction request: one disposition for each finding the assessment raised, each
|
|
13
|
+
# checked as a Disposition, and no member changed that no fix claims. The block locates each piece of
|
|
14
|
+
# evidence, and where a finding is recorded, in the project. samples is the answer guide's disposition for
|
|
15
|
+
# each action, whose text no field may give back unchanged (#789).
|
|
16
|
+
class CorrectionAnswer
|
|
17
|
+
#: (untyped, Array[untyped], samples: Hash[String, untyped]) { (Locator) -> void } -> void
|
|
18
|
+
def initialize(response, findings, samples:, &locate)
|
|
19
|
+
@response = response
|
|
20
|
+
@samples = samples
|
|
21
|
+
@finding_ids = findings.map { |finding| finding.fetch("id") } #: Array[untyped]
|
|
22
|
+
@fields = AnswerFields.new(locate) #: AnswerFields
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The dispositions, once each is checked against changed, the members that changed since the audit began, as
|
|
26
|
+
# AuditStart#changed names them, those added included (T69).
|
|
27
|
+
#: (changed: Array[String]) -> Array[Disposition]
|
|
28
|
+
def dispositions(changed:)
|
|
29
|
+
dispositions = one_per_finding
|
|
30
|
+
unclaimed = changed - dispositions.flat_map { |disposition| disposition.claimed_members(changed) }
|
|
31
|
+
return dispositions if unclaimed.empty?
|
|
32
|
+
|
|
33
|
+
raise Error.new("unassessed_changes", "Other members changed: #{unclaimed.join(", ")}",
|
|
34
|
+
"Start a fresh audit, or include their reassessment in a fixed disposition.")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
#: () -> Array[Disposition]
|
|
40
|
+
def one_per_finding
|
|
41
|
+
given = @fields.array(@response["dispositions"], "dispositions")
|
|
42
|
+
dispositions = given.each_with_index.map do |disposition, index|
|
|
43
|
+
Disposition.new(disposition, @fields, at: "/dispositions/#{index}", samples: @samples)
|
|
44
|
+
end
|
|
45
|
+
unless dispositions.map(&:finding_id).sort == @finding_ids.sort
|
|
46
|
+
raise Error.new("incomplete_dispositions",
|
|
47
|
+
"Dispose of each finding exactly once: #{@finding_ids.join(", ")}", "Complete dispositions.")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
refuse_unfinished(dispositions)
|
|
51
|
+
dispositions
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Refuses every field the dispositions leave empty, as the template's placeholder or as the answer guide's
|
|
55
|
+
# sample in one refusal, so a copied template takes one repair, not one submit per field (#789, #791).
|
|
56
|
+
#: (Array[Disposition]) -> void
|
|
57
|
+
def refuse_unfinished(dispositions)
|
|
58
|
+
unfinished = dispositions.flat_map(&:unfinished)
|
|
59
|
+
return if unfinished.empty?
|
|
60
|
+
|
|
61
|
+
raise Error.new("invalid_disposition", "Still to write: #{ProseList.new(unfinished.map(&:to_s))}",
|
|
62
|
+
unfinished.map(&:repair).uniq.join(" "), fields: unfinished.map(&:field))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "request_id"
|
|
5
|
+
require_relative "decided_prior_findings"
|
|
6
|
+
require_relative "audit_start"
|
|
7
|
+
require_relative "last_completed_revision"
|
|
8
|
+
require_relative "recorded_finding"
|
|
9
|
+
|
|
10
|
+
module Principles
|
|
11
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
12
|
+
module Models
|
|
13
|
+
# What a correction request depends on, which it prints as its basis and the agent hands back with the answer:
|
|
14
|
+
# the base commit, the map's version, each finding's members and problem, what the assessment reported beyond its
|
|
15
|
+
# findings, as AssessmentReport carries it for the completed record to keep, and the decisions on prior findings
|
|
16
|
+
# (D35), which travel to completion as those do. It also carries the last completed record's revision, so a
|
|
17
|
+
# correction is refused once that record changed (#646).
|
|
18
|
+
class CorrectionBasis
|
|
19
|
+
# The basis printed for a correction of findings, assessed from start: its base, its map's version and the
|
|
20
|
+
# assessment's report, as AssessmentReport carries it; revision is the last completed record's.
|
|
21
|
+
#: (AuditStart, findings: Array[untyped], revision: String?, ?prior: DecidedPriorFindings) -> CorrectionBasis
|
|
22
|
+
def self.printed(start, findings:, revision:, prior: DecidedPriorFindings::NONE)
|
|
23
|
+
basis = { "base_commit" => start.base, "map_version" => start.map_version,
|
|
24
|
+
"last_completed_revision" => revision,
|
|
25
|
+
"findings" => findings.map { |finding| finding.slice(*RecordedFinding::RAISED_FIELDS) } }
|
|
26
|
+
basis = basis.merge(start.reported.basis_fields)
|
|
27
|
+
new(prior.none? ? basis : basis.merge("prior_findings" => prior.to_h))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: (untyped) -> void
|
|
31
|
+
def initialize(fields)
|
|
32
|
+
@fields = fields #: untyped
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: () -> untyped
|
|
37
|
+
def to_h = @fields
|
|
38
|
+
|
|
39
|
+
# The commit the audit it corrects began from, or nil outside git.
|
|
40
|
+
#: () -> String?
|
|
41
|
+
def base_commit = @fields.fetch("base_commit")
|
|
42
|
+
|
|
43
|
+
# The version of the map the audit it corrects began from.
|
|
44
|
+
#: () -> String
|
|
45
|
+
def map_version = @fields.fetch("map_version")
|
|
46
|
+
|
|
47
|
+
# The findings it asks the correction to dispose of, each with the fields RecordedFinding::RAISED_FIELDS names.
|
|
48
|
+
#: () -> Array[Hash[String, untyped]]
|
|
49
|
+
def findings = @fields.fetch("findings")
|
|
50
|
+
|
|
51
|
+
# The decisions on prior findings it carries to completion, or none (D35).
|
|
52
|
+
#: () -> DecidedPriorFindings
|
|
53
|
+
def prior_decided
|
|
54
|
+
decided = @fields["prior_findings"]
|
|
55
|
+
decided ? DecidedPriorFindings.new(decided) : DecidedPriorFindings::NONE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The id of the correction request of principle_id for this basis, with each member's hash when the audit began.
|
|
59
|
+
#: (String, Hash[String, String]) -> RequestId
|
|
60
|
+
def request_id(principle_id, before)
|
|
61
|
+
RequestId.for(principle_id: principle_id, phase: "correction", parts: [@fields, before])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The last completed record's revision when the request was printed, which refuses a correction once that
|
|
65
|
+
# record changed (#646).
|
|
66
|
+
#: () -> LastCompletedRevision
|
|
67
|
+
def printed_after = LastCompletedRevision.new(@fields["last_completed_revision"])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# A line an answer gives that a record keeps under field, with field_since, the date it was first given, beside
|
|
8
|
+
# it: the same line keeps its date, a new line is dated afresh by the audit that records it, and null drops it
|
|
9
|
+
# (D52).
|
|
10
|
+
class DatedLine
|
|
11
|
+
# line is what the answer gave, nil for none; field is the name the record keeps it under.
|
|
12
|
+
#: (untyped, field: String) -> void
|
|
13
|
+
def initialize(line, field:)
|
|
14
|
+
@line = line
|
|
15
|
+
@field = field
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The line as the record keeps it, given had, the line and date it carried: had when the line is unchanged, so
|
|
20
|
+
# it keeps its date; this line alone, which the record's audited_at dates, when new; nothing when it is null.
|
|
21
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
22
|
+
def kept_over(had)
|
|
23
|
+
return had if @line == had[@field]
|
|
24
|
+
|
|
25
|
+
@line.nil? ? {} : { @field => @line }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|