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,171 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "locator"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# One member of a principle as its map records it: a file, a Markdown section of it, or a passage
|
|
11
|
+
# starting at a quoted first line. Its id names all three, so an agent can read where the member is
|
|
12
|
+
# from the id alone.
|
|
13
|
+
class Member
|
|
14
|
+
# The Unicode names of the format characters and separators most often met in text, by code point.
|
|
15
|
+
# Ruby has no table of names, so a character missing here is given by its code point alone.
|
|
16
|
+
HIDDEN_CHARACTER_NAMES = {
|
|
17
|
+
0x00AD => "SOFT HYPHEN", 0x061C => "ARABIC LETTER MARK", 0x180E => "MONGOLIAN VOWEL SEPARATOR",
|
|
18
|
+
0x200B => "ZERO WIDTH SPACE", 0x200C => "ZERO WIDTH NON-JOINER", 0x200D => "ZERO WIDTH JOINER",
|
|
19
|
+
0x200E => "LEFT-TO-RIGHT MARK", 0x200F => "RIGHT-TO-LEFT MARK", 0x2028 => "LINE SEPARATOR",
|
|
20
|
+
0x2029 => "PARAGRAPH SEPARATOR", 0x202A => "LEFT-TO-RIGHT EMBEDDING", 0x202B => "RIGHT-TO-LEFT EMBEDDING",
|
|
21
|
+
0x202C => "POP DIRECTIONAL FORMATTING", 0x202D => "LEFT-TO-RIGHT OVERRIDE",
|
|
22
|
+
0x202E => "RIGHT-TO-LEFT OVERRIDE", 0x2060 => "WORD JOINER", 0x2066 => "LEFT-TO-RIGHT ISOLATE",
|
|
23
|
+
0x2067 => "RIGHT-TO-LEFT ISOLATE", 0x2068 => "FIRST STRONG ISOLATE", 0x2069 => "POP DIRECTIONAL ISOLATE",
|
|
24
|
+
0xFEFF => "ZERO WIDTH NO-BREAK SPACE"
|
|
25
|
+
}.freeze #: Hash[Integer, String]
|
|
26
|
+
|
|
27
|
+
# The map format each member field needs: a member using a field is unreadable, without loss, by a
|
|
28
|
+
# version older than its format. Every field the member-map schema names is listed; a spec checks it.
|
|
29
|
+
# A new member field that changes what an audit asks or records gets the next format, and
|
|
30
|
+
# Data::MapFiles::NEWEST_FORMAT moves with it.
|
|
31
|
+
FIELD_FORMATS = { "path" => 1, "section" => 1, "starting_at" => 1, "ending_at" => 1, "role" => 1,
|
|
32
|
+
"key" => 1, "check" => 2, "query" => 2 }.freeze #: Hash[String, Integer]
|
|
33
|
+
|
|
34
|
+
# The fields that say where a member's text is; a move replaces them whole.
|
|
35
|
+
LOCATORS = %w[path section starting_at ending_at query].freeze #: [String, String, String, String, String]
|
|
36
|
+
|
|
37
|
+
# fields is copied and frozen, so no caller changes a member through the hash it was built from or handed;
|
|
38
|
+
# the copy is shallow, so its strings are the caller's. MemberMap builds its members from strings it has
|
|
39
|
+
# copied and frozen, so a member in a map shares nothing (#654).
|
|
40
|
+
#: (Hash[String, untyped]) -> void
|
|
41
|
+
def initialize(fields)
|
|
42
|
+
@fields = fields.dup.freeze #: Hash[String, untyped]
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The member's fields, as its map's file holds them.
|
|
47
|
+
#: () -> Hash[String, untyped]
|
|
48
|
+
def to_h = @fields
|
|
49
|
+
|
|
50
|
+
# path, path#section or path$query, or either of the first two followed by @ and the passage's first-line
|
|
51
|
+
# anchor.
|
|
52
|
+
#: () -> String
|
|
53
|
+
def id
|
|
54
|
+
id = @fields["section"] ? "#{@fields.fetch("path")}##{@fields["section"]}" : @fields.fetch("path")
|
|
55
|
+
id = "#{id}#{@fields["query"]}" if @fields["query"]
|
|
56
|
+
@fields["starting_at"] ? "#{id}@#{@fields["starting_at"]}" : id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# What the member is known by across moves: its stored key, which move-member keeps from its id before
|
|
60
|
+
# its first move, or else its id. Audit records file each member's hash under it.
|
|
61
|
+
#: () -> String
|
|
62
|
+
def key
|
|
63
|
+
@fields["key"] || id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The move-member options that place a member of this kind, with placeholders to fill: a section's heading,
|
|
67
|
+
# a passage's anchors or a query, or else the whole file's path. Run as printed, a placeholder fails, where a
|
|
68
|
+
# bare --path would silently widen a section or passage to its whole file.
|
|
69
|
+
#: () -> String
|
|
70
|
+
def move_placeholders
|
|
71
|
+
placed = { "section" => "--section HEADING", "starting_at" => "--starting-at TEXT",
|
|
72
|
+
"ending_at" => "--ending-at TEXT", "query" => "--query QUERY" }
|
|
73
|
+
.filter_map { |field, option| option if @fields[field] }
|
|
74
|
+
placed.empty? ? "--path PATH" : placed.join(" ")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The oldest map format that holds this member: 1 unless it uses a later field.
|
|
78
|
+
#: () -> Integer
|
|
79
|
+
def map_format
|
|
80
|
+
@fields.keys.map { |field| FIELD_FORMATS.fetch(field, 1) }.max || 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The file the member is in.
|
|
84
|
+
#: () -> String
|
|
85
|
+
def path
|
|
86
|
+
@fields.fetch("path")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Whether it is its whole file, which no locator but its path narrows, so it holds all the text of every
|
|
90
|
+
# line it spans (#756).
|
|
91
|
+
#: () -> bool
|
|
92
|
+
def whole_file?
|
|
93
|
+
(LOCATORS - ["path"]).none? { |locator| @fields.key?(locator) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# What the member does for its principle, as the map states it.
|
|
97
|
+
#: () -> String?
|
|
98
|
+
def role
|
|
99
|
+
@fields["role"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The command that enforces the member, which an audit asks the agent to run (D30); nil when it has none.
|
|
103
|
+
#: () -> String?
|
|
104
|
+
def check
|
|
105
|
+
@fields["check"]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Each invisible formatting or direction-changing character, or line separator, the member's check
|
|
109
|
+
# holds, in order, as its code point, its name when HIDDEN_CHARACTER_NAMES has it, and its position
|
|
110
|
+
# counted in characters from 1; none when it has no check. This tool prints such characters escaped, and
|
|
111
|
+
# naming them helps a reviewer who scans the command quickly (#258, #260).
|
|
112
|
+
#: () -> Array[String]
|
|
113
|
+
def check_hidden_characters
|
|
114
|
+
check.to_s.each_char.with_index(1).filter_map do |character, position|
|
|
115
|
+
next unless character.match?(/[\p{Cf}\p{Zl}\p{Zp}]/)
|
|
116
|
+
|
|
117
|
+
code = character.ord
|
|
118
|
+
[format("U+%04X", code), HIDDEN_CHARACTER_NAMES[code], "at character #{position}"].compact.join(" ")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Where the member's text is, as MemberFile finds it.
|
|
123
|
+
#: () -> Hash[String, untyped]
|
|
124
|
+
def reference
|
|
125
|
+
{ "path" => @fields.fetch("path"), "heading" => @fields["section"],
|
|
126
|
+
"starting_at" => @fields["starting_at"], "ending_at" => @fields["ending_at"],
|
|
127
|
+
"query" => @fields["query"] }.compact
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Where the member's text is, as the Locator MemberFile reads.
|
|
131
|
+
#: () -> Locator
|
|
132
|
+
def locator = Locator.new(reference)
|
|
133
|
+
|
|
134
|
+
# This member, still known by key once it has moved. The key is stored only while it differs from the
|
|
135
|
+
# member's id, so a member moved back to where it began has the bytes it had.
|
|
136
|
+
#: (String) -> Member
|
|
137
|
+
def keeping_key(key)
|
|
138
|
+
without = Member.new(@fields.except("key"))
|
|
139
|
+
without.id == key ? without : without.keyed_as(key)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# This member pointed at the place located names: the new place replaces the old one whole, every other
|
|
143
|
+
# field is kept, and the member is still known by its key.
|
|
144
|
+
#: (Hash[String, untyped]) -> Member
|
|
145
|
+
def moved_to(located)
|
|
146
|
+
Member.new(located.merge(@fields.except(*LOCATORS))).keeping_key(key)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# This member known by key, as a member added where another moved away from keeps its own audit.
|
|
150
|
+
#: (String) -> Member
|
|
151
|
+
def keyed_as(key) = with("key" => key)
|
|
152
|
+
|
|
153
|
+
# This member with fields in place of its own.
|
|
154
|
+
#: (Hash[String, untyped]) -> Member
|
|
155
|
+
def with(fields) = Member.new(@fields.merge(fields))
|
|
156
|
+
|
|
157
|
+
# This member with no check.
|
|
158
|
+
#: () -> Member
|
|
159
|
+
def without_check = Member.new(@fields.except("check"))
|
|
160
|
+
|
|
161
|
+
# Refuses a place an agent gave that quotes where a passage ends but not where it starts.
|
|
162
|
+
#: () -> void
|
|
163
|
+
def check_location
|
|
164
|
+
return unless @fields["ending_at"] && !@fields["starting_at"]
|
|
165
|
+
|
|
166
|
+
raise Error.new("ending_without_start", "--ending-at needs --starting-at",
|
|
167
|
+
"Quote the passage's opening with --starting-at too.")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "locator"
|
|
6
|
+
require_relative "answer_fields"
|
|
7
|
+
require_relative "answer_place"
|
|
8
|
+
require_relative "carried_assessments"
|
|
9
|
+
require_relative "map_members"
|
|
10
|
+
require_relative "member_sighting"
|
|
11
|
+
require_relative "misspelt_member_id"
|
|
12
|
+
require_relative "record_judged_against"
|
|
13
|
+
require_relative "reported_check"
|
|
14
|
+
|
|
15
|
+
module Principles
|
|
16
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
17
|
+
module Models
|
|
18
|
+
# The member_assessments of an assessment answer: one for each mapped member, each with an assessment, a delivery,
|
|
19
|
+
# an observation when delivery is observed, evidence that resolves, and the result of the member's check when it
|
|
20
|
+
# has one (D30).
|
|
21
|
+
class MemberAssessments
|
|
22
|
+
# What a record entry keeps for a member carried forward, until completion puts back its last assessment.
|
|
23
|
+
CARRIED = { "carried_forward" => true }.freeze #: Hash[String, untyped]
|
|
24
|
+
|
|
25
|
+
# assessments is the answer's member_assessments; members are the mapped members, with each checked one's
|
|
26
|
+
# check; fields checks each field and locates each piece of evidence; judged_against is the principle's record
|
|
27
|
+
# as it stands, which says which members' last assessments may be carried forward, marked {"member_id",
|
|
28
|
+
# "carried_forward": true} (D49), and which command it ran for each member's check (D63); answered_at is when
|
|
29
|
+
# the answer came, which no sighting it reports is after (#699).
|
|
30
|
+
#: (untyped, MapMembers, fields: AnswerFields, answered_at: String, ?judged_against: RecordJudgedAgainst?)
|
|
31
|
+
#| -> void
|
|
32
|
+
def initialize(assessments, members, fields:, answered_at:, judged_against: nil)
|
|
33
|
+
@assessments = assessments
|
|
34
|
+
@members = members
|
|
35
|
+
@answered_at = answered_at
|
|
36
|
+
@fields = fields
|
|
37
|
+
@judged_against = judged_against
|
|
38
|
+
@carried = judged_against&.carried #: CarriedAssessments?
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Refuses the assessments unless each mapped member is assessed exactly once, and each assessment is complete.
|
|
43
|
+
#: () -> void
|
|
44
|
+
def check
|
|
45
|
+
assessments = @fields.array(@assessments, "member_assessments")
|
|
46
|
+
refuse_incomplete_coverage(assessments.map { |assessment| assessment["member_id"] })
|
|
47
|
+
assessments.each_with_index { |assessment, index| member_assessment(assessment, place_of(assessment, index)) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The result reported for each checked member's check, by member id, once each has been checked. Call it after
|
|
51
|
+
# check, which refuses an unmapped id first: finding the command the record ran needs each member's key.
|
|
52
|
+
#: () -> Hash[String, Hash[String, untyped]]
|
|
53
|
+
def check_results
|
|
54
|
+
@assessments.each_with_index.to_h do |assessment, index|
|
|
55
|
+
[assessment["member_id"], reported(assessment, place_of(assessment, index))]
|
|
56
|
+
end.compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Each member's assessment by id, as its record entry keeps it: what the agent said, and the hash of each piece
|
|
60
|
+
# of evidence it cited, which the block locates, so the next audit can say whether that evidence changed (D49).
|
|
61
|
+
#: () { (Locator) -> Hash[String, untyped] } -> Hash[String, Hash[String, untyped]]
|
|
62
|
+
def kept(&locate)
|
|
63
|
+
@assessments.to_h do |assessment|
|
|
64
|
+
next [assessment.fetch("member_id"), { "assessment" => CARRIED }] if carried_forward?(assessment)
|
|
65
|
+
|
|
66
|
+
hashes = assessment.fetch("evidence").map { |reference| locate.call(Locator.new(reference)).fetch("sha256") }
|
|
67
|
+
[assessment.fetch("member_id"),
|
|
68
|
+
{ "assessment" => assessment.slice("assessment", "delivery", "observation", "observed_at", "evidence")
|
|
69
|
+
.reject { |key, value| key == "observed_at" && value.nil? }
|
|
70
|
+
.merge("evidence_sha256" => hashes) }]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Refuses answered, the ids the assessments name, unless they are the mapped ids, each once, hinting at any
|
|
77
|
+
# mapped id an unmapped one is spelled like.
|
|
78
|
+
#: (Array[untyped]) -> void
|
|
79
|
+
def refuse_incomplete_coverage(answered)
|
|
80
|
+
ids = @members.ids
|
|
81
|
+
return if answered.sort == ids.sort
|
|
82
|
+
|
|
83
|
+
raise Error.new("incomplete_coverage", "Assess each member exactly once: #{ids.join(", ")}",
|
|
84
|
+
[*(answered - ids).grep(String).filter_map { |id| MisspeltMemberId.new(id).hint(ids) },
|
|
85
|
+
"Complete the member_assessments array, copying each member id exactly."].join(" "))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The place of the assessment at index in the answer, whose fields each refusal about it points into.
|
|
89
|
+
#: (untyped, Integer) -> AnswerPlace
|
|
90
|
+
def place_of(assessment, index)
|
|
91
|
+
AnswerPlace.new("/member_assessments/#{index}", "Member #{assessment["member_id"]}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#: (untyped, AnswerPlace) -> void
|
|
95
|
+
def member_assessment(assessment, place)
|
|
96
|
+
return carried_assessment(assessment, place) if carried_forward?(assessment)
|
|
97
|
+
|
|
98
|
+
@fields.text(assessment["assessment"], "member_assessment.assessment")
|
|
99
|
+
sighting = MemberSighting.for(assessment, carried: @carried, answered_at: @answered_at, at: place)
|
|
100
|
+
@fields.evidence(assessment["evidence"], place.child("evidence"))
|
|
101
|
+
sighting.check
|
|
102
|
+
reported(assessment, place)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Whether the assessment is a marker carrying forward the member's last one.
|
|
106
|
+
#: (untyped) -> bool
|
|
107
|
+
def carried_forward?(assessment) = assessment.key?("carried_forward")
|
|
108
|
+
|
|
109
|
+
# A marker is accepted only for a member the request listed as carried, alone but for its check's result.
|
|
110
|
+
#: (Hash[String, untyped], AnswerPlace) -> void
|
|
111
|
+
def carried_assessment(assessment, place)
|
|
112
|
+
reason = marker_refusal(assessment) or return reported(assessment, place)
|
|
113
|
+
|
|
114
|
+
raise Error.new("invalid_answer", "#{assessment["member_id"]} cannot be carried forward: #{reason}",
|
|
115
|
+
"Assess it as the request's template does, or give only {\"member_id\", " \
|
|
116
|
+
"\"carried_forward\": true} for a member the request lists with carried_forward.",
|
|
117
|
+
fields: [{ "pointer" => place.child("carried_forward").pointer, "problem" => reason }])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Why a marker cannot carry the member's assessment forward, or nil when it can: the request asks to judge the
|
|
121
|
+
# member, giving why, or the marker is not true or comes with an assessment.
|
|
122
|
+
#: (Hash[String, untyped]) -> String?
|
|
123
|
+
def marker_refusal(assessment)
|
|
124
|
+
return "carried_forward must be true" unless assessment["carried_forward"] == true
|
|
125
|
+
|
|
126
|
+
assessed = assessment.keys.intersect?(%w[assessment delivery evidence])
|
|
127
|
+
return "give the marker or an assessment, not both" if assessed
|
|
128
|
+
|
|
129
|
+
reason = @carried ? @carried.judge_because(assessment["member_id"]) : "not_assessed_before"
|
|
130
|
+
"the request asks to judge it: judge_because #{reason}" if reason
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The result reported for the member assessed at place, checked against its check.
|
|
134
|
+
#: (untyped, AnswerPlace) -> Hash[String, untyped]?
|
|
135
|
+
def reported(assessment, place)
|
|
136
|
+
id = assessment["member_id"]
|
|
137
|
+
key = @members.found(id).key
|
|
138
|
+
ReportedCheck.new(place, @members.check(id), ran: @judged_against&.check_run_for(key),
|
|
139
|
+
kept: @judged_against&.check_review_for(key) || {})
|
|
140
|
+
.result(assessment["check_result"])
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "line_span"
|
|
5
|
+
require_relative "member"
|
|
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
|
+
# A member of another principle's map whose text shares a line with one located member: its principle, id,
|
|
11
|
+
# role there and lines, and how its lines stand to the located member's: the same lines, all of them, within
|
|
12
|
+
# them, or none of these, for a partial overlap (#383, #442, #610, #718). These are lines, not text, so what an
|
|
13
|
+
# edit reaches is left to the agent.
|
|
14
|
+
class MemberElsewhere
|
|
15
|
+
# member is the other map's member, whose id and role are printed; span is its lines now.
|
|
16
|
+
#: (principle_id: String, member: Member, span: LineSpan, located: LineSpan) -> void
|
|
17
|
+
def initialize(principle_id:, member:, span:, located:)
|
|
18
|
+
@principle_id = principle_id
|
|
19
|
+
@member = member
|
|
20
|
+
@span = span
|
|
21
|
+
@relation = relation_to(located) #: String?
|
|
22
|
+
@text_check_needed = text_check_needed_against?(located) #: bool
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether the agent should check the text where the lines meet before deciding which edit reaches which
|
|
27
|
+
# member: one holds the other's lines, and the one holding them is not a whole file (#756).
|
|
28
|
+
#: () -> bool
|
|
29
|
+
def text_check_needed? = @text_check_needed
|
|
30
|
+
|
|
31
|
+
# The principle whose map holds it.
|
|
32
|
+
#: String
|
|
33
|
+
attr_reader :principle_id
|
|
34
|
+
|
|
35
|
+
# The findings among left_open, its principle's open findings, that name it (#772).
|
|
36
|
+
#: (Array[OpenFinding]) -> Array[OpenFinding]
|
|
37
|
+
def open_among(left_open) = left_open.select { |finding| finding.on?(@member.key) }
|
|
38
|
+
|
|
39
|
+
# Its entry, naming by id the findings among left_open that name it, which the request lists once, so the same
|
|
40
|
+
# problem is not raised again in another principle's audit (#772, D62); as to_h when none do.
|
|
41
|
+
#: (Array[OpenFinding]) -> Hash[String, untyped]
|
|
42
|
+
def naming(left_open)
|
|
43
|
+
on_it = open_among(left_open)
|
|
44
|
+
on_it.empty? ? to_h : to_h.merge("open_finding_ids" => on_it.map(&:id))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# As add-member, show-principle and a full audit's request print it under mapped_elsewhere, its relation marked
|
|
48
|
+
# true when it has one.
|
|
49
|
+
#: () -> Hash[String, untyped]
|
|
50
|
+
def to_h
|
|
51
|
+
{ "principle_id" => @principle_id, "member_id" => @member.id, "role" => @member.role,
|
|
52
|
+
"first_line" => @span.first_line, "last_line" => @span.last_line, **(@relation ? { @relation => true } : {}) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
#: (LineSpan) -> String?
|
|
58
|
+
def relation_to(located)
|
|
59
|
+
return "same_lines" if @span.same_lines_as?(located)
|
|
60
|
+
return "spans_these_lines" if @span.holds?(located)
|
|
61
|
+
|
|
62
|
+
"within_these_lines" if located.holds?(@span)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: (LineSpan) -> bool
|
|
66
|
+
def text_check_needed_against?(located)
|
|
67
|
+
return !@span.holds_all_text_of?(located) if @relation == "spans_these_lines"
|
|
68
|
+
return !located.holds_all_text_of?(@span) if @relation == "within_these_lines"
|
|
69
|
+
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
require_relative "locator"
|
|
7
|
+
require_relative "write_up_opening"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# The text of one project file, as the type that knows how to find a member in it. It holds text, not a
|
|
13
|
+
# file, so the same rules apply to a file on disk and to the file at a commit. Each type finds the region
|
|
14
|
+
# a member names; passage anchors then narrow it the same way in every type.
|
|
15
|
+
class MemberFile
|
|
16
|
+
# The refusals, raised here and by the file types, that say a member's reference finds nothing in a file's
|
|
17
|
+
# text, rather than that the reference itself is wrong. Add a new one here when it raises one (#457).
|
|
18
|
+
PLACEMENT_REFUSALS = %w[section_not_found ambiguous_heading passage_not_found ambiguous_passage
|
|
19
|
+
passage_end_not_found section_needs_markdown query_needs_frontmatter field_not_found
|
|
20
|
+
unsupported_frontmatter duplicate_frontmatter_key].freeze
|
|
21
|
+
# The placement refusals of a quote the text does not hold, which a pointer that is not a member may also
|
|
22
|
+
# repair by pointing at other text (#742).
|
|
23
|
+
QUOTE_NOT_FOUND = %w[passage_not_found passage_end_not_found].freeze #: Array[String]
|
|
24
|
+
# The placement refusals that mean a member's place in its file is gone: its heading, passage anchor or
|
|
25
|
+
# frontmatter field no longer matches, so it is lost there, not changed and not unreadable.
|
|
26
|
+
PLACE_LOST = %w[section_not_found passage_not_found passage_end_not_found ambiguous_heading
|
|
27
|
+
ambiguous_passage field_not_found].freeze #: Array[String]
|
|
28
|
+
|
|
29
|
+
# The file name endings read as Markdown, where a # line is a heading and not a comment.
|
|
30
|
+
MARKDOWN = /\.(md|markdown|mdx)\z/i #: Regexp
|
|
31
|
+
|
|
32
|
+
# The file type for path, chosen by its name.
|
|
33
|
+
#: (path: String, text: String) -> MemberFile
|
|
34
|
+
def self.for(path:, text:)
|
|
35
|
+
(path.match?(MARKDOWN) ? MarkdownFile : PlainTextFile).new(path: path, text: text)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: String
|
|
39
|
+
attr_reader :path
|
|
40
|
+
#: String
|
|
41
|
+
attr_reader :text
|
|
42
|
+
|
|
43
|
+
#: (path: String, text: String) -> void
|
|
44
|
+
def initialize(path:, text:)
|
|
45
|
+
@path = path
|
|
46
|
+
@text = text
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Where the member locator names is in this text, and the hash of what it says there.
|
|
51
|
+
#: (Locator) -> Hash[String, untyped]
|
|
52
|
+
def locate(locator)
|
|
53
|
+
refuse_mixed_locators(locator)
|
|
54
|
+
lines = @text.lines
|
|
55
|
+
first, last = region(lines, locator)
|
|
56
|
+
selected = lines[first...last].to_a.join
|
|
57
|
+
start_text = locator.starting_at
|
|
58
|
+
first, last, selected = passage(lines, first, selected, start_text, locator.ending_at) if start_text
|
|
59
|
+
snapshot(locator, selected, first + 1, last)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The lines of the region locator names, as the agent reads them (D33).
|
|
63
|
+
#: (Locator) -> Array[String]
|
|
64
|
+
def lines_of(locator)
|
|
65
|
+
located = locate(locator)
|
|
66
|
+
@text.lines[(located.fetch("first_line") - 1)...located.fetch("last_line")].to_a
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The opening of the region locator names, after the heading line itself when it names a heading, as
|
|
70
|
+
# WriteUpOpening finds it; nil when there is none. This tool does not judge what it says (#300).
|
|
71
|
+
#: (Locator) -> Hash[String, untyped]?
|
|
72
|
+
def write_up_opening(locator)
|
|
73
|
+
located = locate(locator)
|
|
74
|
+
heading_line = locator.heading && !locator.starting_at ? 1 : 0
|
|
75
|
+
WriteUpOpening.new(lines: @text.lines, first: located.fetch("first_line") - 1 + heading_line,
|
|
76
|
+
last: located.fetch("last_line")).to_h
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# What locate found: the member's place, the hash of its text and its span. query appears only for a
|
|
82
|
+
# member that has one, so every other member's snapshot, and each request id digesting it, is unchanged.
|
|
83
|
+
#: (Locator, String, Integer, Integer) -> Hash[String, untyped]
|
|
84
|
+
def snapshot(locator, selected, first_line, last_line)
|
|
85
|
+
{ "path" => @path, "heading" => locator.heading, "starting_at" => locator.starting_at,
|
|
86
|
+
"sha256" => Digest::SHA256.hexdigest(selected), "characters" => selected.length,
|
|
87
|
+
"first_line" => first_line, "last_line" => last_line }.merge(locator.to_h.slice("query"))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A query names a field whole, so it takes no section or passage beside it.
|
|
91
|
+
#: (Locator) -> void
|
|
92
|
+
def refuse_mixed_locators(locator)
|
|
93
|
+
return unless locator.query && (locator.heading || locator.starting_at)
|
|
94
|
+
|
|
95
|
+
raise Error.new("query_with_other_locators",
|
|
96
|
+
"--query names a whole field, so it takes no --section or --starting-at",
|
|
97
|
+
"Give --query alone, or --section and --starting-at without it.", @path)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The line range a member names before any passage narrows it; each file type says.
|
|
101
|
+
#: (Array[String], Locator) -> [Integer, Integer]
|
|
102
|
+
def region(lines, locator)
|
|
103
|
+
raise NotImplementedError, "#{self.class} names no region"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Narrows a member to a passage of its section, or of the whole text. starting_at must occur exactly
|
|
107
|
+
# once. With ending_at, the passage runs from the start of that match to the end of the first ending_at
|
|
108
|
+
# after it; without, to the end of the line it starts on. Anchors, never line numbers, so the passage
|
|
109
|
+
# stays found while text around and inside it changes. A lost anchor is refused, never widened to the
|
|
110
|
+
# whole file. Returns the passage's first and last lines, counted from the start of the text, and the
|
|
111
|
+
# passage.
|
|
112
|
+
#: (Array[String], Integer, String, String, String?) -> [Integer, Integer, String]
|
|
113
|
+
def passage(lines, first, selected, start_text, end_text)
|
|
114
|
+
start = only_opening(selected, start_text)
|
|
115
|
+
found = selected[start...passage_end(selected, start, start_text, end_text)].to_s
|
|
116
|
+
line = @text[0, lines[0...first].to_a.join.length + start].to_s.count("\n")
|
|
117
|
+
[line, line + line_count(found), found]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# How many lines a passage spans, counting a last line with no line break.
|
|
121
|
+
#: (String) -> Integer
|
|
122
|
+
def line_count(found)
|
|
123
|
+
found.count("\n") + (found.end_with?("\n") ? 0 : 1)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#: (String, String) -> Integer
|
|
127
|
+
def only_opening(text, start_text)
|
|
128
|
+
matches = text.scan(start_text).length
|
|
129
|
+
return text.index(start_text).to_i if matches == 1
|
|
130
|
+
if matches.zero?
|
|
131
|
+
raise Error.new("passage_not_found", "The passage starting #{start_text.inspect} is not in the file",
|
|
132
|
+
"Quote the passage's opening text exactly.", @path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
raise Error.new("ambiguous_passage", "#{start_text.inspect} occurs #{matches} times",
|
|
136
|
+
"Quote more of the passage's start so it occurs once.", @path)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#: (String, Integer, String, String?) -> Integer
|
|
140
|
+
def passage_end(text, start, start_text, end_text)
|
|
141
|
+
return (text.index("\n", start) || (text.length - 1)) + 1 unless end_text
|
|
142
|
+
|
|
143
|
+
found = text.index(end_text, start + start_text.length)
|
|
144
|
+
unless found
|
|
145
|
+
raise Error.new("passage_end_not_found", "No #{end_text.inspect} after #{start_text.inspect}",
|
|
146
|
+
"Quote the passage's closing text exactly.", @path)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
found + end_text.length
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
require_relative "markdown_file"
|
|
156
|
+
require_relative "plain_text_file"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "json_digest"
|
|
5
|
+
require_relative "loading_facts"
|
|
6
|
+
require_relative "map_members"
|
|
7
|
+
require_relative "member_loads"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# The facts beside each member's own text that its assessment was judged on: its map entry, which holds its path,
|
|
13
|
+
# locator, role and check, and when it loads, without the sizes an edit elsewhere in its file moves. A full audit
|
|
14
|
+
# keeps a digest of each with the assessment, and carries the assessment forward only while both match (D49).
|
|
15
|
+
class MemberGrounds
|
|
16
|
+
# members are the map's members; loads is each member's MemberLoads by id.
|
|
17
|
+
#: (MapMembers, Hash[String, MemberLoads]) -> void
|
|
18
|
+
def initialize(members, loads)
|
|
19
|
+
@members = members.by_id #: Hash[String, Member]
|
|
20
|
+
@loads = loads
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The digests of the member id's map entry and of its loading facts, as its kept assessment holds them.
|
|
25
|
+
#: (String) -> Hash[String, String]
|
|
26
|
+
def of(id)
|
|
27
|
+
{ "entry_sha256" => JsonDigest.new(@members.fetch(id).to_h).to_s,
|
|
28
|
+
"loads_sha256" => JsonDigest.new(LoadingFacts.new(@loads.fetch(id).to_h).to_h).to_s }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The commits that changed one member's own text since this tool last knew it, newest first. The walk goes
|
|
8
|
+
# back from HEAD and stops at the first commit whose text for the member hashes to the baseline, so a fix
|
|
9
|
+
# an audit already took in is never listed. It also stops where the member cannot be found, or at the limit.
|
|
10
|
+
# It reports facts; whether a change is accounted for is the agent's judgement.
|
|
11
|
+
class MemberHistory
|
|
12
|
+
# The most commits listed for one member; the walk says when it stopped here.
|
|
13
|
+
LIMIT = 20
|
|
14
|
+
|
|
15
|
+
# commits are the commits that touched the member's file or its map, newest first; hash_at gives the
|
|
16
|
+
# member's hash at a revision, or nil where it cannot be found there.
|
|
17
|
+
#: (baseline: String, now: String, commits: Array[Hash[String, String]], hash_at: ^(String) -> String?) -> void
|
|
18
|
+
def initialize(baseline:, now:, commits:, hash_at:)
|
|
19
|
+
@baseline = baseline
|
|
20
|
+
@now = now
|
|
21
|
+
@commits = commits
|
|
22
|
+
@hash_at = hash_at
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The commits that changed the member's text, where and why the walk stopped, and whether the text on disk
|
|
27
|
+
# differs from HEAD.
|
|
28
|
+
#: () -> Hash[String, untyped]
|
|
29
|
+
def to_h
|
|
30
|
+
listed = [] #: Array[Hash[String, String]]
|
|
31
|
+
stopped = walk(listed)
|
|
32
|
+
{ "commits" => listed, "stopped_at" => stopped, "uncommitted" => @hash_at.call("HEAD") != @now }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
#: (Array[Hash[String, String]]) -> Hash[String, String]
|
|
38
|
+
def walk(listed)
|
|
39
|
+
@commits.each do |commit|
|
|
40
|
+
after = @hash_at.call(commit.fetch("sha"))
|
|
41
|
+
return stop("baseline_matched", commit) if after == @baseline
|
|
42
|
+
return { "reason" => "limit" } if listed.length == LIMIT
|
|
43
|
+
|
|
44
|
+
stopped = step(listed, commit, after)
|
|
45
|
+
return stopped if stopped
|
|
46
|
+
end
|
|
47
|
+
{ "reason" => "history_start" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Lists commit when it changed the member's text, and says why the walk stops at it, if it does: a root
|
|
51
|
+
# commit has no parent, and a parent without the member ends what can be tied to it.
|
|
52
|
+
#: (Array[Hash[String, String]], Hash[String, String], String?) -> Hash[String, String]?
|
|
53
|
+
def step(listed, commit, after)
|
|
54
|
+
root = commit.fetch("root") == "true"
|
|
55
|
+
before = root ? nil : @hash_at.call("#{commit.fetch("sha")}^")
|
|
56
|
+
listed << commit.except("sha", "root") if before != after
|
|
57
|
+
return { "reason" => "history_start" } if root
|
|
58
|
+
|
|
59
|
+
stop("member_not_found", commit) unless before
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: (String, Hash[String, String]) -> Hash[String, String]
|
|
63
|
+
def stop(reason, commit)
|
|
64
|
+
{ "reason" => reason, "commit" => commit.fetch("commit") }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|