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,45 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "member_loads"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# What a member's loads say about when it loads and what, at any depth, without the file's or the member's size.
|
|
10
|
+
# A file's size moves with every edit elsewhere in a shared file, so a judgement weighed on a member's loading
|
|
11
|
+
# compares these facts, not the sizes (second alpha user on 5bedec4, #515).
|
|
12
|
+
class LoadingFacts
|
|
13
|
+
# loads is a member's loads as MemberLoads#to_h gives them, or as a record keeps them.
|
|
14
|
+
#: (Hash[String, untyped]) -> void
|
|
15
|
+
def initialize(loads)
|
|
16
|
+
@facts = without_sizes(loads) #: untyped
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: () -> untyped
|
|
21
|
+
def to_h = @facts
|
|
22
|
+
|
|
23
|
+
#: (untyped) -> bool
|
|
24
|
+
def ==(other) = other.is_a?(LoadingFacts) && other.to_h == @facts
|
|
25
|
+
|
|
26
|
+
alias eql? ==
|
|
27
|
+
|
|
28
|
+
#: () -> Integer
|
|
29
|
+
def hash = @facts.hash
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
#: (untyped) -> untyped
|
|
34
|
+
def without_sizes(value)
|
|
35
|
+
case value
|
|
36
|
+
when Hash
|
|
37
|
+
value.except(*MemberLoads::FILE_SIZE_FIELDS, *MemberLoads::MEMBER_SIZE_FIELDS)
|
|
38
|
+
.transform_values { |each| without_sizes(each) }
|
|
39
|
+
when Array then value.map { |each| without_sizes(each) }
|
|
40
|
+
else value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "member_loads"
|
|
6
|
+
require_relative "loading_decision"
|
|
7
|
+
require_relative "accepted_loading"
|
|
8
|
+
require_relative "activation"
|
|
9
|
+
|
|
10
|
+
module Principles
|
|
11
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
12
|
+
module Models
|
|
13
|
+
# The members a full audit lists for the agent to weigh against the principle's activation, each with the
|
|
14
|
+
# facts that put it there (#355 slice 3, brief 2026-11-09-1322). It lists by fact and compares no globs, so
|
|
15
|
+
# whether a member loads outside the activation is the agent's judgement.
|
|
16
|
+
class LoadsToCheck
|
|
17
|
+
# activation is the map's, or nil when none is declared; loads is each member's MemberLoads by id; accepted
|
|
18
|
+
# is, by id, the decision the last completed audit accepted on a member's loading; hashes is each member's
|
|
19
|
+
# text hash now, by id.
|
|
20
|
+
#: (activation: Hash[String, untyped]?, loads: Hash[String, MemberLoads],
|
|
21
|
+
#| ?accepted: Hash[String, AcceptedLoading], ?hashes: Hash[String, String]) -> void
|
|
22
|
+
def initialize(activation:, loads:, accepted: {}, hashes: {})
|
|
23
|
+
@activation = activation
|
|
24
|
+
@loads = loads
|
|
25
|
+
@accepted = accepted
|
|
26
|
+
@hashes = hashes
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The activation, each listed member with what put it there and its loads, and the members this tool knows no
|
|
31
|
+
# carrier for; nil when the activation lists nothing to check.
|
|
32
|
+
#: () -> Hash[String, untyped]?
|
|
33
|
+
def listed
|
|
34
|
+
activation = @activation
|
|
35
|
+
return unless activation && Activation.new(activation).full_audit_lists == "session_start_members"
|
|
36
|
+
|
|
37
|
+
{ "activation" => activation, "rows" => rows,
|
|
38
|
+
"not_listed_no_known_carrier" => @loads.select { |_, loads| loads.moments == ["no_known_carrier"] }.keys }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Each listed member's accepted decision, by id, as the record keeps it: its reason and the loads it accepted;
|
|
42
|
+
# refused unless each listed member is decided exactly once, each accepted with a one-line reason.
|
|
43
|
+
#: (untyped) -> Hash[String, Hash[String, untyped]]
|
|
44
|
+
def decided(decisions)
|
|
45
|
+
given = loading_decisions(decisions)
|
|
46
|
+
expected = listed_by_id
|
|
47
|
+
raise incomplete(expected.keys) unless given.map(&:member_id).sort == expected.keys.sort
|
|
48
|
+
|
|
49
|
+
given.each(&:check)
|
|
50
|
+
given.to_h { |decision| [decision.member_id, decision.kept(expected.fetch(decision.member_id))] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Each of the answer's loading decisions, none when it gives no list.
|
|
56
|
+
#: (untyped) -> Array[LoadingDecision]
|
|
57
|
+
def loading_decisions(decisions)
|
|
58
|
+
given = decisions.is_a?(Array) ? decisions : []
|
|
59
|
+
given.each_with_index.map { |decision, index| LoadingDecision.new(decision, index) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Each listed member's row, by its id; none when the activation lists nothing to check.
|
|
63
|
+
#: () -> Hash[String, Hash[String, untyped]]
|
|
64
|
+
def listed_by_id = (listed&.fetch("rows") || []).to_h { |row| [row.fetch("member_id"), row] }
|
|
65
|
+
|
|
66
|
+
#: (Array[String]) -> Error
|
|
67
|
+
def incomplete(expected)
|
|
68
|
+
Error.new("incomplete_loading_decisions",
|
|
69
|
+
"Decide each member listed in loads_to_check_against_activation exactly once: " \
|
|
70
|
+
"#{expected.empty? ? "none is listed" : expected.join(", ")}",
|
|
71
|
+
"Give loading_decisions one {member_id, decision} for each listed member, and none for another.")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#: () -> Array[Hash[String, untyped]]
|
|
75
|
+
def rows
|
|
76
|
+
@loads.filter_map do |id, loads|
|
|
77
|
+
listed_for = loads.moments & ["session_start"]
|
|
78
|
+
next if listed_for.empty?
|
|
79
|
+
|
|
80
|
+
{ "member_id" => id, "listed_for" => listed_for, "loads" => loads.to_h }.merge(last_accepted(id, loads.to_h))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# What the last audit accepted on the member's loading, and whether what it weighed changed since; nothing
|
|
85
|
+
# when no audit accepted it.
|
|
86
|
+
#: (String, Hash[String, untyped]) -> Hash[String, untyped]
|
|
87
|
+
def last_accepted(id, now)
|
|
88
|
+
accepted = @accepted[id] or return {}
|
|
89
|
+
{ "last_accepted" => accepted.last_accepted(now, @hashes[id]) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "member_file"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# A refusal to locate a member, as its payload gives it, and what it makes of the member's state: lost in its
|
|
10
|
+
# file when its place there is gone, or else unavailable, as when its file cannot be read.
|
|
11
|
+
class LocateRefusal
|
|
12
|
+
#: (Hash[String, untyped]) -> void
|
|
13
|
+
def initialize(payload)
|
|
14
|
+
@payload = payload
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# locator_lost when its code says the member's place in its file is gone, or else unavailable.
|
|
19
|
+
#: () -> String
|
|
20
|
+
def member_state = MemberFile::PLACE_LOST.include?(@payload["code"]) ? "locator_lost" : "unavailable"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "line_span"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# The text a locator found now: its file, the lines it spans and the hash of what it says. A Locator says where
|
|
10
|
+
# to look; this is what was found there, as Project#locate finds it (#909).
|
|
11
|
+
class LocatedText
|
|
12
|
+
# The text found, from what Project#locate gives.
|
|
13
|
+
#: (Hash[String, untyped]) -> LocatedText
|
|
14
|
+
def self.of(found)
|
|
15
|
+
new(path: found.fetch("path"), first_line: found.fetch("first_line"), last_line: found.fetch("last_line"),
|
|
16
|
+
sha256: found.fetch("sha256"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: String
|
|
20
|
+
attr_reader :path, :sha256
|
|
21
|
+
|
|
22
|
+
#: Integer
|
|
23
|
+
attr_reader :first_line, :last_line
|
|
24
|
+
|
|
25
|
+
#: (path: String, first_line: Integer, last_line: Integer, sha256: String) -> void
|
|
26
|
+
def initialize(path:, first_line:, last_line:, sha256:)
|
|
27
|
+
@path = path
|
|
28
|
+
@first_line = first_line
|
|
29
|
+
@last_line = last_line
|
|
30
|
+
@sha256 = sha256
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The lines it spans, whole_file when it is its whole file.
|
|
35
|
+
#: (?whole_file: bool) -> LineSpan
|
|
36
|
+
def span(whole_file: false) = LineSpan.new(@first_line, @last_line, whole_file:)
|
|
37
|
+
|
|
38
|
+
# Its first and last lines, as a request prints them.
|
|
39
|
+
#: () -> Hash[String, Integer]
|
|
40
|
+
def lines = { "first_line" => @first_line, "last_line" => @last_line }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
# Where some text is in the project: a path, with a heading, a passage between starting_at and ending_at, or a
|
|
8
|
+
# frontmatter query inside it, or none of those to name the whole file. A member's reference, an evidence item
|
|
9
|
+
# and a write-up pointer each name text this way. It keeps the hash it was given, keys and order, since request
|
|
10
|
+
# ids and record bytes digest it.
|
|
11
|
+
class Locator
|
|
12
|
+
# What narrows a locator within its path, as references and the answer schemas name them; a locator naming
|
|
13
|
+
# none of them names its whole file. A member map calls the heading "section", which Member#reference
|
|
14
|
+
# translates.
|
|
15
|
+
KEYS = %w[heading starting_at ending_at query].freeze #: Array[String]
|
|
16
|
+
|
|
17
|
+
#: (Hash[String, untyped]) -> void
|
|
18
|
+
def initialize(fields)
|
|
19
|
+
@fields = fields.dup.freeze #: Hash[String, untyped]
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The file it points into.
|
|
24
|
+
#: () -> String
|
|
25
|
+
def path = @fields.fetch("path")
|
|
26
|
+
|
|
27
|
+
# The heading of the section it names, if it names one.
|
|
28
|
+
#: () -> String?
|
|
29
|
+
def heading = @fields["heading"]
|
|
30
|
+
|
|
31
|
+
# The opening text of the passage it names, if it names one.
|
|
32
|
+
#: () -> String?
|
|
33
|
+
def starting_at = @fields["starting_at"]
|
|
34
|
+
|
|
35
|
+
# The closing text of the passage it names, if it gives one.
|
|
36
|
+
#: () -> String?
|
|
37
|
+
def ending_at = @fields["ending_at"]
|
|
38
|
+
|
|
39
|
+
# The frontmatter field it names, as a JSONPath query, if it names one.
|
|
40
|
+
#: () -> String?
|
|
41
|
+
def query = @fields["query"]
|
|
42
|
+
|
|
43
|
+
# Whether it names its whole file, with no heading, passage or query inside it.
|
|
44
|
+
#: () -> bool
|
|
45
|
+
def whole_file? = KEYS.none? { |key| @fields.key?(key) }
|
|
46
|
+
|
|
47
|
+
# The locator as it was given.
|
|
48
|
+
#: () -> Hash[String, untyped]
|
|
49
|
+
def to_h = @fields
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "member"
|
|
6
|
+
require_relative "misspelt_member_id"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# One principle map's members, as Members: finding one by its id, and the rules on ids and keys among them,
|
|
12
|
+
# which MemberMap asks as it changes the map. It is Enumerable over its members, in the map's order.
|
|
13
|
+
#: [out Elem = Member]
|
|
14
|
+
class MapMembers
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
# The principle whose map these are.
|
|
18
|
+
#: String
|
|
19
|
+
attr_reader :principle_id
|
|
20
|
+
|
|
21
|
+
#: (String, Array[Member]) -> void
|
|
22
|
+
def initialize(principle_id, list)
|
|
23
|
+
@principle_id = principle_id
|
|
24
|
+
@list = list.freeze #: Array[Member]
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Each member, in the map's order, which every Enumerable method iterates.
|
|
29
|
+
# @override
|
|
30
|
+
#: () { (Member) -> void } -> void
|
|
31
|
+
def each(&) = @list.each(&)
|
|
32
|
+
|
|
33
|
+
# The members as the frozen list they are held in, without the copy Enumerable#to_a would make.
|
|
34
|
+
#: () -> Array[Member]
|
|
35
|
+
def to_a = @list
|
|
36
|
+
|
|
37
|
+
# Whether there are none, as in a map with no members yet.
|
|
38
|
+
#: () -> bool
|
|
39
|
+
def empty? = @list.empty?
|
|
40
|
+
|
|
41
|
+
# Each member's id, in the map's order.
|
|
42
|
+
#: () -> Array[String]
|
|
43
|
+
def ids = @list.map(&:id)
|
|
44
|
+
|
|
45
|
+
# Each member's key, in the map's order: its stored key, or its id when it has none.
|
|
46
|
+
#: () -> Array[String]
|
|
47
|
+
def keys = @list.map(&:key)
|
|
48
|
+
|
|
49
|
+
# Each member by its id.
|
|
50
|
+
#: () -> Hash[String, Member]
|
|
51
|
+
def by_id = @list.to_h { |member| [member.id, member] }
|
|
52
|
+
|
|
53
|
+
# Each file a member is in, once, in the map's order.
|
|
54
|
+
#: () -> Array[String]
|
|
55
|
+
def paths = @list.map(&:path).uniq
|
|
56
|
+
|
|
57
|
+
# Each member in one of paths, in the map's order.
|
|
58
|
+
#: (Array[String]) -> Array[Member]
|
|
59
|
+
def in_files(paths) = @list.select { |member| paths.include?(member.path) }
|
|
60
|
+
|
|
61
|
+
# The member with member_id now, or nil when none has it.
|
|
62
|
+
#: (untyped) -> Member?
|
|
63
|
+
def with_id(member_id) = @list.find { |member| member.id == member_id }
|
|
64
|
+
|
|
65
|
+
# The member known by key, its id before its first move or its id when it never moved, or nil when none is.
|
|
66
|
+
# The first is the only one, since a map whose members share a key is refused (repeated_keys).
|
|
67
|
+
#: (String) -> Member?
|
|
68
|
+
def with_key(key) = @list.find { |member| member.key == key }
|
|
69
|
+
|
|
70
|
+
# The check of the member with member_id, or nil when it has none or no member has that id (D30).
|
|
71
|
+
#: (untyped) -> String?
|
|
72
|
+
def check(member_id) = with_id(member_id)&.check
|
|
73
|
+
|
|
74
|
+
# Each key more than one member is known by, a member with no stored key being known by its id, with the ids of
|
|
75
|
+
# the members that share it. An audit record files each member under its key, so one would stand for the other.
|
|
76
|
+
#: () -> Hash[String, Array[String]]
|
|
77
|
+
def repeated_keys
|
|
78
|
+
@list.group_by(&:key).select { |_, sharing| sharing.length > 1 }.transform_values { it.map(&:id) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Refuses these members when two share an id or a key, as a hand edit or a merge can leave them. where names the
|
|
82
|
+
# map as read, and file the map file the refusal points at.
|
|
83
|
+
#: (where: String, file: String) -> void
|
|
84
|
+
def refuse_repeats(where:, file:)
|
|
85
|
+
refuse_repeated_ids(where, file)
|
|
86
|
+
refuse_repeated_keys(where, file)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Each member's key by id, for the members whose key is not their id because they have moved.
|
|
90
|
+
#: () -> Hash[String, String]
|
|
91
|
+
def moved_keys = @list.to_h { |member| [member.id, member.key] }.reject { |id, key| id == key }
|
|
92
|
+
|
|
93
|
+
# The member with member_id, refused unless one has it.
|
|
94
|
+
#: (String) -> Member
|
|
95
|
+
def found(member_id)
|
|
96
|
+
with_id(member_id) || raise(unknown(member_id))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (String) -> bool
|
|
100
|
+
def taken?(member_id) = !with_id(member_id).nil?
|
|
101
|
+
|
|
102
|
+
# member, with a key of its own when its id is already the key of a member that moved away from there;
|
|
103
|
+
# otherwise the new member would inherit that member's audit.
|
|
104
|
+
#: (Member) -> Member
|
|
105
|
+
def keyed(member)
|
|
106
|
+
return member unless keys.include?(member.id)
|
|
107
|
+
|
|
108
|
+
free = (2..).lazy.map { |n| "#{member.id}~#{n}" }.find { |key| !keys.include?(key) }
|
|
109
|
+
member.keyed_as(free || raise(KeyError, "no free key for #{member.id}"))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The refusal for an id no member has now, naming the member that had it before it moved, since a member keeps
|
|
113
|
+
# its id from before its first move as its key (#552). A member moved twice is not found by its middle id.
|
|
114
|
+
#: (String) -> Error
|
|
115
|
+
def unknown(member_id)
|
|
116
|
+
ids = "To see each member's id: #{Error::EXECUTABLE} show-principle #{@principle_id}"
|
|
117
|
+
moved = @list.select { |each| each.key == member_id }
|
|
118
|
+
return misspelt(member_id, ids) unless moved.one?
|
|
119
|
+
|
|
120
|
+
now = moved.fetch(0).id
|
|
121
|
+
Error.new("unknown_member", "#{member_id} is not a member of #{@principle_id}: it moved, and its id is now " \
|
|
122
|
+
"#{now}", "Give its id now, #{now}, in place of #{member_id}.\n#{ids}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The refusal for an id no member has or had, naming the one mapped id it is spelled like apart from case and
|
|
126
|
+
# punctuation, if any, under did_you_mean, so the controller can print the command again with it. The near miss
|
|
127
|
+
# itself is never accepted (D56, #729).
|
|
128
|
+
#: (String, String) -> Error
|
|
129
|
+
def misspelt(member_id, ids)
|
|
130
|
+
unknown = "#{member_id} is not a member of #{@principle_id}"
|
|
131
|
+
closest = MisspeltMemberId.new(member_id).closest(@list.map(&:id))
|
|
132
|
+
return Error.new("unknown_member", unknown, ids) unless closest
|
|
133
|
+
|
|
134
|
+
Error.new("unknown_member", "#{unknown}; did you mean #{closest}?", ids).including("did_you_mean" => closest)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The oldest map format that holds every member: 1 unless a member uses a later field.
|
|
138
|
+
#: () -> Integer
|
|
139
|
+
def format = @list.map(&:map_format).max || 1
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# The ids more than one member has, which a hand edit or a merge can leave.
|
|
144
|
+
#: () -> Array[String]
|
|
145
|
+
def repeated_ids = @list.map(&:id).tally.select { |_, count| count > 1 }.keys
|
|
146
|
+
|
|
147
|
+
# Every member is found and recorded by its id, so of two sharing one, one would silently stand for both.
|
|
148
|
+
#: (String, String) -> void
|
|
149
|
+
def refuse_repeated_ids(where, file)
|
|
150
|
+
shared = repeated_ids
|
|
151
|
+
return if shared.empty?
|
|
152
|
+
|
|
153
|
+
raise Error.new("duplicate_member", "#{where} has more than one member with id #{shared.join(", ")}",
|
|
154
|
+
"Keep one of each: edit the map file so that no two members point at the same place, " \
|
|
155
|
+
"then commit it.", file)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Two members with ids of their own known by one key (#647): which keeps it decides which keeps the audit filed
|
|
159
|
+
# under it, so the repair leaves that to the agent.
|
|
160
|
+
#: (String, String) -> void
|
|
161
|
+
def refuse_repeated_keys(where, file)
|
|
162
|
+
key, ids = repeated_keys.first
|
|
163
|
+
return unless ids
|
|
164
|
+
|
|
165
|
+
raise Error.new("duplicate_member", "#{where} has more than one member known by key #{key}: #{ids.join(", ")}",
|
|
166
|
+
"Keep one key per member: the member that keeps #{key} keeps the audit filed under it, so " \
|
|
167
|
+
"give the other a key of its own, such as #{key}~2, then commit the map.", file)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "member_file"
|
|
5
|
+
require_relative "frontmatter_fields"
|
|
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 Markdown file, where a member may be a section: a heading and what follows it, up to the next heading
|
|
11
|
+
# at its level or above.
|
|
12
|
+
class MarkdownFile < MemberFile
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# The frontmatter field the reference's query names, or the section under its heading, or the whole text.
|
|
16
|
+
#: (Array[String], Locator) -> [Integer, Integer]
|
|
17
|
+
def region(lines, locator)
|
|
18
|
+
query = locator.query
|
|
19
|
+
return FrontmatterFields.new(path: @path, lines: lines).field(query) if query
|
|
20
|
+
|
|
21
|
+
section(lines, locator.heading)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The line range of the Markdown section under heading, or the whole text without one. A section runs
|
|
25
|
+
# to the next heading at its level or above, so its subsections come with it.
|
|
26
|
+
#: (Array[String], String?) -> [Integer, Integer]
|
|
27
|
+
def section(lines, heading)
|
|
28
|
+
return [0, lines.length] unless heading
|
|
29
|
+
|
|
30
|
+
headings = heading_lines(lines)
|
|
31
|
+
first = only_heading(lines, headings, heading)
|
|
32
|
+
level = lines.fetch(first)[/\A\#+/].to_s.length
|
|
33
|
+
[first, headings.find { |i| i > first && lines.fetch(i).match?(/\A\#{1,#{level}}\s/) } || lines.length]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The indexes of lines that are Markdown headings: a # line outside the leading frontmatter and outside
|
|
37
|
+
# fenced code, where # starts a comment.
|
|
38
|
+
#: (Array[String]) -> Array[Integer]
|
|
39
|
+
def heading_lines(lines)
|
|
40
|
+
fence = nil #: String?
|
|
41
|
+
(frontmatter_end(lines)...lines.length).select do |i|
|
|
42
|
+
marker = lines.fetch(i)[/\A {0,3}(`{3,}|~{3,})/, 1]
|
|
43
|
+
fence = fenced_after(fence, marker) if marker
|
|
44
|
+
!marker && fence.nil? && lines.fetch(i).match?(/\A\#{1,6}\s/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The fence still open after a line that starts with marker: it opens one, or closes the one open
|
|
49
|
+
# when it repeats that fence's character at least as many times.
|
|
50
|
+
#: (String?, String) -> String?
|
|
51
|
+
def fenced_after(fence, marker)
|
|
52
|
+
return marker unless fence
|
|
53
|
+
|
|
54
|
+
marker.start_with?(fence) ? nil : fence
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The index of the first line after the frontmatter, which runs from a first line of --- to the next.
|
|
58
|
+
#: (Array[String]) -> Integer
|
|
59
|
+
def frontmatter_end(lines)
|
|
60
|
+
return 0 unless lines.first&.chomp == "---"
|
|
61
|
+
|
|
62
|
+
closing = (1...lines.length).find { |i| lines.fetch(i).chomp == "---" }
|
|
63
|
+
closing ? closing + 1 : 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether line is a heading of one to six #s, then whitespace, whose text is heading. It compares strings
|
|
67
|
+
# rather than building a pattern from heading, so a heading from a map cannot make the match slow.
|
|
68
|
+
#: (String, String) -> bool
|
|
69
|
+
def titled?(line, heading)
|
|
70
|
+
marks = line[/\A#+/].to_s.length
|
|
71
|
+
return false unless (1..6).cover?(marks)
|
|
72
|
+
|
|
73
|
+
rest = line[marks..].to_s
|
|
74
|
+
rest.match?(/\A\s/) && rest.strip == heading
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: (Array[String], Array[Integer], String) -> Integer
|
|
78
|
+
def only_heading(lines, headings, heading)
|
|
79
|
+
matches = headings.select { |i| titled?(lines.fetch(i), heading) }
|
|
80
|
+
return matches.fetch(0) if matches.length == 1
|
|
81
|
+
if matches.empty?
|
|
82
|
+
raise Error.new("section_not_found", "No Markdown heading #{heading.inspect}",
|
|
83
|
+
"Give the heading's exact text without its # marks, " \
|
|
84
|
+
"or leave out the heading to point at the whole file.", @path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
raise Error.new("ambiguous_heading", "Heading #{heading.inspect} has #{matches.length} matches",
|
|
88
|
+
"Use a unique Markdown heading, or leave out the heading to point at the whole file.", @path)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|