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,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The opening of a write-up's region in a file's text: what list-open-findings prints as a finding's
|
|
8
|
+
# write_up_opening, and show-answer-check previews (#300, #411). This tool does not judge what it says.
|
|
9
|
+
class WriteUpOpening
|
|
10
|
+
# The longest opening printed whole; a longer one is cut and marked truncated.
|
|
11
|
+
LIMIT = 200
|
|
12
|
+
# A line that starts a new block, a list item or a heading, rather than continuing the paragraph before it.
|
|
13
|
+
NEW_BLOCK = /\A(([-*+]|\d+[.)])\s|#)/
|
|
14
|
+
# What a heading line starts with, a block of its own line.
|
|
15
|
+
HEADING = "#"
|
|
16
|
+
# The end of a sentence: its mark and any closing quotes or brackets, before a space or the end.
|
|
17
|
+
SENTENCE_END = /[.!?]["')\]]*(?=\s|\z)/
|
|
18
|
+
|
|
19
|
+
# One line of the file, as the opening reads it.
|
|
20
|
+
class Line
|
|
21
|
+
#: (String) -> void
|
|
22
|
+
def initialize(text)
|
|
23
|
+
@text = text
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The line without its indentation or line break.
|
|
28
|
+
#: () -> String
|
|
29
|
+
def text = @text.strip
|
|
30
|
+
|
|
31
|
+
#: () -> bool
|
|
32
|
+
def blank? = text.empty?
|
|
33
|
+
|
|
34
|
+
#: () -> bool
|
|
35
|
+
def heading? = text.start_with?(HEADING)
|
|
36
|
+
|
|
37
|
+
# Whether the line continues the paragraph before it: it is not blank and starts no new block. It keeps its
|
|
38
|
+
# line break, which NEW_BLOCK's \s may match, so a bare "-" line still starts a list item.
|
|
39
|
+
#: () -> bool
|
|
40
|
+
def continues? = !blank? && !@text.lstrip.match?(NEW_BLOCK)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# lines are the file's lines; the region runs from index first up to, not including, index last.
|
|
44
|
+
#: (lines: Array[String], first: Integer, last: Integer) -> void
|
|
45
|
+
def initialize(lines:, first:, last:)
|
|
46
|
+
@lines = lines.map { |each| Line.new(each) } #: Array[Line]
|
|
47
|
+
@first = first
|
|
48
|
+
@last = last
|
|
49
|
+
freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The opening from the region's first non-blank line, with that line's number. nil when every line in the
|
|
53
|
+
# region is blank.
|
|
54
|
+
#: () -> Hash[String, untyped]?
|
|
55
|
+
def to_h
|
|
56
|
+
index = (@first...@last).find { |each| !line(each).blank? } or return nil
|
|
57
|
+
text = opening_text(index)
|
|
58
|
+
{ "text" => text[0, LIMIT], "line" => index + 1 }.merge(text.length > LIMIT ? { "truncated" => true } : {})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# A heading line whole, since a heading is not a sentence and "F4." may number it (#739). Otherwise the
|
|
64
|
+
# first sentence: hard-wrapped lines joined with single spaces up to the sentence's end, or else to its
|
|
65
|
+
# paragraph's end, a blank line, the next list item or heading, or the file's end. It runs past the region's
|
|
66
|
+
# end, since a starting_at alone locates one line (#697).
|
|
67
|
+
#: (Integer) -> String
|
|
68
|
+
def opening_text(index)
|
|
69
|
+
return line(index).text if line(index).heading?
|
|
70
|
+
|
|
71
|
+
sentence(paragraph_from(index).join(" "))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The trimmed lines of the paragraph that starts at index, to its end in the file.
|
|
75
|
+
#: (Integer) -> Array[String]
|
|
76
|
+
def paragraph_from(index)
|
|
77
|
+
rest = @lines.drop(index + 1).take_while(&:continues?)
|
|
78
|
+
[line(index), *rest].map(&:text)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# text up to the end of its first sentence, or all of it when none ends.
|
|
82
|
+
#: (String) -> String
|
|
83
|
+
def sentence(text)
|
|
84
|
+
found = SENTENCE_END.match(text) or return text
|
|
85
|
+
text[0, found.end(0)].to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The line at index, or a blank one past the file's end.
|
|
89
|
+
#: (Integer) -> Line
|
|
90
|
+
def line(index) = @lines[index] || Line.new("")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "locator"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
8
|
+
module Models
|
|
9
|
+
# Where a finding is written up, as its record's recorded_at keeps it: a path, with a heading or a starting_at
|
|
10
|
+
# inside it (#768). It holds that place as the Locator the hash it was read from gives, which keeps its keys, nulls
|
|
11
|
+
# and order, since the record's bytes and each digest over them depend on it; two pointers are the same when
|
|
12
|
+
# those hashes hold the same keys and values, in any order.
|
|
13
|
+
class WriteUpPointer
|
|
14
|
+
#: (Hash[String, untyped]) -> void
|
|
15
|
+
def initialize(fields)
|
|
16
|
+
@locator = Locator.new(fields) #: Locator
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The file the write-up is in.
|
|
21
|
+
#: () -> String
|
|
22
|
+
def path = @locator.path
|
|
23
|
+
|
|
24
|
+
# Whether it names a starting_at inside its file.
|
|
25
|
+
#: () -> bool
|
|
26
|
+
def starting_at? = !@locator.starting_at.nil?
|
|
27
|
+
|
|
28
|
+
# The same heading or starting_at in the file at path, as a repoint checks git's match for a missing file (#722).
|
|
29
|
+
#: (String) -> WriteUpPointer
|
|
30
|
+
def in_file(path) = WriteUpPointer.new(to_h.merge("path" => path))
|
|
31
|
+
|
|
32
|
+
# The opening of its write-up in project's file as it stands now, as WriteUpOpening finds it, or nil when there
|
|
33
|
+
# is none; refused when the pointer no longer resolves (#300).
|
|
34
|
+
#: (Project) -> Hash[String, untyped]?
|
|
35
|
+
def opening_in(project) = project.member_file(path).write_up_opening(@locator)
|
|
36
|
+
|
|
37
|
+
# The pointer as its record's file holds it.
|
|
38
|
+
#: () -> Hash[String, untyped]
|
|
39
|
+
def to_h = @locator.to_h
|
|
40
|
+
|
|
41
|
+
#: (untyped) -> bool
|
|
42
|
+
def ==(other) = other.is_a?(WriteUpPointer) && other.to_h.eql?(to_h)
|
|
43
|
+
|
|
44
|
+
alias eql? ==
|
|
45
|
+
|
|
46
|
+
#: () -> Integer
|
|
47
|
+
def hash = to_h.hash
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "answer_fields"
|
|
5
|
+
require_relative "recorded_finding"
|
|
6
|
+
require_relative "write_up_pointer"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# One finding's write-up pointer moved by a repoint (D59): the finding it names, the recorded_at the agent
|
|
12
|
+
# found the write-up at, and why. It moves a finding there, keeping where it pointed before, when and why.
|
|
13
|
+
class WriteUpRepoint
|
|
14
|
+
# The repoint one entry of an answer's repoints gives, to recorded_at, its pointer already located, refused by
|
|
15
|
+
# fields without a reason, since the record keeps why each pointer moved (#760), naming its place in the answer
|
|
16
|
+
# (#787).
|
|
17
|
+
#: (Hash[String, untyped], WriteUpPointer, AnswerFields, AnswerPlace) -> WriteUpRepoint
|
|
18
|
+
def self.answered(repoint, recorded_at, fields, place)
|
|
19
|
+
new(finding_id: repoint.fetch("finding_id"), recorded_at: recorded_at,
|
|
20
|
+
reason: fields.text_at(repoint["reason"], "repoint.reason", place.child("reason")))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: String
|
|
24
|
+
attr_reader :finding_id
|
|
25
|
+
|
|
26
|
+
#: (finding_id: String, recorded_at: WriteUpPointer, reason: String) -> void
|
|
27
|
+
def initialize(finding_id:, recorded_at:, reason:)
|
|
28
|
+
@finding_id = finding_id
|
|
29
|
+
@recorded_at = recorded_at
|
|
30
|
+
@reason = reason
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# finding moved to this repoint's recorded_at, for its reason, at at.
|
|
35
|
+
#: (RecordedFinding, at: String) -> RecordedFinding
|
|
36
|
+
def applied_to(finding, at:) = finding.moved_to(@recorded_at, at: at, reason: @reason)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# Items as one would say them in a sentence: "a", "a and b", or "a, b and c", or with "or" for alternatives. It
|
|
6
|
+
# is the one owner of that list, which several results and refusals print (#445, #731, #737), so every layer may
|
|
7
|
+
# load it, as it may error.rb.
|
|
8
|
+
class ProseList
|
|
9
|
+
#: (Array[String], ?conjunction: String) -> void
|
|
10
|
+
def initialize(items, conjunction: "and")
|
|
11
|
+
@items = items
|
|
12
|
+
@conjunction = conjunction
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#: () -> String
|
|
17
|
+
def to_s
|
|
18
|
+
return @items.join if @items.length < 2
|
|
19
|
+
|
|
20
|
+
"#{@items.take(@items.length - 1).join(", ")} #{@conjunction} #{@items.last}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The Ruby this tool runs on. exe/principles loads this before anything else, so an agent on a Ruby too old to
|
|
6
|
+
# parse lib/ still gets this tool's JSON refusal, not a SyntaxError. So this file stays in syntax any Ruby 2
|
|
7
|
+
# parses, and requires only rbconfig: a GEM_HOME left by another Ruby can hand an old Ruby a json it cannot
|
|
8
|
+
# parse. When the Ruby running is unsupported, it looks for a supported one that rbenv installed, since
|
|
9
|
+
# rbenv otherwise picks Ruby by the directory this tool runs in, and runs this tool on that instead (D31).
|
|
10
|
+
# Its bounds equal the gemspec's required_ruby_version; a spec checks it.
|
|
11
|
+
class SupportedRuby
|
|
12
|
+
MINIMUM = [3, 4, 2].freeze #: Array[Integer]
|
|
13
|
+
BELOW = [3, 5].freeze #: Array[Integer]
|
|
14
|
+
# Set on the process this tool hands itself to, so a Ruby that is not what its folder name says cannot
|
|
15
|
+
# hand it on again.
|
|
16
|
+
HANDED_OFF = "PRINCIPLES_HANDED_TO_RUBY"
|
|
17
|
+
|
|
18
|
+
#: (String, Hash[String, String]) -> void
|
|
19
|
+
def initialize(running, environment)
|
|
20
|
+
@running = running
|
|
21
|
+
@environment = environment
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns when this Ruby is supported; otherwise runs executable on a supported rbenv Ruby, or prints the
|
|
25
|
+
# refusal and exits 1.
|
|
26
|
+
#: (String, Array[String]) -> void
|
|
27
|
+
def run_here_or_on_another(executable, arguments)
|
|
28
|
+
step = next_step(executable, arguments)
|
|
29
|
+
return if step.key?("run_here")
|
|
30
|
+
|
|
31
|
+
if step.key?("hand_to")
|
|
32
|
+
process = Kernel #: as untyped
|
|
33
|
+
process.exec(*step.fetch("hand_to"))
|
|
34
|
+
end
|
|
35
|
+
warn step.fetch("refuse")
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# What this tool does on this Ruby, decided without acting: run_here; hand_to, the arguments for Kernel.exec
|
|
40
|
+
# that run executable on a supported rbenv Ruby; or refuse, the refusal to print before exiting 1.
|
|
41
|
+
#: (String, Array[String]) -> Hash[String, untyped]
|
|
42
|
+
def next_step(executable, arguments)
|
|
43
|
+
return { "run_here" => true } if supported?(@running)
|
|
44
|
+
|
|
45
|
+
other = rbenv_ruby
|
|
46
|
+
if other && !@environment[HANDED_OFF]
|
|
47
|
+
return { "hand_to" => [{ HANDED_OFF => other }, other, executable, *arguments] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{ "refuse" => refusal }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
#: (String) -> bool
|
|
56
|
+
def supported?(version)
|
|
57
|
+
numbers = version.split(".").map(&:to_i)
|
|
58
|
+
(numbers <=> MINIMUM) >= 0 && (numbers <=> BELOW).negative?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The newest supported Ruby among rbenv's installed versions, if any.
|
|
62
|
+
#: () -> String?
|
|
63
|
+
def rbenv_ruby
|
|
64
|
+
root = @environment["RBENV_ROOT"] || File.join(@environment["HOME"].to_s, ".rbenv")
|
|
65
|
+
found = Dir.glob(File.join(root, "versions", "*", "bin", "ruby")).select do |ruby|
|
|
66
|
+
supported?(version_of(ruby)) && File.executable?(ruby)
|
|
67
|
+
end
|
|
68
|
+
found.max_by { |ruby| version_of(ruby).split(".").map(&:to_i) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The version rbenv installed ruby as: the name of its folder under versions/.
|
|
72
|
+
#: (String) -> String
|
|
73
|
+
def version_of(ruby)
|
|
74
|
+
# File.dirname's level argument needs Ruby 3.1, and this runs on older Rubies.
|
|
75
|
+
File.basename(File.dirname(File.dirname(ruby))) # rubocop:disable Style/NestedFileDirname
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# This tool's refusal as JSON, written by hand because json may not load here.
|
|
79
|
+
#: () -> String
|
|
80
|
+
def refusal
|
|
81
|
+
require "rbconfig"
|
|
82
|
+
needed = ">= #{MINIMUM.join(".")} and < #{BELOW.join(".")}"
|
|
83
|
+
fields = [%w[status refused], %w[code unsupported_ruby],
|
|
84
|
+
["message", "This tool needs Ruby #{needed}; this is Ruby #{@running} at #{RbConfig.ruby}, and rbenv " \
|
|
85
|
+
"has no Ruby it supports"],
|
|
86
|
+
["repair", "In Claude Code with the principles plugin, ask the user to run /principles:setup-ruby, " \
|
|
87
|
+
"which installs one with rbenv and ruby-build. Otherwise install a Ruby #{needed} with " \
|
|
88
|
+
"rbenv; it finds any rbenv Ruby it supports, whatever directory it runs in."]]
|
|
89
|
+
quoted = fields.map { |pair| pair.map { |text| "\"#{text.gsub(/[\\"]/) { |c| "\\#{c}" }}\"" }.join(":") }
|
|
90
|
+
"{#{quoted.join(",")}}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The next production release, as major.minor.patch. A production build is versioned with this alone.
|
|
6
|
+
RELEASE = "0.0.1"
|
|
7
|
+
# The version a built gem prints. `rake build[production]` stamps RELEASE here, in the copy of lib/ it
|
|
8
|
+
# packages, and `rake build[development]` stamps RELEASE.g<short sha>. A checkout leaves it nil, and
|
|
9
|
+
# Data::RunningVersion reads the commit this tool runs from instead.
|
|
10
|
+
BUILT_VERSION = "0.0.1" #: String?
|
|
11
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "prompts"
|
|
5
|
+
|
|
6
|
+
module Principles
|
|
7
|
+
# What this tool prints, from plain values a controller hands it. views/CLAUDE.md owns the boundary.
|
|
8
|
+
module Views
|
|
9
|
+
# The edits to one member acknowledged since the last full audit, as the next full audit's request shows them
|
|
10
|
+
# beside the member, so edits judged one at a time are judged together against the last audited text (D33).
|
|
11
|
+
class AcknowledgedSinceLastAudit
|
|
12
|
+
# last_audited_commit is the last full audit's base commit; acknowledgements are the record's own, oldest
|
|
13
|
+
# first; member_lines its own lines then and now, as a one-member audit shows them. The command that reads every
|
|
14
|
+
# edit since is the member's read_the_change, which every member judged again carries (#771).
|
|
15
|
+
#: (last_audited_commit: String, member_lines: Hash[String, untyped],
|
|
16
|
+
#| acknowledgements: Array[Hash[String, untyped]]) -> void
|
|
17
|
+
def initialize(last_audited_commit:, member_lines:, acknowledgements:)
|
|
18
|
+
@last_audited_commit = last_audited_commit
|
|
19
|
+
@member_lines = member_lines
|
|
20
|
+
@acknowledgements = acknowledgements
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The fields as the request prints them.
|
|
25
|
+
#: () -> Hash[String, untyped]
|
|
26
|
+
def to_h
|
|
27
|
+
{ "last_audited_commit" => @last_audited_commit,
|
|
28
|
+
"acknowledgements" => @acknowledgements.map { |each| each.slice("commit", "reason", "acknowledged_at") },
|
|
29
|
+
"member_lines" => @member_lines,
|
|
30
|
+
"why" => Prompts.text("assess-every-member/judge-acknowledged-edits-together") }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "invocation"
|
|
5
|
+
require_relative "member_change_since_baseline"
|
|
6
|
+
require_relative "prompts"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# What this tool prints, from plain values a controller hands it. views/CLAUDE.md owns the boundary.
|
|
10
|
+
module Views
|
|
11
|
+
# A one-member audit's request and its result (D33).
|
|
12
|
+
class AcknowledgementResult
|
|
13
|
+
#: (Invocation) -> void
|
|
14
|
+
def initialize(invocation)
|
|
15
|
+
@invocation = invocation
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# change is the member's change since its baseline; open_findings, the record's findings on the member. What
|
|
20
|
+
# the agent judges is under context, the question last, as in a full audit's request (D60).
|
|
21
|
+
#: (request_id: String, principle_id: String, change: MemberChangeSinceBaseline,
|
|
22
|
+
#| open_findings: Hash[String, Array[String]]) -> Hash[String, untyped]
|
|
23
|
+
def request(request_id:, principle_id:, change:, open_findings:)
|
|
24
|
+
{ "status" => "agent_judgement_required", "request_id" => request_id, "phase" => "acknowledgement",
|
|
25
|
+
"context" => { "principle_id" => principle_id, **change.to_h, **open_findings,
|
|
26
|
+
"question" => Prompts.text("acknowledge-a-change/say-whether-the-change-keeps-the-meaning") },
|
|
27
|
+
"response_template" => { "request_id" => request_id,
|
|
28
|
+
"basis" => { "base_commit" => change.base_commit, "member_id" => change.member_id },
|
|
29
|
+
"meaning_kept" => true, "reason" => "", "old_line" => "", "new_line" => "" },
|
|
30
|
+
"answer_with" => @invocation.submit_answer(request_id) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The acknowledgement was accepted and file holds it.
|
|
34
|
+
#: (principle_id: String, member_id: String, file: String) -> Hash[String, untyped]
|
|
35
|
+
def acknowledged(principle_id:, member_id:, file:)
|
|
36
|
+
{ "status" => "acknowledged", "principle_id" => principle_id, "member_id" => member_id,
|
|
37
|
+
"files_to_commit" => [file],
|
|
38
|
+
"next_action" => "Commit #{file}. #{member_id} reads as judged until it is edited again.\n" \
|
|
39
|
+
"After committing, this no longer lists it: " \
|
|
40
|
+
"#{@invocation.in_project("list-member-problems", principle_id, usage: "--short")}" }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# What this tool prints, from plain values a controller hands it. views/CLAUDE.md owns the boundary.
|
|
6
|
+
module Views
|
|
7
|
+
# How this tool says when a principle's work is in hand, from the activation its map stores (D38): the phrase
|
|
8
|
+
# change-activation's question carries, and that later results will share.
|
|
9
|
+
class ActivationInHand
|
|
10
|
+
# How the question words each value of Models::Activation#full_audit_lists.
|
|
11
|
+
LISTED = {
|
|
12
|
+
"session_start_members" => "The next full audit lists each member that loads at every session start " \
|
|
13
|
+
"anyway, for you to weigh against it.",
|
|
14
|
+
"none" => "Its work is in hand in every session, so a full audit lists no member to weigh against it.",
|
|
15
|
+
"not_built" => "A full audit's listing of members that load outside it is not built yet."
|
|
16
|
+
}.freeze #: Hash[String, String]
|
|
17
|
+
|
|
18
|
+
#: (Hash[String, untyped]) -> void
|
|
19
|
+
def initialize(activation)
|
|
20
|
+
@activation = activation
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# What change-activation's question says it recorded for principle_id, and, from lists, what the next full audit
|
|
25
|
+
# lists against it.
|
|
26
|
+
#: (String, String) -> String
|
|
27
|
+
def recorded(principle_id, lists)
|
|
28
|
+
"Recorded that #{principle_id}'s work is in hand #{phrase}. #{LISTED.fetch(lists)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# What change-activation's question says when the map already held this activation, so nothing changed.
|
|
32
|
+
#: (String) -> String
|
|
33
|
+
def unchanged(principle_id)
|
|
34
|
+
"#{principle_id}'s work was already in hand #{phrase}; nothing was written."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: () -> String
|
|
38
|
+
def phrase
|
|
39
|
+
case @activation.fetch("when")
|
|
40
|
+
when "files" then "when files matching #{@activation.fetch("files").join(", ")} are read or changed"
|
|
41
|
+
when "invoked" then "when its work is invoked, as a skill or a runbook is"
|
|
42
|
+
else "for all work, in every session"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "check_result_template"
|
|
5
|
+
require_relative "prompts"
|
|
6
|
+
require_relative "request_parts"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# What this tool prints, from plain values a controller hands it. views/CLAUDE.md owns the boundary.
|
|
10
|
+
module Views
|
|
11
|
+
# One assessment request start-audit prints: the principle and its members as the agent assesses them, and
|
|
12
|
+
# the answer's template. Each member is an entry of id, role, first_line, last_line, the evidence that locates
|
|
13
|
+
# it and, for a member with a check, the check whose result the template asks for (D30). Its hash travels in
|
|
14
|
+
# the request id, not here.
|
|
15
|
+
class AssessmentRequest
|
|
16
|
+
# The question every member's assessment answers, which a completed record digests with the description (D49).
|
|
17
|
+
#: () -> String
|
|
18
|
+
def self.question = Prompts.text("assess-every-member/assess-each-member-and-the-set")
|
|
19
|
+
|
|
20
|
+
# basis is what the answer hands back unchanged: the base commit and the last completed record's revision.
|
|
21
|
+
# parts are what the agent reads beside the members before the question, such as the files outside the map
|
|
22
|
+
# that differ from the base commit (#409), each with any slots it adds to the template (D60).
|
|
23
|
+
#: (principle_id: String, description: untyped, members: Array[Hash[String, untyped]],
|
|
24
|
+
#| basis: Hash[String, untyped], ?parts: Array[RequestPart]) -> void
|
|
25
|
+
def initialize(principle_id:, description:, members:, basis:, parts: [])
|
|
26
|
+
@principle_id = principle_id
|
|
27
|
+
@description = description
|
|
28
|
+
@members = members
|
|
29
|
+
@basis = basis
|
|
30
|
+
@parts = RequestParts.new(parts) #: RequestParts
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# What the agent assesses, its other parts, the question and a guide to the answer's shapes.
|
|
35
|
+
#: () -> Hash[String, untyped]
|
|
36
|
+
def context
|
|
37
|
+
listed = { "principle_id" => @principle_id, "description" => @description,
|
|
38
|
+
"members" => @members.map do |member|
|
|
39
|
+
member.slice("id", "role", "first_line", "last_line", "check", "check_since_last_audit",
|
|
40
|
+
"check_hidden_characters", "acknowledged_since_last_audit", "carried_forward",
|
|
41
|
+
"judge_because", "moved_from", "read_the_change", "mapped_elsewhere")
|
|
42
|
+
end }
|
|
43
|
+
@parts.shown_after(listed).merge(asked)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The answer to fill in: one assessment per member, and the set's.
|
|
47
|
+
#: () -> Hash[String, untyped]
|
|
48
|
+
def template
|
|
49
|
+
assessed = {
|
|
50
|
+
"basis" => @basis,
|
|
51
|
+
"member_assessments" => @members.map { |member| member_assessment_template(member) },
|
|
52
|
+
"collective_assessment" => "How the members reinforce the principle, and any contradiction or " \
|
|
53
|
+
"missing connection.",
|
|
54
|
+
"recent_decision" => { "unavailable_reason" => "Or use the assessed shape in answer_guide.recent_decision." },
|
|
55
|
+
"findings" => []
|
|
56
|
+
}
|
|
57
|
+
@parts.slotted_after(assessed)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# What the request asks of what the agent read: the question, why members are judged afresh, how to treat the
|
|
63
|
+
# members it carries forward and the roles other principles give their lines, and the guide to the answer's
|
|
64
|
+
# shapes.
|
|
65
|
+
#: () -> Hash[String, untyped]
|
|
66
|
+
def asked
|
|
67
|
+
{ "question" => AssessmentRequest.question }.merge(judge_because_instruction, carried_forward_instruction,
|
|
68
|
+
mapped_elsewhere_instruction)
|
|
69
|
+
.merge("answer_guide" => answer_guide)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Why each member with judge_because is judged afresh, explaining only the values the members carry, so the
|
|
73
|
+
# agent meets each value's meaning where it meets the value (D61); nothing when no member gives one.
|
|
74
|
+
#: () -> Hash[String, untyped]
|
|
75
|
+
def judge_because_instruction
|
|
76
|
+
reasons = @members.filter_map { |member| member["judge_because"] }.uniq
|
|
77
|
+
return {} if reasons.empty?
|
|
78
|
+
|
|
79
|
+
intro, *meanings = Prompts.text("assess-every-member/why-each-member-is-judged-again").split("\n\n")
|
|
80
|
+
shown = meanings.select { |meaning| reasons.any? { |reason| meaning.start_with?("`#{reason}`:") } }
|
|
81
|
+
{ "judge_because_instruction" => [intro, *shown].join("\n\n") }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# How to judge the roles other principles give a member's lines, printed only when a member shares lines with
|
|
85
|
+
# another principle's (#719, D61), and, when an entry names findings open on its member, where to read how to
|
|
86
|
+
# treat them, which prints apart, before the question (#808).
|
|
87
|
+
#: () -> Hash[String, untyped]
|
|
88
|
+
def mapped_elsewhere_instruction
|
|
89
|
+
entries = @members.flat_map { |member| member["mapped_elsewhere"] || [] }
|
|
90
|
+
return {} if entries.empty?
|
|
91
|
+
|
|
92
|
+
roles, open_findings = Prompts.text("assess-every-member/check-roles-other-principles-give-these-lines")
|
|
93
|
+
.split("\n\n")
|
|
94
|
+
shown = entries.any? { |entry| entry["open_finding_ids"] } ? [roles, open_findings] : [roles]
|
|
95
|
+
{ "mapped_elsewhere_instruction" => shown.join("\n\n") }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The answer guide, showing the check result's shapes only when a member has a check, so an audit
|
|
99
|
+
# with no checks costs no more context than before (D30).
|
|
100
|
+
#: () -> Hash[String, untyped]
|
|
101
|
+
def answer_guide
|
|
102
|
+
guide = Prompts.data("assess-every-member/answer-guide")
|
|
103
|
+
@members.any? { |member| member["check"] } ? guide : guide.except("check_result")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# How to answer when the request carries some members' last assessments forward; nothing when it carries none.
|
|
107
|
+
#: () -> Hash[String, untyped]
|
|
108
|
+
def carried_forward_instruction
|
|
109
|
+
return {} unless @members.any? { |member| member["carried_forward"] }
|
|
110
|
+
|
|
111
|
+
{ "carried_forward_instruction" => Prompts.text("assess-every-member/judge-the-set-with-carried-assessments") }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A member's template entry: a fresh assessment to fill in, or, for a member whose last assessment is carried
|
|
115
|
+
# forward, the marker that keeps it (D49); either with its check's result when it has a check.
|
|
116
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
117
|
+
def member_assessment_template(member)
|
|
118
|
+
assessed = if member["carried_forward"]
|
|
119
|
+
{ "member_id" => member.fetch("id"), "carried_forward" => true }
|
|
120
|
+
else
|
|
121
|
+
{ "member_id" => member.fetch("id"), "assessment" => "Your assessment, including its context cost",
|
|
122
|
+
"delivery" => "not_observed", "evidence" => member.fetch("evidence") }
|
|
123
|
+
end
|
|
124
|
+
check = member["check"] or return assessed
|
|
125
|
+
template = CheckResultTemplate.new(check, member["check_since_last_audit"],
|
|
126
|
+
member.fetch("check_hidden_characters", []))
|
|
127
|
+
assessed.merge("check_result" => template.to_h)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "audit_commit"
|
|
5
|
+
require_relative "landed_since_base"
|
|
6
|
+
require_relative "principle_with_changed_members"
|
|
7
|
+
require_relative "shell_word"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# What this tool prints, from plain values a controller hands it. views/CLAUDE.md owns the boundary.
|
|
11
|
+
module Views
|
|
12
|
+
# What a completed audit changed in the project, as its result prints it: the files to commit, in git what
|
|
13
|
+
# landed since the base commit, as LandedSinceBase prints it, each other principle whose members its fixes changed
|
|
14
|
+
# (#410), each file the record cites that no longer exists (#861), and each member added to the map during the
|
|
15
|
+
# audit, which no assessment read, so the next full audit assesses it (T69).
|
|
16
|
+
class AuditChanges
|
|
17
|
+
#: (files_to_commit: Array[String], ?landed: LandedSinceBase?,
|
|
18
|
+
#| ?other_changed: Array[PrincipleWithChangedMembers], ?cited_files_missing: Array[String],
|
|
19
|
+
#| ?added: Array[String]) -> void
|
|
20
|
+
def initialize(files_to_commit:, landed: nil, other_changed: [], cited_files_missing: [], added: [])
|
|
21
|
+
@files_to_commit = files_to_commit
|
|
22
|
+
@added = added
|
|
23
|
+
@landed = landed
|
|
24
|
+
@other_changed = other_changed
|
|
25
|
+
@cited_files_missing = cited_files_missing
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The files to commit, and each other principle whose members the audit's fixes changed, as the next step
|
|
30
|
+
# names them.
|
|
31
|
+
#: Array[String]
|
|
32
|
+
attr_reader :files_to_commit
|
|
33
|
+
#: Array[PrincipleWithChangedMembers]
|
|
34
|
+
attr_reader :other_changed
|
|
35
|
+
|
|
36
|
+
# The fields as printed; the members added and the cited files missing, what landed since the base only in
|
|
37
|
+
# git, where it is given, and the other principles, each only when there are some.
|
|
38
|
+
#: () -> Hash[String, untyped]
|
|
39
|
+
def to_h
|
|
40
|
+
printed = { "files_to_commit" => @files_to_commit } #: Hash[String, untyped]
|
|
41
|
+
printed = printed.merge("members_added_awaiting_assessment" => @added) unless @added.empty?
|
|
42
|
+
printed = printed.merge("cited_files_missing" => @cited_files_missing) unless @cited_files_missing.empty?
|
|
43
|
+
printed = printed.merge(@landed.to_h) if @landed
|
|
44
|
+
others = @other_changed.map(&:to_h)
|
|
45
|
+
others.empty? ? printed : printed.merge("other_principles_with_changed_members" => others)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Refuses, staging nothing, while a merge is unfinished, whose commit would otherwise take in these files
|
|
49
|
+
# (758f12b, #811), saying why on stderr, the one place the stop is explained (#828). It fails with false, not
|
|
50
|
+
# exit, so a shell running it outside the printed subshell survives; the braces keep a failed cd from reading
|
|
51
|
+
# as a merge. The message is in single quotes, so it holds no apostrophe.
|
|
52
|
+
NO_MERGE_UNFINISHED = '{ test ! -e "$(git rev-parse --git-path MERGE_HEAD)" || ' \
|
|
53
|
+
"{ echo 'principles: a merge is unfinished; finish or abort it, then run this again' " \
|
|
54
|
+
">&2; false; }; }" #: String
|
|
55
|
+
|
|
56
|
+
# In git, the command that commits the files to commit and nothing else staged, with subject, run from
|
|
57
|
+
# project, where those paths are relative. Each path is quoted for the shell and read by git as written, since
|
|
58
|
+
# project files anyone may have written name them (#227, #811). nil outside git, where it could not run.
|
|
59
|
+
#: (String, String) -> String?
|
|
60
|
+
def commit_with(subject, project)
|
|
61
|
+
return unless @landed
|
|
62
|
+
|
|
63
|
+
files = @files_to_commit.map { |file| ShellWord.new(file) }.join(" ")
|
|
64
|
+
"(cd #{ShellWord.new(project)} && #{NO_MERGE_UNFINISHED} && git --literal-pathspecs add -- #{files} && " \
|
|
65
|
+
"git --literal-pathspecs commit -m #{ShellWord.new(subject)} -- #{files})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# How the result says to commit these files with subject, run from project: in git with commit_with's
|
|
69
|
+
# command, outside git by hand.
|
|
70
|
+
#: (String, String) -> (AuditCommit | AuditCommit::ByHand)
|
|
71
|
+
def commit(subject, project)
|
|
72
|
+
command = commit_with(subject, project)
|
|
73
|
+
return AuditCommit::ByHand.new(@files_to_commit) unless command
|
|
74
|
+
|
|
75
|
+
AuditCommit.new(files_to_commit: @files_to_commit, subject:, command:)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|