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,77 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The options one invocation gives: each value or flag, and which the agent typed, which CommandLine checks
|
|
8
|
+
# against what its command takes.
|
|
9
|
+
class InvocationOptions
|
|
10
|
+
# The options that take a value; --help, -h and --version take none.
|
|
11
|
+
VALUE_OPTIONS = %w[project file description path section starting-at ending-at query role
|
|
12
|
+
check member session-start commits-after hook-event].freeze #: Array[String]
|
|
13
|
+
# The options that take no value, as list-member-problems --short (#291).
|
|
14
|
+
FLAG_OPTIONS = %w[short when-invoked always write-ups].freeze #: Array[String]
|
|
15
|
+
# The options that may be given more than once, each value kept in order, as change-activation --when-files.
|
|
16
|
+
LIST_OPTIONS = %w[when-files].freeze #: Array[String]
|
|
17
|
+
# The words that name an option taking the next word as its value.
|
|
18
|
+
VALUE_WORDS = (VALUE_OPTIONS + LIST_OPTIONS).map { |name| "--#{name}" }.freeze #: Array[String]
|
|
19
|
+
# Every word the parser reads as an option, which an option taking a value refuses as its value (#750).
|
|
20
|
+
WORDS = [*VALUE_WORDS, *FLAG_OPTIONS.map { |name| "--#{name}" }, "--help", "-h",
|
|
21
|
+
"--version"].freeze #: Array[String]
|
|
22
|
+
|
|
23
|
+
# Whether word is the name of an option that takes the next word as its value.
|
|
24
|
+
#: (String) -> bool
|
|
25
|
+
def self.takes_a_value?(word) = VALUE_WORDS.include?(word)
|
|
26
|
+
|
|
27
|
+
#: () -> void
|
|
28
|
+
def initialize
|
|
29
|
+
@values = { "project" => Dir.pwd } #: Hash[String, untyped]
|
|
30
|
+
@given = [] #: Array[String]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Takes the options out of words, leaving the command and its positional arguments; an option no command
|
|
34
|
+
# defines is refused with OptionParser::InvalidOption.
|
|
35
|
+
#: (Array[String]) -> void
|
|
36
|
+
def parse!(words)
|
|
37
|
+
parser.parse!(words)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# An option's value, or true for a flag that was given; its name is written with underscores.
|
|
41
|
+
#: (String) -> untyped
|
|
42
|
+
def value(name)
|
|
43
|
+
@values[name]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Each option the agent typed, once, by its name on the command line.
|
|
47
|
+
#: () -> Array[String]
|
|
48
|
+
def given = @given.uniq
|
|
49
|
+
|
|
50
|
+
# Every option's value, under its name with underscores.
|
|
51
|
+
#: () -> Hash[String, untyped]
|
|
52
|
+
def to_h = @values.dup
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Records that the agent typed name, and keeps its value: appended for an option that may repeat, else set.
|
|
57
|
+
#: (String, untyped) -> void
|
|
58
|
+
def store(name, value)
|
|
59
|
+
@given << name
|
|
60
|
+
key = name.tr("-", "_")
|
|
61
|
+
@values[key] = LIST_OPTIONS.include?(name) ? [*@values[key], value] : value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A parser that fills the options, and records each value or flag option the agent typed.
|
|
65
|
+
#: () -> OptionParser
|
|
66
|
+
def parser
|
|
67
|
+
OptionParser.new do |opts|
|
|
68
|
+
(VALUE_OPTIONS + FLAG_OPTIONS + LIST_OPTIONS).each do |name|
|
|
69
|
+
# A flag yields true; a value option yields its value.
|
|
70
|
+
opts.on(FLAG_OPTIONS.include?(name) ? "--#{name}" : "--#{name} VALUE") { |value| store(name, value) }
|
|
71
|
+
end
|
|
72
|
+
opts.on("--help", "-h") { @values["help"] = true }
|
|
73
|
+
opts.on("--version") { @values["version"] = true }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "loading_facts"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# The decision a completed audit accepted on a member's loading, kept in the member's record entry with the
|
|
10
|
+
# member's text hash, as the next full audit shows it beside the member's row (#502 step 5). It changed since when
|
|
11
|
+
# what it weighed changed: when the member loads, or the member's own text. A file's size alone moves with every
|
|
12
|
+
# edit elsewhere in a shared file, so it does not count (second alpha user on 5bedec4, #515).
|
|
13
|
+
class AcceptedLoading
|
|
14
|
+
# entry is the member's record entry, holding loading_accepted and sha256.
|
|
15
|
+
#: (Hash[String, untyped]) -> void
|
|
16
|
+
def initialize(entry)
|
|
17
|
+
decision = entry.fetch("loading_accepted")
|
|
18
|
+
@reason = decision.fetch("reason") #: String
|
|
19
|
+
@loads = decision.fetch("loads") #: Hash[String, untyped]
|
|
20
|
+
@sha256 = entry.fetch("sha256") #: String
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The reason, and whether what the decision weighed changed since, given the member's loads and hash now; when
|
|
25
|
+
# it did, which of the two changed and the loads accepted, for the agent to see what moved.
|
|
26
|
+
#: (Hash[String, untyped], String?) -> Hash[String, untyped]
|
|
27
|
+
def last_accepted(loads_now, hash_now)
|
|
28
|
+
changed = [("loading" if LoadingFacts.new(@loads) != LoadingFacts.new(loads_now)),
|
|
29
|
+
("member_text" if @sha256 != hash_now)].compact
|
|
30
|
+
return { "reason" => @reason, "since_accepted" => "unchanged" } if changed.empty?
|
|
31
|
+
|
|
32
|
+
{ "reason" => @reason, "since_accepted" => "changed", "changed" => changed, "loads" => @loads }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "request_id"
|
|
6
|
+
require_relative "audit_record"
|
|
7
|
+
require_relative "member"
|
|
8
|
+
require_relative "placement"
|
|
9
|
+
|
|
10
|
+
module Principles
|
|
11
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
12
|
+
module Models
|
|
13
|
+
# A one-member audit (D33): the agent says a committed change to one member keeps its meaning, quoting a line
|
|
14
|
+
# from before and after it, so the record's baseline for that member moves on and --short stops repeating it.
|
|
15
|
+
# Refused outside git, for a member no completed audit assessed, and for an unchanged or checked member.
|
|
16
|
+
class Acknowledgement
|
|
17
|
+
# One side of the change a quote is checked against: the member's lines then, and when they read.
|
|
18
|
+
class SideOfTheChange
|
|
19
|
+
#: String
|
|
20
|
+
attr_reader :reads_at
|
|
21
|
+
|
|
22
|
+
#: (Array[String], String) -> void
|
|
23
|
+
def initialize(lines, reads_at)
|
|
24
|
+
@lines = lines.map(&:chomp) #: Array[String]
|
|
25
|
+
@reads_at = reads_at
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#: (String) -> bool
|
|
30
|
+
def whole_line?(quote) = @lines.include?(quote)
|
|
31
|
+
|
|
32
|
+
#: (String) -> bool
|
|
33
|
+
def holds?(text) = @lines.any? { |line| line.include?(text) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
PHASE = "acknowledgement"
|
|
37
|
+
# The fewest characters a quoted fragment of a line may have, so it shows which edit was read (#352).
|
|
38
|
+
FRAGMENT_LENGTH = 30
|
|
39
|
+
|
|
40
|
+
# before holds the member's lines at its baseline commit, now its lines and hash at base, HEAD.
|
|
41
|
+
#: (principle_id: String, member: Member, record: AuditRecord?, base: String?, now_sha256: String) -> void
|
|
42
|
+
def initialize(principle_id:, member:, record:, base:, now_sha256:)
|
|
43
|
+
@principle_id = principle_id
|
|
44
|
+
@member = member
|
|
45
|
+
@record = record
|
|
46
|
+
@base = base
|
|
47
|
+
@now_sha256 = now_sha256
|
|
48
|
+
freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The member's lines at its baseline and now, which the answer quotes; refused when the member cannot be found
|
|
52
|
+
# where its baseline was read, since there is no line there to quote (#459); #baseline reads a moved member
|
|
53
|
+
# where the map then placed it (#547).
|
|
54
|
+
#: (MemberLines?) -> MemberLines
|
|
55
|
+
def compared(lines)
|
|
56
|
+
lines or refuse("it cannot be found in its file at its baseline commit #{last_judged_commit}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The member's baseline: the oldest of placements, from the commit it was last judged at on, that holds the
|
|
60
|
+
# text the record vouches for, which is later than that commit when its full audit fixed it (#550), and as the
|
|
61
|
+
# map then placed it when it has moved since (#547). Refused when none does, since that text is not the
|
|
62
|
+
# audited one.
|
|
63
|
+
#: (Enumerable[Placement]) -> Placement
|
|
64
|
+
def baseline(placements)
|
|
65
|
+
recorded = acknowledgeable.members.hash_for(@member.key)
|
|
66
|
+
placements.find { |placement| placement.holds?(recorded) } ||
|
|
67
|
+
refuse("its text does not match the audit record at #{last_judged_commit} or any later commit")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Where the member was at baseline, when that is not where it is now, as after a move (#547).
|
|
71
|
+
#: (Placement) -> Hash[String, untyped]?
|
|
72
|
+
def moved_from(baseline)
|
|
73
|
+
baseline.reference unless baseline.reference == @member.reference
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The paths the change since baseline spans, which the baseline's placement owns (#551).
|
|
77
|
+
#: (Placement) -> Array[String]
|
|
78
|
+
def paths_since(baseline) = baseline.paths_to(@member.path)
|
|
79
|
+
|
|
80
|
+
# The commit the member was last judged at, when baseline is later than it, as when the member's own full
|
|
81
|
+
# audit fixed it after its base (#550).
|
|
82
|
+
#: (Placement) -> String?
|
|
83
|
+
def audit_base(baseline)
|
|
84
|
+
judged = last_judged_commit
|
|
85
|
+
judged unless baseline.commit == judged
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The commit the member was last judged at: its last acknowledgement's, or the audit's base commit. Refused
|
|
89
|
+
# unless the member can be acknowledged now.
|
|
90
|
+
#: () -> String
|
|
91
|
+
def last_judged_commit
|
|
92
|
+
record = acknowledgeable
|
|
93
|
+
record.members.acknowledgements_for(@member.key).last&.fetch("commit") || record.base_commit.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The request's id: the member, its hash at the baseline and now, and the commit it is judged at.
|
|
97
|
+
#: () -> RequestId
|
|
98
|
+
def request_id
|
|
99
|
+
RequestId.for(principle_id: @principle_id, phase: PHASE,
|
|
100
|
+
parts: [@member.key, acknowledgeable.members.hash_for(@member.key), @now_sha256, @base])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The ids of the deferred and the accepted findings the last audit left on this member, which the request
|
|
104
|
+
# and the acknowledgement name (D33), the accepted ones since an edit may bear on whether they still hold
|
|
105
|
+
# (D35 brief, finding 11).
|
|
106
|
+
#: () -> Hash[String, Array[String]]
|
|
107
|
+
def open_findings
|
|
108
|
+
{ "deferred_findings" => findings_on_member("deferred"), "accepted_findings" => findings_on_member("accepted") }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The record with this member's baseline moved to its text now, once the answer is checked against the
|
|
112
|
+
# member's lines at the baseline commit and now.
|
|
113
|
+
#: (untyped, before: Array[String], now: Array[String], at: String) -> AuditRecord
|
|
114
|
+
def accepted(answer, before:, now:, at:)
|
|
115
|
+
checked(answer, before, now)
|
|
116
|
+
acknowledgeable.acknowledged(@member.key, { "from_sha256" => acknowledgeable.members.hash_for(@member.key),
|
|
117
|
+
"to_sha256" => @now_sha256, "commit" => @base,
|
|
118
|
+
"reason" => answer.fetch("reason"), "acknowledged_at" => at,
|
|
119
|
+
**open_findings })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# The ids of the findings the last audit left open on this member with action.
|
|
125
|
+
#: (String) -> Array[String]
|
|
126
|
+
def findings_on_member(action)
|
|
127
|
+
acknowledgeable.open_findings
|
|
128
|
+
.select { |finding| finding.fetch("action") == action }
|
|
129
|
+
.select { |finding| finding.fetch("member_ids").any? { |id| on_member?(id) } }
|
|
130
|
+
.map { |finding| finding.fetch("finding_id") }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Whether the record's id names this member, by key, so a member moved since keeps its findings (#547).
|
|
134
|
+
#: (String) -> bool
|
|
135
|
+
def on_member?(id) = acknowledgeable.members.key_of(id) == @member.key
|
|
136
|
+
|
|
137
|
+
# The completed record, once the member can be acknowledged.
|
|
138
|
+
#: () -> AuditRecord
|
|
139
|
+
def acknowledgeable
|
|
140
|
+
refuse("an acknowledgement is judged against committed text, so it needs git") unless @base
|
|
141
|
+
record = @record or no_completed_audit
|
|
142
|
+
refused_for(record.acknowledgement_refusal(@member, now_sha256: @now_sha256))
|
|
143
|
+
record
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Raises the refusal the record gives for this member, if any.
|
|
147
|
+
#: (Symbol?) -> void
|
|
148
|
+
def refused_for(reason)
|
|
149
|
+
case reason
|
|
150
|
+
when :not_completed then no_completed_audit
|
|
151
|
+
when :not_assessed then refuse("no completed audit assessed #{@member.id}")
|
|
152
|
+
when :unchanged then nothing_to_acknowledge
|
|
153
|
+
when :checked then refuse("#{@member.id} has a check, and acknowledging a checked member is not built yet")
|
|
154
|
+
when nil then nil
|
|
155
|
+
else raise ArgumentError, "unknown acknowledgement refusal #{reason}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
#: (untyped, Array[String], Array[String]) -> void
|
|
160
|
+
def checked(answer, before, now)
|
|
161
|
+
refuse("the answer says the change does not keep #{@member.id}'s meaning") unless answer["meaning_kept"] == true
|
|
162
|
+
reason = answer.fetch("reason").to_s
|
|
163
|
+
refuse("the reason must be one line") if reason.include?("\n")
|
|
164
|
+
then_side = SideOfTheChange.new(before, "before the change")
|
|
165
|
+
now_side = SideOfTheChange.new(now, "at HEAD")
|
|
166
|
+
quoted(answer.fetch("old_line"), then_side, now_side)
|
|
167
|
+
quoted(answer.fetch("new_line"), now_side, then_side)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A quote is a whole line of this side, or a fragment long enough to show the edit that only this side reads.
|
|
171
|
+
#: (String, SideOfTheChange, SideOfTheChange) -> void
|
|
172
|
+
def quoted(line, side, other)
|
|
173
|
+
return if side.whole_line?(line) || (line.length >= FRAGMENT_LENGTH && side.holds?(line) && !other.holds?(line))
|
|
174
|
+
|
|
175
|
+
raise Error.new("quote_not_found", "#{line.inspect} is not a line of #{@member.id} #{side.reads_at}",
|
|
176
|
+
"Quote a whole line of the member as it reads #{side.reads_at}, or at least " \
|
|
177
|
+
"#{FRAGMENT_LENGTH} characters of one that the member #{other.reads_at} does not hold, " \
|
|
178
|
+
"from the change the request names.")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#: () -> bot
|
|
182
|
+
def no_completed_audit = refuse("#{@principle_id} has no completed audit, so no baseline has been judged")
|
|
183
|
+
|
|
184
|
+
# A member whose text is the one last judged, as after a pure move, needs nothing: not an acknowledgement, and
|
|
185
|
+
# not the full audit the other refusals send the agent to (#553).
|
|
186
|
+
#: () -> bot
|
|
187
|
+
def nothing_to_acknowledge
|
|
188
|
+
raise Error.new("cannot_acknowledge",
|
|
189
|
+
"#{@member.id}'s text is the one last judged, so there is nothing to acknowledge",
|
|
190
|
+
"Nothing needs doing.")
|
|
191
|
+
.ending_with([Error::RepairCommand.new(purpose: "After a committed edit changes it, acknowledge it",
|
|
192
|
+
command: "start-audit", values: [@principle_id],
|
|
193
|
+
options: ["--member", @member.id])])
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#: (String) -> bot
|
|
197
|
+
def refuse(why)
|
|
198
|
+
raise Error.new("cannot_acknowledge", "#{@member.id} cannot be acknowledged: #{why}",
|
|
199
|
+
"For a full audit instead: #{Error::EXECUTABLE} start-audit #{@principle_id}")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# When a principle's work is in hand, as the agent judged it (D38): when files matching its globs are read or
|
|
10
|
+
# changed, when its work is invoked, or always. This tool stores what the agent declared and does not choose it.
|
|
11
|
+
class Activation
|
|
12
|
+
FORMS = "Give exactly one of --when-files GLOB (repeat it for each glob), --when-invoked, or --always."
|
|
13
|
+
|
|
14
|
+
# The activation change-activation's options declare, refused unless they name exactly one form.
|
|
15
|
+
#: (when_files: Array[String]?, when_invoked: bool?, always: bool?) -> Activation
|
|
16
|
+
def self.declared(when_files:, when_invoked:, always:)
|
|
17
|
+
given = { "files" => when_files, "invoked" => when_invoked, "always" => always }.compact
|
|
18
|
+
unless given.length == 1
|
|
19
|
+
raise Error.new("invalid_activation", "change-activation needs one activation, not #{given.length}", FORMS)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
globs = when_files
|
|
23
|
+
return new({ "when" => given.keys.first }) unless globs
|
|
24
|
+
|
|
25
|
+
new({ "when" => "files", "files" => globs })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (Hash[String, untyped]) -> void
|
|
29
|
+
def initialize(fields)
|
|
30
|
+
@fields = fields.freeze #: Hash[String, untyped]
|
|
31
|
+
check_globs
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The activation as the map stores it.
|
|
36
|
+
#: () -> Hash[String, untyped]
|
|
37
|
+
def to_h
|
|
38
|
+
@fields
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# What a full audit lists against this activation for the agent to weigh: the members that load at every
|
|
42
|
+
# session start under a files activation; none under always, where the work is in hand in every session; and,
|
|
43
|
+
# under invoked, a listing not built yet (#355, #523).
|
|
44
|
+
#: () -> String
|
|
45
|
+
def full_audit_lists
|
|
46
|
+
case @fields.fetch("when")
|
|
47
|
+
when "files" then "session_start_members"
|
|
48
|
+
when "invoked" then "not_built"
|
|
49
|
+
else "none"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
#: () -> void
|
|
56
|
+
def check_globs
|
|
57
|
+
return unless @fields.fetch("files", []).any? { |glob| glob.strip.empty? }
|
|
58
|
+
|
|
59
|
+
raise Error.new("invalid_activation", "an empty glob matches no file",
|
|
60
|
+
"Give each --when-files a glob such as 'spec/**'.")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "frontmatter_fields"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# When Claude Code loads a project subagent's definition. Its description, which Claude reads to decide when
|
|
11
|
+
# to delegate, loads at session start in the main session; its body is the subagent's own system prompt, loaded
|
|
12
|
+
# in the subagent's context when Claude delegates to it. Claude Code reads .claude/agents recursively, from
|
|
13
|
+
# the start folder up to the repository root, as its subagents documentation says; the #354 experiment saw
|
|
14
|
+
# a project subagent's description listed at start.
|
|
15
|
+
class AgentLoads
|
|
16
|
+
# An agent's definition: a Markdown file at any depth in a .claude/agents folder, at the root or below it.
|
|
17
|
+
AGENT_FILE = %r{\A(?<folder>(?:.*/)?)\.claude/agents/(?:[^/]+/)*[^/]+\.md\z} #: Regexp
|
|
18
|
+
|
|
19
|
+
# text is the agent's whole file, or nil when it cannot be read.
|
|
20
|
+
#: (String, String?) -> void
|
|
21
|
+
def initialize(path, text)
|
|
22
|
+
@path = path
|
|
23
|
+
@text = text
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: () -> Hash[String, untyped]
|
|
28
|
+
def to_h
|
|
29
|
+
lines = @text&.lines or return { "when" => "not_determined" }
|
|
30
|
+
return not_derived("an agents folder below the project root") unless folder.empty?
|
|
31
|
+
|
|
32
|
+
description = FrontmatterFields.new(path: @path, lines: lines).field_lines("$.description")
|
|
33
|
+
return not_derived("an agent file without a description field") unless description
|
|
34
|
+
|
|
35
|
+
{ "when" => "on_delegation", "context" => "subagent", "description" => { "when" => "session_start" } }
|
|
36
|
+
rescue Error => e
|
|
37
|
+
{ "when" => "not_determined", "frontmatter_refused" => e.payload.fetch("code") }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The description lines Claude reads at start to decide when to delegate, as written; nil without the field, or
|
|
41
|
+
# when the file cannot be read. The subagents page states no cap on one description.
|
|
42
|
+
#: () -> String?
|
|
43
|
+
def description_text
|
|
44
|
+
lines = @text&.lines or return nil
|
|
45
|
+
FrontmatterFields.new(path: @path, lines: lines).field_lines("$.description")&.join
|
|
46
|
+
rescue Error
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# The folder whose .claude/agents holds the agent: empty at the project root, else ending in a slash.
|
|
53
|
+
#: () -> String
|
|
54
|
+
def folder
|
|
55
|
+
AGENT_FILE.match(@path)&.[](:folder).to_s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: (String) -> Hash[String, untyped]
|
|
59
|
+
def not_derived(why)
|
|
60
|
+
{ "when" => "not_determined", "not_derived" => why }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "locator"
|
|
6
|
+
require_relative "answer_place"
|
|
7
|
+
require_relative "answer_text"
|
|
8
|
+
require_relative "write_up_pointer"
|
|
9
|
+
|
|
10
|
+
module Principles
|
|
11
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
12
|
+
module Models
|
|
13
|
+
# The checks every answer's fields share: a field is a nonempty string or an array, and each piece of
|
|
14
|
+
# evidence names a path that the caller's block locates in the project.
|
|
15
|
+
class AnswerFields
|
|
16
|
+
#: (^(Locator) -> void) -> void
|
|
17
|
+
def initialize(locate)
|
|
18
|
+
@locate = locate
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#: (untyped, String) -> String
|
|
22
|
+
def text(value, field) = AnswerText.new(value).required(field)
|
|
23
|
+
|
|
24
|
+
# value as text, refused as field at its place in the answer, so one of many entries is found (#787).
|
|
25
|
+
#: (untyped, String, AnswerPlace) -> String
|
|
26
|
+
def text_at(value, field, place)
|
|
27
|
+
text(value, field)
|
|
28
|
+
rescue Error => e
|
|
29
|
+
raise place.refusal(e)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (untyped, String) -> untyped
|
|
33
|
+
def array(value, field)
|
|
34
|
+
unless value.is_a?(Array)
|
|
35
|
+
raise Error.new("invalid_answer", "#{field} must be an array",
|
|
36
|
+
"Supply #{field} as the question asks.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Each piece of evidence, located where place says the answer holds it; a refusal locating one names its place.
|
|
43
|
+
#: (untyped, AnswerPlace) -> void
|
|
44
|
+
def evidence(references, place)
|
|
45
|
+
array(references, "evidence").each_with_index { |reference, index| pointer(reference, place.child(index)) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# One pointer into the project, such as where a finding is written up, located at its place in the answer.
|
|
49
|
+
#: (untyped, AnswerPlace) -> void
|
|
50
|
+
def pointer(reference, place)
|
|
51
|
+
text(reference["path"], "evidence.path")
|
|
52
|
+
begin
|
|
53
|
+
@locate.call(Locator.new(reference))
|
|
54
|
+
rescue Error => e
|
|
55
|
+
raise place.refusal(e)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Where a finding is written up, located at its place in the answer, as the value its record keeps (#768).
|
|
60
|
+
#: (untyped, AnswerPlace) -> WriteUpPointer
|
|
61
|
+
def write_up_pointer(reference, place)
|
|
62
|
+
pointer(reference, place)
|
|
63
|
+
WriteUpPointer.new(reference)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "member_file"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# A place in an agent's answer: its JSON pointer, and whose part of the answer it is, such as "Finding F1".
|
|
11
|
+
# A refusal about what sits there names both, so the agent can find the one pointer to repair (#702).
|
|
12
|
+
class AnswerPlace
|
|
13
|
+
# What a pointer in an answer may do instead of quoting exactly a passage its file does not hold (#742).
|
|
14
|
+
ELSEWHERE = " Or point it at text the file holds."
|
|
15
|
+
|
|
16
|
+
#: String
|
|
17
|
+
attr_reader :pointer
|
|
18
|
+
|
|
19
|
+
# Whose part of the answer it is, such as "Finding F1".
|
|
20
|
+
#: String
|
|
21
|
+
attr_reader :subject
|
|
22
|
+
|
|
23
|
+
#: (String, String) -> void
|
|
24
|
+
def initialize(pointer, subject)
|
|
25
|
+
@pointer = pointer
|
|
26
|
+
@subject = subject
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The place of name, a key or an index, inside this one, with the same subject.
|
|
31
|
+
#: (String | Integer) -> AnswerPlace
|
|
32
|
+
def child(name) = AnswerPlace.new("#{@pointer}/#{name}", @subject)
|
|
33
|
+
|
|
34
|
+
# error, a refusal about what sits here, saying whose it is and where, as a malformed answer's fields do (D29).
|
|
35
|
+
#: (Error) -> Error
|
|
36
|
+
def refusal(error)
|
|
37
|
+
payload = error.payload
|
|
38
|
+
message = payload.fetch("message")
|
|
39
|
+
Error.new(payload.fetch("code"), "#{@subject}: #{message}", repair(payload))
|
|
40
|
+
.including(payload.except("message", "repair"))
|
|
41
|
+
.including("fields" => [{ "pointer" => @pointer, "problem" => message }])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
#: (Hash[String, untyped]) -> String
|
|
47
|
+
def repair(payload)
|
|
48
|
+
repair = payload.fetch("repair")
|
|
49
|
+
MemberFile::QUOTE_NOT_FOUND.include?(payload.fetch("code")) ? repair + ELSEWHERE : repair
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# A value an agent gave where its answer asks for text, the one owner of whether it wrote any (#796).
|
|
10
|
+
class AnswerText
|
|
11
|
+
#: (untyped) -> void
|
|
12
|
+
def initialize(value)
|
|
13
|
+
@value = value
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Whether the agent wrote it: a string with more than whitespace in it.
|
|
18
|
+
#: () -> bool
|
|
19
|
+
def written? = @value.is_a?(String) && !@value.strip.empty? ? true : false
|
|
20
|
+
|
|
21
|
+
# Whether the agent wrote it as one line: written, with no line break in it.
|
|
22
|
+
#: () -> bool
|
|
23
|
+
def one_line? = written? && !@value.include?("\n")
|
|
24
|
+
|
|
25
|
+
# The text where field requires it: as written, or refused naming field (#798).
|
|
26
|
+
#: (String) -> String
|
|
27
|
+
def required(field)
|
|
28
|
+
unless written?
|
|
29
|
+
raise Error.new("invalid_answer", "#{field} must be a nonempty string",
|
|
30
|
+
"Supply #{field} as the question asks.")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# A time as an agent wrote it in an answer, such as a sighting's observed_at, which the record compares with
|
|
10
|
+
# audited_at only in audited_at's own form: a UTC time to the second (#699).
|
|
11
|
+
class AnsweredTime
|
|
12
|
+
#: (untyped) -> void
|
|
13
|
+
def initialize(value)
|
|
14
|
+
@value = value
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Whether it is a UTC time in audited_at's form. Only a time that reads back as written is: Time.iso8601 takes an
|
|
19
|
+
# offset or a fraction, and rolls 30 February over to March.
|
|
20
|
+
#: () -> bool
|
|
21
|
+
def utc?
|
|
22
|
+
return false unless @value.is_a?(String)
|
|
23
|
+
|
|
24
|
+
Time.iso8601(@value).utc.iso8601 == @value
|
|
25
|
+
rescue ArgumentError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Whether it is written as a date alone, such as 2026-11-01, whether or not a calendar has that date.
|
|
30
|
+
#: () -> bool
|
|
31
|
+
def date_alone?
|
|
32
|
+
return false unless @value.is_a?(String)
|
|
33
|
+
|
|
34
|
+
@value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The UTC time in audited_at's form that it names exactly, or nil when it names none: a date alone at its
|
|
38
|
+
# midnight, and a time to the second with an offset as the same instant (#801). A fraction, or a date no
|
|
39
|
+
# calendar has, names none.
|
|
40
|
+
#: () -> String?
|
|
41
|
+
def exact_utc
|
|
42
|
+
return midnight if date_alone?
|
|
43
|
+
return unless @value.is_a?(String)
|
|
44
|
+
|
|
45
|
+
time = Time.iso8601(@value)
|
|
46
|
+
time.utc.iso8601 if time.iso8601 == @value
|
|
47
|
+
rescue ArgumentError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether it is later than time, both UTC times in audited_at's form: a sighting is made before the answer that
|
|
52
|
+
# reports it (#699), so a repair never prints one later (#801).
|
|
53
|
+
#: (String) -> bool
|
|
54
|
+
def after?(time) = Time.iso8601(@value) > Time.iso8601(time)
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
#: () -> String?
|
|
59
|
+
def midnight
|
|
60
|
+
at_midnight = "#{@value}T00:00:00Z"
|
|
61
|
+
at_midnight if AnsweredTime.new(at_midnight).utc?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|