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,176 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "../error"
|
|
7
|
+
require_relative "answer_place"
|
|
8
|
+
require_relative "answer_text"
|
|
9
|
+
require_relative "answered_time"
|
|
10
|
+
require_relative "carried_assessments"
|
|
11
|
+
|
|
12
|
+
module Principles
|
|
13
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
14
|
+
module Models
|
|
15
|
+
# One member assessment's sighting: whether its delivery was observed, what was seen, and when, as the last full
|
|
16
|
+
# audit's sighting of the member bears on it (#680, #716). Its delivery picks its kind: Observed, or NotObserved.
|
|
17
|
+
class MemberSighting
|
|
18
|
+
# The sighting of assessment, one of the answer's member_assessments at the place at, of the kind its
|
|
19
|
+
# delivery names; carried knows the last full audit's sightings, and answered_at is when the answer came. A
|
|
20
|
+
# delivery other than observed or not_observed is refused here, before the evidence, as the refusals have always
|
|
21
|
+
# come.
|
|
22
|
+
#: (Hash[String, untyped], carried: CarriedAssessments?, answered_at: String, at: AnswerPlace) -> MemberSighting
|
|
23
|
+
def self.for(assessment, carried:, answered_at:, at:)
|
|
24
|
+
case assessment["delivery"]
|
|
25
|
+
when "observed" then Observed.new(assessment, carried: carried, answered_at: answered_at, at: at)
|
|
26
|
+
when "not_observed" then NotObserved.new(assessment, at: at)
|
|
27
|
+
else raise Error.new("invalid_delivery", "Use observed or not_observed",
|
|
28
|
+
"Record unavailable delivery as not_observed.")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (Hash[String, untyped], at: AnswerPlace) -> void
|
|
33
|
+
def initialize(assessment, at:)
|
|
34
|
+
@assessment = assessment
|
|
35
|
+
@at = at
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Refuses the sighting unless it holds to its kind's rules.
|
|
39
|
+
#: () -> void
|
|
40
|
+
def check = raise(NotImplementedError, "#{self.class} has no rules of its own")
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
#: () -> untyped
|
|
45
|
+
def id = @assessment["member_id"]
|
|
46
|
+
|
|
47
|
+
# The JSON pointer to field of this assessment.
|
|
48
|
+
#: (String) -> String
|
|
49
|
+
def pointer(field) = @at.child(field).pointer
|
|
50
|
+
|
|
51
|
+
# A delivery not observed has no sighting, so it gives no observed_at (#680).
|
|
52
|
+
class NotObserved < MemberSighting
|
|
53
|
+
#: (Hash[String, untyped], at: AnswerPlace) -> void
|
|
54
|
+
def initialize(assessment, at:)
|
|
55
|
+
super
|
|
56
|
+
freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#: () -> void
|
|
60
|
+
def check
|
|
61
|
+
return unless @assessment.key?("observed_at")
|
|
62
|
+
|
|
63
|
+
raise Error.new("invalid_answer", "Member #{id} is not observed but gives observed_at",
|
|
64
|
+
"Drop observed_at, or record delivery as observed with what you saw in observation.",
|
|
65
|
+
fields: [{ "pointer" => pointer("observed_at"),
|
|
66
|
+
"problem" => "only an observed delivery has a sighting to date" }])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# An observed delivery says what was seen, and when, as a time the record can compare, and no word-for-word
|
|
71
|
+
# repeat of the last full audit's but as that sighting or one in this audit.
|
|
72
|
+
class Observed < MemberSighting
|
|
73
|
+
SHAPE_EXAMPLE = "2026-11-01T09:30:00Z" #: String
|
|
74
|
+
# A record that gives only its date gives it at midnight UTC, the observation saying the time is unknown (#785).
|
|
75
|
+
DATE_ALONE_EXAMPLE = "2026-11-01T00:00:00Z" #: String
|
|
76
|
+
OR_NULL_FOR_THIS_AUDIT = "or null for a sighting in this audit." #: String
|
|
77
|
+
# The repair for an observed_at naming no UTC time exactly, which shows the form to follow.
|
|
78
|
+
FORM_REPAIR = %(Give "observed_at" like "#{SHAPE_EXAMPLE}", in UTC, #{OR_NULL_FOR_THIS_AUDIT} If the ) +
|
|
79
|
+
"record gives only a date, give it at 00:00:00Z, such as #{DATE_ALONE_EXAMPLE}, and say in " \
|
|
80
|
+
"observation that the time is unknown." #: String
|
|
81
|
+
|
|
82
|
+
# answered_at is when the answer reporting it came, a UTC time like audited_at, no later than the audit it
|
|
83
|
+
# completes is dated.
|
|
84
|
+
#: (Hash[String, untyped], carried: CarriedAssessments?, answered_at: String, at: AnswerPlace) -> void
|
|
85
|
+
def initialize(assessment, carried:, answered_at:, at:)
|
|
86
|
+
super(assessment, at: at)
|
|
87
|
+
@carried = carried
|
|
88
|
+
@answered_at = answered_at
|
|
89
|
+
freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#: () -> void
|
|
93
|
+
def check
|
|
94
|
+
refuse_missing_observation
|
|
95
|
+
refuse_unshaped_observed_at
|
|
96
|
+
refuse_observed_after_answer
|
|
97
|
+
refuse_repeated_observation
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# An observed delivery needs what was seen (D29: a rule across fields stays in the model).
|
|
103
|
+
#: () -> void
|
|
104
|
+
def refuse_missing_observation
|
|
105
|
+
return if AnswerText.new(@assessment["observation"]).written?
|
|
106
|
+
|
|
107
|
+
raise Error.new("invalid_answer", "Member #{id} is observed but has no observation",
|
|
108
|
+
"Give observation: what you saw the member do, where you saw it, and what you did not see; " \
|
|
109
|
+
"if you did not see it act, record delivery as not_observed.",
|
|
110
|
+
fields: [{ "pointer" => pointer("observation"), "problem" => "must be a nonempty string" }])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# An observed_at is a UTC time in audited_at's form, as the record dates every other sighting, to compare; a
|
|
114
|
+
# date alone, an offset or fractional seconds cannot, nor a time no calendar has (#699). Null or none is this
|
|
115
|
+
# audit's.
|
|
116
|
+
#: () -> void
|
|
117
|
+
def refuse_unshaped_observed_at
|
|
118
|
+
observed_at = @assessment["observed_at"]
|
|
119
|
+
return if observed_at.nil? || AnsweredTime.new(observed_at).utc?
|
|
120
|
+
|
|
121
|
+
raise Error.new("invalid_answer",
|
|
122
|
+
"Member #{id}'s observed_at #{observed_at.to_json} is not a UTC time like audited_at",
|
|
123
|
+
shape_repair(AnsweredTime.new(observed_at)),
|
|
124
|
+
fields: [{ "pointer" => pointer("observed_at"),
|
|
125
|
+
"problem" => "must be a UTC time like #{SHAPE_EXAMPLE}" }])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The repair for an observed_at not in audited_at's form: the value it names exactly, to copy, when that is no
|
|
129
|
+
# later than the answer, which would refuse it in turn; otherwise the form, and how to date a date alone (#801).
|
|
130
|
+
#: (AnsweredTime) -> String
|
|
131
|
+
def shape_repair(given)
|
|
132
|
+
exact = given.exact_utc
|
|
133
|
+
return FORM_REPAIR if exact.nil? || AnsweredTime.new(exact).after?(@answered_at)
|
|
134
|
+
unless given.date_alone?
|
|
135
|
+
return %(Give "observed_at" as "#{exact}", the same time in UTC, #{OR_NULL_FOR_THIS_AUDIT})
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
%(Give "observed_at" as "#{exact}", that date at 00:00:00Z, and say in observation that the time is ) +
|
|
139
|
+
"unknown, #{OR_NULL_FOR_THIS_AUDIT}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A sighting is made before the answer that reports it, so its observed_at is no later than answered_at
|
|
143
|
+
# (#699). Checked once its form is, so the two compare as times.
|
|
144
|
+
#: () -> void
|
|
145
|
+
def refuse_observed_after_answer
|
|
146
|
+
observed_at = @assessment["observed_at"]
|
|
147
|
+
return if observed_at.nil? || !AnsweredTime.new(observed_at).after?(@answered_at)
|
|
148
|
+
|
|
149
|
+
raise Error.new("invalid_answer", "Member #{id}'s observed_at, #{observed_at}, is after this answer",
|
|
150
|
+
%(Give "observed_at" as when you saw it, no later than #{@answered_at}, ) +
|
|
151
|
+
OR_NULL_FOR_THIS_AUDIT,
|
|
152
|
+
fields: [{ "pointer" => pointer("observed_at"),
|
|
153
|
+
"problem" => "must not be after this answer, #{@answered_at}" }])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A fresh observation that repeats the last full audit's word for word describes that audit's sighting or one
|
|
157
|
+
# made in this audit; only the agent knows which, so it gives the recorded date or null, copied from the repair
|
|
158
|
+
# so it never types a clock time (#680, #716). Any other date is another sighting, described in its own words.
|
|
159
|
+
#: () -> void
|
|
160
|
+
def refuse_repeated_observation
|
|
161
|
+
seen = @carried&.repeated_sighting(id, @assessment["observation"]) or return
|
|
162
|
+
return if @assessment.key?("observed_at") && [seen, nil].include?(@assessment["observed_at"])
|
|
163
|
+
|
|
164
|
+
raise Error.new("observation_repeats_last_audit",
|
|
165
|
+
"#{id}'s observation repeats the last full audit's word for word, a sighting dated #{seen}",
|
|
166
|
+
%(Give "observed_at": "#{seen}" if you kept that sighting. If you saw the member again, ) +
|
|
167
|
+
"describe what you saw this time in your own words, with \"observed_at\": null for a " \
|
|
168
|
+
"sighting in this audit, or the date of another, such as a dated record.",
|
|
169
|
+
fields: [{ "pointer" => pointer("observed_at"),
|
|
170
|
+
"problem" => "must be the last full audit's date or null when observation " \
|
|
171
|
+
"repeats its text" }])
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "audited_member_lines"
|
|
5
|
+
require_relative "located_text"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# Each of a map's members by id, where it is in its file now and the hash of what it says there, as
|
|
11
|
+
# Project#snapshots located them all.
|
|
12
|
+
class MemberSnapshots
|
|
13
|
+
#: (Hash[String, Hash[String, untyped]]) -> void
|
|
14
|
+
def initialize(by_id)
|
|
15
|
+
@by_id = by_id.freeze #: Hash[String, Hash[String, untyped]]
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Each member's snapshot by id.
|
|
20
|
+
#: () -> Hash[String, Hash[String, untyped]]
|
|
21
|
+
def to_h = @by_id
|
|
22
|
+
|
|
23
|
+
# Where the member id is in its file now, and the hash of what it says there.
|
|
24
|
+
#: (String) -> LocatedText
|
|
25
|
+
def of(id) = LocatedText.of(@by_id.fetch(id))
|
|
26
|
+
|
|
27
|
+
# Each member's id and the hash of its text now.
|
|
28
|
+
#: () -> Hash[String, String]
|
|
29
|
+
def hashes = @by_id.transform_values { |snapshot| snapshot.fetch("sha256") }
|
|
30
|
+
|
|
31
|
+
# The lines the members span now, against which another principle's evidence is said to share them (D64).
|
|
32
|
+
#: () -> AuditedMemberLines
|
|
33
|
+
def audited_lines = AuditedMemberLines.new(@by_id.transform_values { LocatedText.of(it) })
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
# One member by its id, with its state now as list-member-problems reports it, which a completed audit names for
|
|
8
|
+
# another principle sharing a file its fixes changed (#410, #823).
|
|
9
|
+
class MemberState
|
|
10
|
+
# The states that mean a member's reference is broken, so an audit would refuse it. unreadable_map is a map's
|
|
11
|
+
# state, which MemberProblems#broken reads over its entries; no MemberState holds it, since a map that cannot be
|
|
12
|
+
# read has no members to name.
|
|
13
|
+
BROKEN = %w[locator_lost unavailable unreadable_map].freeze #: Array[String]
|
|
14
|
+
|
|
15
|
+
#: (String, String) -> void
|
|
16
|
+
def initialize(id, state)
|
|
17
|
+
@id = id
|
|
18
|
+
@state = state
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Whether its own text changed or its reference broke, so the principle it belongs to needs auditing again.
|
|
23
|
+
#: () -> bool
|
|
24
|
+
def changed_or_broken? = @state == "changed" || broken?
|
|
25
|
+
|
|
26
|
+
# Whether its reference is broken, so an audit would refuse it.
|
|
27
|
+
#: () -> bool
|
|
28
|
+
def broken? = BROKEN.include?(@state)
|
|
29
|
+
|
|
30
|
+
# The member as printed: its id and state.
|
|
31
|
+
#: () -> Hash[String, String]
|
|
32
|
+
def to_h = { "id" => @id, "state" => @state }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "member_elsewhere"
|
|
5
|
+
require_relative "open_finding"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The members of other principles' maps sharing a line with one located member, as MembersSharingLines finds
|
|
11
|
+
# them: what each result prints of them under mapped_elsewhere, and whether the agent should check the text
|
|
12
|
+
# where their lines meet (#756, #776).
|
|
13
|
+
class MembersElsewhere
|
|
14
|
+
#: (Array[MemberElsewhere]) -> void
|
|
15
|
+
def initialize(members)
|
|
16
|
+
@members = members
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Each as add-member, show-principle and a full audit's request print it under mapped_elsewhere.
|
|
21
|
+
#: () -> Array[Hash[String, untyped]]
|
|
22
|
+
def shown = @members.map(&:to_h)
|
|
23
|
+
|
|
24
|
+
# Whether one of them holds the located member's lines, or lies within them, without either being a whole file.
|
|
25
|
+
#: () -> bool
|
|
26
|
+
def text_check_needed? = @members.any?(&:text_check_needed?)
|
|
27
|
+
|
|
28
|
+
# The principles whose maps hold them, each once.
|
|
29
|
+
#: () -> Array[String]
|
|
30
|
+
def principle_ids = @members.map(&:principle_id).uniq
|
|
31
|
+
|
|
32
|
+
# Each as a full audit's request prints it, naming the findings left_open_by_principle gives its principle that
|
|
33
|
+
# name it (#772, D62).
|
|
34
|
+
#: (Hash[String, Array[OpenFinding]]) -> Array[Hash[String, untyped]]
|
|
35
|
+
def naming(left_open_by_principle)
|
|
36
|
+
@members.map { |member| member.naming(left_open_by_principle.fetch(member.principle_id, [])) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The findings left_open_by_principle gives their principles that name any of them.
|
|
40
|
+
#: (Hash[String, Array[OpenFinding]]) -> Array[OpenFinding]
|
|
41
|
+
def open_among(left_open_by_principle)
|
|
42
|
+
@members.flat_map { |member| member.open_among(left_open_by_principle.fetch(member.principle_id, [])) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "line_span"
|
|
6
|
+
require_relative "located_text"
|
|
7
|
+
require_relative "member"
|
|
8
|
+
require_relative "member_elsewhere"
|
|
9
|
+
require_relative "members_elsewhere"
|
|
10
|
+
|
|
11
|
+
module Principles
|
|
12
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
13
|
+
module Models
|
|
14
|
+
# The members whose text shares a line with one member located in its file, so one edit there shows on
|
|
15
|
+
# each. The project locates the others; a member this tool cannot find now is left out, since show-principle
|
|
16
|
+
# reports it.
|
|
17
|
+
class MembersSharingLines
|
|
18
|
+
# The members sharing a line with member, located where it is now; refuses as Project#locate does when it
|
|
19
|
+
# cannot be found.
|
|
20
|
+
#: (Project, Member) -> MembersSharingLines
|
|
21
|
+
def self.of(project, member)
|
|
22
|
+
new(project, project.located_text(member.locator), id: member.id, whole_file: member.whole_file?)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# located is the text found for one member, its path and lines, id is that member's id, and whole_file whether
|
|
26
|
+
# it is its whole file.
|
|
27
|
+
#: (Project, LocatedText, id: String, ?whole_file: bool) -> void
|
|
28
|
+
def initialize(project, located, id:, whole_file: false)
|
|
29
|
+
@project = project
|
|
30
|
+
@located = located
|
|
31
|
+
@span = located.span(whole_file:) #: LineSpan
|
|
32
|
+
@id = id
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The ids of members of its own map whose text shares a line with the located member, which is not among
|
|
37
|
+
# them. A member of another map may have the same id and is another member, so elsewhere keeps it.
|
|
38
|
+
#: (Array[Member]) -> Array[String]
|
|
39
|
+
def ids_in(members)
|
|
40
|
+
sharing(others(members)).map { |member, _found| member.id }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The ids of members whose text spans exactly the located member's first and last lines, so the agent sees
|
|
44
|
+
# it may be mapping the same lines twice (#442). Equal lines are not equal text: a passage can end within
|
|
45
|
+
# its last line.
|
|
46
|
+
#: (Array[Member]) -> Array[String]
|
|
47
|
+
def same_lines_ids_in(members)
|
|
48
|
+
sharing(others(members)).filter_map { |member, found| member.id if same_lines?(found) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The members sharing a line with the located one, beside map, its own: those in map, those of them on exactly
|
|
52
|
+
# its lines, and those in the other maps, as plain values, with whether the agent should check the text where
|
|
53
|
+
# one of those holds the other's lines without being a whole file (#756).
|
|
54
|
+
#: (MemberMap)
|
|
55
|
+
#| -> { overlaps: Array[String], same_lines_as: Array[String], mapped_elsewhere: Array[Hash[String, untyped]],
|
|
56
|
+
#| text_check: bool }
|
|
57
|
+
def facts_beside(map)
|
|
58
|
+
others = elsewhere(except: map.id)
|
|
59
|
+
members = map.members.to_a
|
|
60
|
+
{ overlaps: ids_in(members), same_lines_as: same_lines_ids_in(members),
|
|
61
|
+
mapped_elsewhere: others.shown, text_check: others.text_check_needed? }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Each member of a map other than except whose text shares a line with the located member, with its map, its
|
|
65
|
+
# role there and its lines, so the agent sees that one edit changes both, checks the roles fit (#383) and how
|
|
66
|
+
# much of the located member it holds, which no other part of the page shows (#610): same_lines when it spans
|
|
67
|
+
# exactly the located member's lines (#442), spans_these_lines when its lines include all of them, and
|
|
68
|
+
# within_these_lines when they include all of its, so the agent can tell which member an edit reaches (#718). A
|
|
69
|
+
# partial overlap is marked neither way. A map that cannot be read is left out; list-member-problems
|
|
70
|
+
# reports it.
|
|
71
|
+
#: (except: String) -> MembersElsewhere
|
|
72
|
+
def elsewhere(except:)
|
|
73
|
+
maps = @project.member_maps
|
|
74
|
+
MembersElsewhere.new((maps.ids - [except]).flat_map { |id| sharing_in(maps, id) })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# The members of map id sharing a line with the located member, or none when that map cannot be read.
|
|
80
|
+
#: (MemberMaps, String) -> Array[MemberElsewhere]
|
|
81
|
+
def sharing_in(maps, id)
|
|
82
|
+
sharing(maps.find(id).members.to_a).map do |member, found|
|
|
83
|
+
MemberElsewhere.new(principle_id: id, member:, span: found.span(whole_file: member.whole_file?),
|
|
84
|
+
located: @span)
|
|
85
|
+
end
|
|
86
|
+
rescue Error
|
|
87
|
+
[]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Each member that shares a line with the located member, with where it is found now: in the same file, by
|
|
91
|
+
# its own path or through a symbolic link (#695).
|
|
92
|
+
#: (Array[Member]) -> Array[[Member, LocatedText]]
|
|
93
|
+
def sharing(members)
|
|
94
|
+
members.filter_map do |member|
|
|
95
|
+
next unless @project.same_file?(member.path, @located.path)
|
|
96
|
+
|
|
97
|
+
found = @project.located_text(member.locator)
|
|
98
|
+
[member, found] if found.span.shares_a_line_with?(@span)
|
|
99
|
+
rescue Error
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The members of its own map but the located one.
|
|
105
|
+
#: (Array[Member]) -> Array[Member]
|
|
106
|
+
def others(members)
|
|
107
|
+
members.reject { |member| member.id == @id }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#: (LocatedText) -> bool
|
|
111
|
+
def same_lines?(found)
|
|
112
|
+
found.span.same_lines_as?(@span)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "activation"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The principle that keeps every other principle held, which setting principles up maps first (D67, D68). It is
|
|
11
|
+
# this product's own principle, not an example, so what its map starts with ships with the tool; once mapped, the
|
|
12
|
+
# project owns the map like any other.
|
|
13
|
+
module MetaPrinciple
|
|
14
|
+
ID = "sticking-to-your-principles"
|
|
15
|
+
|
|
16
|
+
DESCRIPTION = "Every other principle stays held: each place that carries one is mapped, a change to any of " \
|
|
17
|
+
"them is judged against the rest before work goes on, and the agent loads only the guidance its " \
|
|
18
|
+
"task needs, so harness drift and debt stay visible and an agent can be trusted with longer, " \
|
|
19
|
+
"wider unsupervised work."
|
|
20
|
+
|
|
21
|
+
# Its work is in hand in every session, since it brings the other principles back into attention.
|
|
22
|
+
ACTIVATION = Activation.new({ "when" => "always" }) #: Activation
|
|
23
|
+
|
|
24
|
+
# The heading of the passage this tool proposes, which the add-member command it prints names as the section.
|
|
25
|
+
PASSAGE_HEADING = "Stick to your principles"
|
|
26
|
+
|
|
27
|
+
# The part the passage plays as the principle's first member. It carries no check: a command stored in the
|
|
28
|
+
# committed map runs on every machine that reads it, where principles may not be installed or may run from
|
|
29
|
+
# another path (D71).
|
|
30
|
+
PASSAGE_ROLE = "States the practice of keeping every principle held, in every session"
|
|
31
|
+
|
|
32
|
+
# The part a passage the project already has plays when it carries one of the passage's steps in its place,
|
|
33
|
+
# so the step is said once (#895, #911).
|
|
34
|
+
STEP_ROLE = "Says, for this project, one of the keeping steps"
|
|
35
|
+
|
|
36
|
+
# The part any other place that keeps the principles held plays as a member, such as a timer or the committed
|
|
37
|
+
# setting that enables the plugin: the agent replaces STEP with the keeping step it runs and TRIGGER with what
|
|
38
|
+
# starts it, a hook, a timer or a reader (D75).
|
|
39
|
+
KEEPING_PLACE_ROLE = "Runs STEP, started by TRIGGER"
|
|
40
|
+
|
|
41
|
+
# The repair line that sets this principle up, for a refusal of its id before its map exists: setup writes no
|
|
42
|
+
# map, and the add-member it prints creates the map with its first member (T74).
|
|
43
|
+
SET_UP = "To set it up: #{Error::EXECUTABLE} show-meta-principle-passage".freeze #: String
|
|
44
|
+
|
|
45
|
+
# The fields this principle's map starts with as setup starts it, with no members; the add-member setup prints
|
|
46
|
+
# writes the map with its first member (T74).
|
|
47
|
+
#: () -> Hash[String, untyped]
|
|
48
|
+
def self.starting_fields = { "description" => DESCRIPTION, "members" => [], "activation" => ACTIVATION.to_h }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
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 that broken members name but that cannot be read, each with the members it held, so one rename
|
|
11
|
+
# reads as one new path to find (#273). Each member still takes its own move: whether every one of them
|
|
12
|
+
# landed in the same place is the agent's judgement. Beside a path, git_detected_rename gives any rename git
|
|
13
|
+
# detects for it, which the agent checks before using (#276).
|
|
14
|
+
class MissingFiles
|
|
15
|
+
# How to read a git_detected_rename, wherever this tool gives one beside a missing file.
|
|
16
|
+
RENAME_MATCH = "check before you use it: its similarity is git's percentage of the old file found in the " \
|
|
17
|
+
"new one, and 50 is git's default cut-off for calling it a rename."
|
|
18
|
+
# What a broken_members repair says of missing_files, so each refusal says it the same way.
|
|
19
|
+
REPAIR = "missing_files groups the members of each file that cannot be read: a renamed file usually takes " \
|
|
20
|
+
"one new path for all of them. git_detected_rename, when given, is git's match for that path, to " \
|
|
21
|
+
"#{RENAME_MATCH}".freeze #: String
|
|
22
|
+
# broken holds each broken member's entry: its "id", any "principle", and its refusal's "code" and "path".
|
|
23
|
+
#: (Array[Hash[String, untyped]], ?principle: String?, ?history: ProjectHistory?) -> void
|
|
24
|
+
def initialize(broken, principle: nil, history: nil)
|
|
25
|
+
@broken = broken
|
|
26
|
+
@principle = principle
|
|
27
|
+
@history = history
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The broken members, each whose file is missing with a repair that points to its missing_files entry, so
|
|
32
|
+
# a reader of the member alone sees any rename git detected (#285).
|
|
33
|
+
#: () -> Array[Hash[String, untyped]]
|
|
34
|
+
def members
|
|
35
|
+
@broken.map do |entry|
|
|
36
|
+
next entry unless missing?(entry)
|
|
37
|
+
|
|
38
|
+
entry.merge("repair" => "Its file, #{entry.fetch("path")}, is missing: see that path in missing_files, " \
|
|
39
|
+
"with any rename git detected, then run a command from repair_with.")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The broken_members refusal for these members, saying how to repair each. note comes before what
|
|
44
|
+
# missing_files says, and rerun names what to run once they are repaired.
|
|
45
|
+
# rerun is the command to run once the members are repaired, or nil when it is the one the agent ran.
|
|
46
|
+
#: (String, ?rerun: String?, ?note: String) -> Error
|
|
47
|
+
def refusal(message, rerun: nil, note: "")
|
|
48
|
+
Error.new("broken_members", message,
|
|
49
|
+
"For each member listed, say whether it moved or is gone, and run the matching command in its " \
|
|
50
|
+
"repair_with: the move-member, with its NEW_PATH, HEADING or QUERY filled in, if it moved, or the " \
|
|
51
|
+
"remove-member if it is gone. #{note}#{REPAIR}" \
|
|
52
|
+
"#{rerun ? "\nThen run it again: #{rerun}" : " Then run the command again."}")
|
|
53
|
+
.including(**({ "principle" => @principle } if @principle), "members" => members, "missing_files" => to_a)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Each missing path, in the order first named, with the members that name it.
|
|
57
|
+
#: () -> Array[Hash[String, untyped]]
|
|
58
|
+
def to_a
|
|
59
|
+
missing = @broken.select { |entry| missing?(entry) }
|
|
60
|
+
missing.group_by { |entry| entry.fetch("path") }.map do |path, entries|
|
|
61
|
+
{ "path" => path, "members" => entries.map { |entry| named(entry) } }.merge(renamed(path))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
#: (Hash[String, untyped]) -> bool
|
|
68
|
+
def missing?(entry)
|
|
69
|
+
entry["code"] == "unreadable_file" && !entry["path"].nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# git_detected_rename for path, when git detects one.
|
|
73
|
+
#: (String) -> Hash[String, untyped]
|
|
74
|
+
def renamed(path)
|
|
75
|
+
rename = @history&.rename_of(path)
|
|
76
|
+
rename ? { "git_detected_rename" => rename } : {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#: (Hash[String, untyped]) -> Hash[String, String]
|
|
80
|
+
def named(entry)
|
|
81
|
+
{ "principle" => entry["principle"] || @principle, "id" => entry["id"] }.compact
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# A member id an answer named that the map does not hold. A member id copies its heading exactly, so
|
|
8
|
+
# an agent typing it may use a straight apostrophe for a curly one, a hyphen for a dash, or other
|
|
9
|
+
# case and spacing (alpha 011 friction 12). This finds the mapped id it differs from only so, to name
|
|
10
|
+
# in the refusal; this tool never accepts the misspelt id itself.
|
|
11
|
+
class MisspeltMemberId
|
|
12
|
+
APOSTROPHES = /[‘’‚‛′ʼ`´]/
|
|
13
|
+
QUOTES = /[“”„‟″]/
|
|
14
|
+
DASHES = /[‐-―−]/
|
|
15
|
+
|
|
16
|
+
#: (String) -> void
|
|
17
|
+
def initialize(id)
|
|
18
|
+
@id = id
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The one mapped id spelled like this one apart from punctuation, case and spacing, or nil.
|
|
23
|
+
#: (Array[String]) -> String?
|
|
24
|
+
def closest(mapped)
|
|
25
|
+
matches = mapped.select { |each| loose(each) == loose(@id) }
|
|
26
|
+
matches.first if matches.one?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# What a refusal says of this id: that it is not mapped, and the mapped id it is spelled like; nil when none.
|
|
30
|
+
#: (Array[String]) -> String?
|
|
31
|
+
def hint(mapped)
|
|
32
|
+
closest = closest(mapped) or return nil
|
|
33
|
+
"#{@id} is not mapped; did you mean #{closest}?"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
#: (String) -> String
|
|
39
|
+
def loose(text)
|
|
40
|
+
text.unicode_normalize(:nfkc).gsub(APOSTROPHES, "'").gsub(QUOTES, '"').gsub(DASHES, "-")
|
|
41
|
+
.downcase.split.join(" ")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|