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,112 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "answer_fields"
|
|
6
|
+
require_relative "answer_place"
|
|
7
|
+
require_relative "audit_record"
|
|
8
|
+
require_relative "project_history"
|
|
9
|
+
require_relative "request_id"
|
|
10
|
+
require_relative "write_up_repoint"
|
|
11
|
+
|
|
12
|
+
module Principles
|
|
13
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
14
|
+
module Models
|
|
15
|
+
# A repoint of a principle's open findings' write-ups without a full audit (D59): the agent, having checked
|
|
16
|
+
# where a write-up moved to, gives each finding it names a new recorded_at, and the last completed record keeps
|
|
17
|
+
# it, with where it pointed before, when and why. Judging that the new place holds the write-up is the agent's.
|
|
18
|
+
class Repoint
|
|
19
|
+
PHASE = "repointing"
|
|
20
|
+
|
|
21
|
+
# record is the principle's audit record now; history says whether a new pointer's file is committed.
|
|
22
|
+
#: (principle_id: String, record: AuditRecord?, history: ProjectHistory) -> void
|
|
23
|
+
def initialize(principle_id:, record:, history:)
|
|
24
|
+
@principle_id = principle_id
|
|
25
|
+
@record = record
|
|
26
|
+
@history = history
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The request's id, digesting the last completed record's revision it was printed after, so an answer is
|
|
31
|
+
# matched to the record it read.
|
|
32
|
+
#: () -> RequestId
|
|
33
|
+
def request_id = RequestId.for(principle_id: @principle_id, phase: PHASE, parts: [revision])
|
|
34
|
+
|
|
35
|
+
# The completed record's revision, which the request prints and the answer hands back.
|
|
36
|
+
#: () -> String
|
|
37
|
+
def revision = completed.revision
|
|
38
|
+
|
|
39
|
+
# The record with each repoint the answer gives in it, once each names a different open finding and each new
|
|
40
|
+
# pointer is located by the block, in a committed file; a repoint whose recorded_at is null leaves its
|
|
41
|
+
# finding as it is.
|
|
42
|
+
#: (untyped, at: String) { (Locator) -> void } -> AuditRecord
|
|
43
|
+
def accepted(response, at:, &locate)
|
|
44
|
+
fields = AnswerFields.new(locate)
|
|
45
|
+
repoints = response.fetch("repoints")
|
|
46
|
+
each_open_once(repoints.map { |repoint| repoint.fetch("finding_id") })
|
|
47
|
+
given = repoints.each_with_index.select { |repoint, _| repoint["recorded_at"] }
|
|
48
|
+
completed.repointed(given.map { |repoint, index| located(repoint, index, fields) }, at: at)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The ids of the completed record's open findings that repointed, the record accepted, points elsewhere (T16).
|
|
52
|
+
#: (AuditRecord) -> Array[String]
|
|
53
|
+
def moved_ids(repointed) = repointed.write_ups_moved_since(completed)
|
|
54
|
+
|
|
55
|
+
# The ids of the rest, which repointed leaves where they pointed, so a repeat or a skipped finding is seen as one.
|
|
56
|
+
#: (AuditRecord) -> Array[String]
|
|
57
|
+
def unchanged_ids(repointed) = completed.open_ids - moved_ids(repointed)
|
|
58
|
+
|
|
59
|
+
# Refuses unless the principle's record is a completed audit's: an open record outside git keeps the completed
|
|
60
|
+
# one, whose findings its correction will decide, and with none there is nothing to repoint (D59).
|
|
61
|
+
#: () -> AuditRecord
|
|
62
|
+
def completed
|
|
63
|
+
record = @record
|
|
64
|
+
return record if record&.completed?
|
|
65
|
+
|
|
66
|
+
start = "#{Error::EXECUTABLE} start-audit #{@principle_id}"
|
|
67
|
+
if record
|
|
68
|
+
raise Error.new("cannot_repoint",
|
|
69
|
+
"#{@principle_id} has an audit left open, whose correction decides its findings",
|
|
70
|
+
"Answer that correction, or start the audit again: #{start}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
raise Error.new("cannot_repoint", "#{@principle_id} has no completed audit, so no finding is open",
|
|
74
|
+
"Run a full audit first: #{start}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Refuses ids, the findings an answer's repoints name, when one is not open in the completed record or two
|
|
80
|
+
# name one finding, since either would otherwise read as a success (#760, T20).
|
|
81
|
+
#: (Array[String]) -> void
|
|
82
|
+
def each_open_once(ids)
|
|
83
|
+
open = completed.open_ids
|
|
84
|
+
unknown = ids - open
|
|
85
|
+
unless unknown.empty?
|
|
86
|
+
raise Error.new("unknown_finding", "#{@principle_id}'s last completed audit leaves no finding " \
|
|
87
|
+
"#{unknown.join(", ")} open",
|
|
88
|
+
"Repoint only its open findings: #{open.join(", ")}")
|
|
89
|
+
end
|
|
90
|
+
each_once(ids)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (Array[String]) -> void
|
|
94
|
+
def each_once(ids)
|
|
95
|
+
repeated = ids.tally.select { |_, count| count > 1 }.keys
|
|
96
|
+
return if repeated.empty?
|
|
97
|
+
|
|
98
|
+
raise Error.new("duplicate_finding", "Repoints name #{repeated.join(", ")} more than once",
|
|
99
|
+
"Give each finding one repoint.")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The repoint at index, its recorded_at located and committed, and its reason given.
|
|
103
|
+
#: (Hash[String, untyped], Integer, AnswerFields) -> WriteUpRepoint
|
|
104
|
+
def located(repoint, index, fields)
|
|
105
|
+
place = AnswerPlace.new("/repoints/#{index}", "Finding #{repoint["finding_id"]}")
|
|
106
|
+
pointer = fields.write_up_pointer(repoint["recorded_at"], place.child("recorded_at"))
|
|
107
|
+
@history.refuse_uncommitted_write_up(pointer.path)
|
|
108
|
+
WriteUpRepoint.answered(repoint, pointer, fields, place)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "answer_place"
|
|
6
|
+
require_relative "answer_text"
|
|
7
|
+
require_relative "dated_line"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# One member's check and the result an agent reports for it (D30). This tool never runs the check: it
|
|
13
|
+
# refuses a report that cannot be about the member's own check, and cannot confirm the run happened.
|
|
14
|
+
class ReportedCheck
|
|
15
|
+
SHAPES = "Give check_result as answer_guide.check_result shows."
|
|
16
|
+
BASIS = "Say in review_basis, in one line, what the review before the run rested on, such as " \
|
|
17
|
+
"\"The owner reviewed the command in this session.\" or the document that confirms it. If you did " \
|
|
18
|
+
"the review yourself, such as confirming against the documentation a check you changed, say so: " \
|
|
19
|
+
"\"Self-reviewed by the agent: <what you checked it against>\". If no record of the review was " \
|
|
20
|
+
"kept, say so, such as \"Reviewed in an earlier session; basis not kept.\"" #: String
|
|
21
|
+
|
|
22
|
+
# assessment is the place of the member's assessment in the answer, whose check_result each refusal points
|
|
23
|
+
# into (#817). ran is the command the principle's stored record was given an exit status for, nil when it ran
|
|
24
|
+
# none; a check it did not run is reviewed before it runs, and the result says in review_basis what that review
|
|
25
|
+
# rested on (D63). kept is the line the stored record keeps for that run's review, with its
|
|
26
|
+
# review_basis_since, which an unchanged check's result carries until the answer gives another (D52).
|
|
27
|
+
#: (AnswerPlace, String?, ran: String?, kept: Hash[String, untyped]) -> void
|
|
28
|
+
def initialize(assessment, check, ran:, kept:)
|
|
29
|
+
@assessment = assessment
|
|
30
|
+
@check = check
|
|
31
|
+
@ran = ran
|
|
32
|
+
@kept = kept
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The reported result, once it is checked against the member's check; nil when it has none.
|
|
37
|
+
#: (untyped) -> Hash[String, untyped]?
|
|
38
|
+
def result(reported)
|
|
39
|
+
return no_check(reported) unless @check
|
|
40
|
+
|
|
41
|
+
missing_result unless reported
|
|
42
|
+
same_command(reported.fetch("command"))
|
|
43
|
+
status_or_reason(reported)
|
|
44
|
+
review_basis(reported)
|
|
45
|
+
carrying_review(reported)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The place of the reported check_result, or of one of its fields.
|
|
51
|
+
#: (?String?) -> AnswerPlace
|
|
52
|
+
def reported_at(field = nil)
|
|
53
|
+
place = @assessment.child("check_result")
|
|
54
|
+
field ? place.child(field) : place
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Refuses an assessment of a checked member that reports no result for its check, pointing at the assessment
|
|
58
|
+
# it is missing from.
|
|
59
|
+
#: () -> bot
|
|
60
|
+
def missing_result
|
|
61
|
+
refuse(@assessment, "the member has a check, which context.members gives, so its assessment needs a " \
|
|
62
|
+
"check_result",
|
|
63
|
+
"Report the member's check once you have run it, after a review if " \
|
|
64
|
+
"answer_guide.check_result.limits asks for one. #{SHAPES}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: (untyped) -> nil
|
|
68
|
+
def no_check(reported)
|
|
69
|
+
return unless reported
|
|
70
|
+
|
|
71
|
+
refuse(reported_at, "the member has no check, so its assessment takes no check_result",
|
|
72
|
+
"Remove check_result from this member's assessment.")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#: (String) -> void
|
|
76
|
+
def same_command(command)
|
|
77
|
+
return if command == @check
|
|
78
|
+
|
|
79
|
+
refuse(reported_at("command"), "check_result.command is not the member's check as context.members gives it",
|
|
80
|
+
"Report the command exactly as context.members lists it, run after a review if " \
|
|
81
|
+
"answer_guide.check_result.limits asks for one.")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#: (Hash[String, untyped]) -> void
|
|
85
|
+
def status_or_reason(reported)
|
|
86
|
+
unless reported.key?("exit_status") ^ reported.key?("not_run_reason")
|
|
87
|
+
refuse(reported_at, "check_result needs exactly one of exit_status and not_run_reason",
|
|
88
|
+
"Give exit_status once the command has run, or not_run_reason if it could not, including when " \
|
|
89
|
+
"no review it needed was possible. #{SHAPES}")
|
|
90
|
+
end
|
|
91
|
+
return if !reported.key?("not_run_reason") || AnswerText.new(reported["not_run_reason"]).written?
|
|
92
|
+
|
|
93
|
+
refuse(reported_at("not_run_reason"), "check_result.not_run_reason must be a nonempty string",
|
|
94
|
+
"Say why the command could not be run here.")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Refuses an exit status for a check the stored record did not run unless review_basis says what its review
|
|
98
|
+
# rested on, and any review_basis that is not one written line. What the line says is the agent's: this tool
|
|
99
|
+
# keeps it and cannot verify it.
|
|
100
|
+
#: (Hash[String, untyped]) -> void
|
|
101
|
+
def review_basis(reported)
|
|
102
|
+
if reported.key?("review_basis")
|
|
103
|
+
return if AnswerText.new(reported["review_basis"]).one_line?
|
|
104
|
+
|
|
105
|
+
refuse(reported_at("review_basis"), "check_result.review_basis is not one written line", BASIS)
|
|
106
|
+
end
|
|
107
|
+
return unless reported.key?("exit_status") && @ran != @check
|
|
108
|
+
|
|
109
|
+
refuse(reported_at("review_basis"),
|
|
110
|
+
"check_result needs review_basis beside exit_status, since no audit ran this command", BASIS)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The result as the record keeps it: an unchanged check's exit status with the kept line and its date when the
|
|
114
|
+
# answer gives the same line or none, or the answer's new line alone; any other result as reported.
|
|
115
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
116
|
+
def carrying_review(reported)
|
|
117
|
+
return reported unless reported.key?("exit_status") && @ran == @check
|
|
118
|
+
|
|
119
|
+
line = DatedLine.new(reported.fetch("review_basis", @kept["review_basis"]), field: "review_basis")
|
|
120
|
+
reported.except("review_basis").merge(line.kept_over(@kept))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Refuses with what is wrong at place, which the refusal names with whose it is and points at (#817).
|
|
124
|
+
#: (AnswerPlace, String, String) -> bot
|
|
125
|
+
def refuse(place, message, repair)
|
|
126
|
+
raise place.refusal(Error.new("invalid_check_result", message, repair))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../error"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The id of a judgement request: the principle, the phase and a digest of everything the request
|
|
12
|
+
# depends on, as ID.PHASE.DIGEST. This tool stores no request, so an answer is matched by recomputing
|
|
13
|
+
# the digest from the project as it is now: a different digest means something the request depended
|
|
14
|
+
# on has changed (D25). The digest has no secret, so it catches mistakes, not forgery.
|
|
15
|
+
class RequestId
|
|
16
|
+
PHASES = %w[assessment correction acknowledgement repointing].freeze
|
|
17
|
+
FORMAT = /\A(?<principle>[a-z0-9]+(?:-[a-z0-9]+)*)\.(?<phase>[a-z]+)\.(?<digest>[0-9a-f]{12})\z/
|
|
18
|
+
|
|
19
|
+
#: String
|
|
20
|
+
attr_reader :principle_id
|
|
21
|
+
#: String
|
|
22
|
+
attr_reader :phase
|
|
23
|
+
|
|
24
|
+
# The id of a request in phase for principle_id, which depends on parts.
|
|
25
|
+
#: (principle_id: String, phase: String, parts: Array[untyped]) -> RequestId
|
|
26
|
+
def self.for(principle_id:, phase:, parts:)
|
|
27
|
+
new(principle_id, phase, Digest::SHA256.hexdigest(JSON.generate([principle_id, phase, *parts]))[0, 12].to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# An id as an agent passed it, refused unless it has the shape this tool prints.
|
|
31
|
+
#: (String) -> RequestId
|
|
32
|
+
def self.parse(text)
|
|
33
|
+
match = FORMAT.match(text)
|
|
34
|
+
unless match && PHASES.include?(match[:phase])
|
|
35
|
+
raise Error.new("unknown_request", "#{text} does not have the form of a request id",
|
|
36
|
+
"Use the request_id of the latest start-audit or submit-answer result.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
new(match[:principle].to_s, match[:phase].to_s, match[:digest].to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#: (String, String, String) -> void
|
|
43
|
+
def initialize(principle_id, phase, digest)
|
|
44
|
+
@principle_id = principle_id
|
|
45
|
+
@phase = phase
|
|
46
|
+
@digest = digest
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Refuses answer unless it is an object whose request_id is this id.
|
|
51
|
+
#: (untyped) -> void
|
|
52
|
+
def check_answer(answer)
|
|
53
|
+
return if answer.is_a?(Hash) && answer["request_id"] == to_s
|
|
54
|
+
|
|
55
|
+
raise Error.new("request_mismatch", "answer.request_id must equal #{self}",
|
|
56
|
+
"Start from the response_template of this request.")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#: () -> String
|
|
60
|
+
def to_s
|
|
61
|
+
"#{@principle_id}.#{@phase}.#{@digest}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: (untyped) -> bool
|
|
65
|
+
def ==(other)
|
|
66
|
+
other.is_a?(RequestId) ? other.to_s == to_s : false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# Where this run of the tool comes from, by the executable its printed commands name. The Claude Code plugin's
|
|
8
|
+
# bin/principles is the one entry point that names itself bare, since its folder is on the Bash tool's PATH
|
|
9
|
+
# while the plugin is enabled (#528); a checkout or an installed gem names a Ruby and a path. Only under the
|
|
10
|
+
# plugin does its session-start hook report beside CLAUDE.md (D41).
|
|
11
|
+
class RunsFrom
|
|
12
|
+
# The executable the plugin's entry point names itself by.
|
|
13
|
+
PLUGIN_EXECUTABLE = "principles"
|
|
14
|
+
|
|
15
|
+
#: (String) -> void
|
|
16
|
+
def initialize(executable)
|
|
17
|
+
@executable = executable
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Where this run comes from, as a fact the agent reads.
|
|
22
|
+
#: () -> String
|
|
23
|
+
def to_s = plugin? ? "claude_code_plugin" : "command_line"
|
|
24
|
+
|
|
25
|
+
# Whether the plugin's session-start hook runs in the session this run is part of, which only a run from the
|
|
26
|
+
# plugin shows; it says nothing of any other person's sessions.
|
|
27
|
+
#: () -> bool
|
|
28
|
+
def session_start_hook_in_this_session? = plugin?
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
#: () -> bool
|
|
33
|
+
def plugin? = @executable == PLUGIN_EXECUTABLE
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "frontmatter_fields"
|
|
6
|
+
require_relative "json_path_query"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# When Claude Code loads a skill: its body when the skill is invoked, and its description, which tells the
|
|
12
|
+
# model the skill exists, at session start, on the files its paths field names, or, for a skill in a folder
|
|
13
|
+
# below the project root, when a file there is first touched. A skill only a person invokes puts nothing in
|
|
14
|
+
# the model's context until they do. This follows Claude Code's skills documentation and the #354 experiment.
|
|
15
|
+
# This tool shows the paths field as written, and reads disable-model-invocation only as a plain true or false.
|
|
16
|
+
class SkillLoads
|
|
17
|
+
# A skill's own file: SKILL.md in a skill's folder in a .claude/skills folder, at the root or below it.
|
|
18
|
+
SKILL_FILE = %r{\A(?<folder>(?:.*/)?)\.claude/skills/[^/]+/SKILL\.md\z} #: Regexp
|
|
19
|
+
|
|
20
|
+
PERSON_ONLY = "disable-model-invocation" #: String
|
|
21
|
+
|
|
22
|
+
# The characters of a skill's description and when_to_use the listing keeps (code.claude.com/docs/en/skills,
|
|
23
|
+
# read 2026-11-09); a project can set another with skillListingMaxDescChars, which this tool does not read.
|
|
24
|
+
LISTING_LIMIT = 1536 #: Integer
|
|
25
|
+
|
|
26
|
+
# text is the skill's whole file, or nil when it cannot be read; query is the member's, when it is a
|
|
27
|
+
# frontmatter field of the skill.
|
|
28
|
+
#: (String, String?, ?query: String?) -> void
|
|
29
|
+
def initialize(path, text, query: nil)
|
|
30
|
+
@path = path
|
|
31
|
+
@text = text
|
|
32
|
+
@query = query
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: () -> Hash[String, untyped]
|
|
37
|
+
def to_h
|
|
38
|
+
lines = @text&.lines or return { "when" => "not_determined" }
|
|
39
|
+
frontmatter = FrontmatterFields.new(path: @path, lines: lines)
|
|
40
|
+
person_only = person_only(frontmatter)
|
|
41
|
+
return not_derived("#{PERSON_ONLY} is not a plain true or false") if person_only.nil?
|
|
42
|
+
|
|
43
|
+
description = description(frontmatter, person_only)
|
|
44
|
+
return description if description.key?("not_derived")
|
|
45
|
+
|
|
46
|
+
description_member? ? description.merge("skill_part" => "description") : body(person_only, description)
|
|
47
|
+
rescue Error => e
|
|
48
|
+
{ "when" => "not_determined", "frontmatter_refused" => e.payload.fetch("code") }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# What the skill listing loads of the skill: its description and when_to_use lines as written, cut at the
|
|
52
|
+
# listing's limit; nil for a skill only a person invokes, which the listing leaves out, or one this tool cannot
|
|
53
|
+
# read.
|
|
54
|
+
#: () -> String?
|
|
55
|
+
def description_text
|
|
56
|
+
lines = @text&.lines or return nil
|
|
57
|
+
frontmatter = FrontmatterFields.new(path: @path, lines: lines)
|
|
58
|
+
return nil unless person_only(frontmatter) == false
|
|
59
|
+
|
|
60
|
+
%w[description when_to_use].filter_map { |field| frontmatter.field_lines("$.#{field}")&.join }
|
|
61
|
+
.join[0, LISTING_LIMIT]
|
|
62
|
+
rescue Error
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# When the skill's body loads, who may invoke it, and when its description loads.
|
|
69
|
+
#: (bool, Hash[String, untyped]) -> Hash[String, untyped]
|
|
70
|
+
def body(person_only, description)
|
|
71
|
+
{ "when" => "on_invocation", "invoked_by" => person_only ? "person" : "model_or_person",
|
|
72
|
+
"description" => description }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether the member is the skill's description field, however its query names it. The docs summary says
|
|
76
|
+
# when_to_use joins the description in the listing; until that is read verbatim, it keeps the body's answer.
|
|
77
|
+
#: () -> bool
|
|
78
|
+
def description_member?
|
|
79
|
+
query = @query or return false
|
|
80
|
+
JsonPathQuery.new(query).top_level_name == "description"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether only a person invokes the skill: false without the field, nil when it is not a plain true or false.
|
|
84
|
+
#: (FrontmatterFields) -> bool?
|
|
85
|
+
def person_only(frontmatter)
|
|
86
|
+
field = frontmatter.field_lines("$[\"#{PERSON_ONLY}\"]") or return false
|
|
87
|
+
value = field.join.strip.delete_prefix("#{PERSON_ONLY}:").strip
|
|
88
|
+
value == "true" if %w[true false].include?(value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# When the description loads.
|
|
92
|
+
#: (FrontmatterFields, bool) -> Hash[String, untyped]
|
|
93
|
+
def description(frontmatter, person_only)
|
|
94
|
+
return { "when" => "not_loaded" } if person_only
|
|
95
|
+
|
|
96
|
+
paths = frontmatter.field_lines("$.paths")
|
|
97
|
+
return on_paths(paths) if paths
|
|
98
|
+
return { "when" => "on_files", "files" => [folder] } unless folder.empty?
|
|
99
|
+
|
|
100
|
+
{ "when" => "session_start" }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The description's moment for a skill with a paths field, shown as written.
|
|
104
|
+
#: (Array[String]) -> Hash[String, untyped]
|
|
105
|
+
def on_paths(paths)
|
|
106
|
+
return not_derived("a skill below the project root with a paths field") unless folder.empty?
|
|
107
|
+
|
|
108
|
+
{ "when" => "on_files", "paths_field" => paths.map(&:chomp) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The folder whose .claude/skills holds the skill: empty at the project root, else ending in a slash.
|
|
112
|
+
#: () -> String
|
|
113
|
+
def folder
|
|
114
|
+
SKILL_FILE.match(@path)&.[](:folder).to_s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#: (String) -> Hash[String, untyped]
|
|
118
|
+
def not_derived(why)
|
|
119
|
+
{ "when" => "not_determined", "not_derived" => why }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "answer_fields"
|
|
5
|
+
require_relative "answer_place"
|
|
6
|
+
require_relative "answer_text"
|
|
7
|
+
require_relative "evidence_replacements"
|
|
8
|
+
require_relative "open_finding"
|
|
9
|
+
|
|
10
|
+
module Principles
|
|
11
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
12
|
+
module Models
|
|
13
|
+
# What a decision on a prior finding restates of that finding, which only a still_holds decision may: the
|
|
14
|
+
# problem its record lacks, a new pointer to its write-up (D35), the evidence its record lacks (D64) and a new
|
|
15
|
+
# place for each kept evidence item no longer found (D70).
|
|
16
|
+
# PriorFindingDecision checks the rest of the decision, and refuses for this through refuse.
|
|
17
|
+
class StillHoldsRestatement
|
|
18
|
+
# finding is the prior finding as its record left it open; place is the decision's place in the answer; refuse
|
|
19
|
+
# raises the decision's refusal for a reason and a repair, or the decision's own repair for nil.
|
|
20
|
+
#: (Hash[String, untyped], OpenFinding, AnswerPlace, refuse: ^(String, String?) -> void) -> void
|
|
21
|
+
def initialize(decision, finding, place, refuse:)
|
|
22
|
+
@decision = decision
|
|
23
|
+
@finding = finding
|
|
24
|
+
@place = place
|
|
25
|
+
@refuse = refuse
|
|
26
|
+
@id = finding.id #: String
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Refuses a restatement on any decision but still_holds, a problem the finding already states or that is not
|
|
31
|
+
# one line, kept evidence it replaces but that is still found or named by no replaces, and a pointer locate
|
|
32
|
+
# cannot resolve.
|
|
33
|
+
#: (^(Locator) -> void) -> void
|
|
34
|
+
def check(locate)
|
|
35
|
+
evidence(locate) if @decision.key?("problem_evidence")
|
|
36
|
+
return if @decision.slice("problem", "recorded_at").empty?
|
|
37
|
+
|
|
38
|
+
unless @decision["decision"] == "still_holds"
|
|
39
|
+
@refuse.call("only still_holds may give a problem or recorded_at",
|
|
40
|
+
"Drop problem and recorded_at from #{@id}'s decision, or decide it still_holds.")
|
|
41
|
+
end
|
|
42
|
+
problem if @decision.key?("problem")
|
|
43
|
+
return unless @decision["recorded_at"]
|
|
44
|
+
|
|
45
|
+
AnswerFields.new(locate).pointer(@decision["recorded_at"], @place.child("recorded_at"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The evidence a still_holds decision may give: a finding whose record keeps none, its evidence (D64), and one
|
|
51
|
+
# whose record keeps it, the new place of each kept item no longer found, which EvidenceReplacements checks, so a
|
|
52
|
+
# decision never rewrites what an audit found (D70). This checks only that each pointer resolves.
|
|
53
|
+
#: (^(Locator) -> void) -> void
|
|
54
|
+
def evidence(locate)
|
|
55
|
+
unless @decision["decision"] == "still_holds"
|
|
56
|
+
@refuse.call("only still_holds may give problem_evidence",
|
|
57
|
+
"Drop problem_evidence from #{@id}'s decision, or decide it still_holds.")
|
|
58
|
+
end
|
|
59
|
+
place = @place.child("problem_evidence")
|
|
60
|
+
if @finding.evidence_kept?
|
|
61
|
+
return @finding.replacements(@decision["problem_evidence"]).check(locate, @id, place, refuse: @refuse)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
lacking(locate, place)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The evidence a decision gives a finding whose record keeps none, which has nothing to replace (D64).
|
|
68
|
+
#: (^(Locator) -> void, AnswerPlace) -> void
|
|
69
|
+
def lacking(locate, place)
|
|
70
|
+
given = @decision["problem_evidence"]
|
|
71
|
+
if given.any? { |entry| entry.key?(EvidenceReplacements::REPLACES) }
|
|
72
|
+
@refuse.call("its record keeps no evidence to replace", "Drop replaces from #{@id}'s problem_evidence.")
|
|
73
|
+
end
|
|
74
|
+
AnswerFields.new(locate).evidence(given, place)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: () -> void
|
|
78
|
+
def problem
|
|
79
|
+
if @finding.states_problem?
|
|
80
|
+
@refuse.call("it already states its problem",
|
|
81
|
+
"Drop problem from #{@id}'s decision, or raise it again as a new finding to restate it.")
|
|
82
|
+
end
|
|
83
|
+
@refuse.call("problem must be one line", nil) unless AnswerText.new(@decision["problem"]).one_line?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
# How big a text that loads into context is: its bytes, and its tokens estimated as characters / 4, rounded up,
|
|
8
|
+
# an estimate rather than a tokenizer count. A text not measured has no size, so nothing is printed for it.
|
|
9
|
+
class TextSize
|
|
10
|
+
#: (String?) -> void
|
|
11
|
+
def initialize(text)
|
|
12
|
+
@text = text
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The size under fields' two names, its bytes then its tokens; empty when there is no text.
|
|
17
|
+
#: ([String, String]) -> Hash[String, Integer]
|
|
18
|
+
def under(fields)
|
|
19
|
+
text = @text or return {}
|
|
20
|
+
|
|
21
|
+
bytes, tokens = fields
|
|
22
|
+
{ bytes => text.bytesize, tokens => (text.length / 4.0).ceil }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Principles
|
|
5
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
6
|
+
module Models
|
|
7
|
+
# The project's files outside a principle's map that differ from the audit's base commit, as an assessment
|
|
8
|
+
# request names them (#409). A project with many untracked files would put every one into each request, so
|
|
9
|
+
# it names the first few and counts the rest (#426, D38).
|
|
10
|
+
class UncommittedOutsideTheMap
|
|
11
|
+
# The most paths a request names: enough to show a pattern, such as a generated folder, in a short list of
|
|
12
|
+
# its own; git status lists them all.
|
|
13
|
+
NAMED_AT_MOST = 10
|
|
14
|
+
|
|
15
|
+
#: (Array[String]) -> void
|
|
16
|
+
def initialize(paths)
|
|
17
|
+
@paths = paths
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The paths a request names, sorted, and how many beyond them it does not, when any.
|
|
22
|
+
#: () -> Hash[String, untyped]
|
|
23
|
+
def shown
|
|
24
|
+
named = { "paths" => @paths.sort.first(NAMED_AT_MOST) }
|
|
25
|
+
not_named = @paths.length - NAMED_AT_MOST
|
|
26
|
+
not_named.positive? ? named.merge("not_named" => not_named) : named
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "answer_text"
|
|
6
|
+
require_relative "dated_line"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# The one line an answer may give on a deferred finding, saying whom it waits on and to decide what (D52).
|
|
12
|
+
# What the line says is the agent's judgement; this tool checks only that it is one line, on a finding left
|
|
13
|
+
# deferred. A null stands on any finding, so a template's null kept on another action is not refused.
|
|
14
|
+
class WaitsOn
|
|
15
|
+
# What to write instead of a line this tool cannot keep.
|
|
16
|
+
REPAIR = "Say in one line whom the deferral waits on and to decide what, or set waits_on to null." #: String
|
|
17
|
+
|
|
18
|
+
# line is what the answer gave, and action the finding's action once the answer is applied, or nil when the
|
|
19
|
+
# answer does not leave it open.
|
|
20
|
+
#: (untyped, action: String?) -> void
|
|
21
|
+
def initialize(line, action:)
|
|
22
|
+
@line = line
|
|
23
|
+
@action = action
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Refuses the line, with code and naming subject, unless it is null, or one line on a finding left deferred.
|
|
28
|
+
#: (code: String, subject: String) -> void
|
|
29
|
+
def check(code:, subject:)
|
|
30
|
+
return if @line.nil?
|
|
31
|
+
|
|
32
|
+
unless deferred?
|
|
33
|
+
raise Error.new(code, "#{subject} gives waits_on, but only a deferred finding waits on anyone",
|
|
34
|
+
"Set waits_on to null, or defer the finding.")
|
|
35
|
+
end
|
|
36
|
+
return if AnswerText.new(@line).one_line?
|
|
37
|
+
|
|
38
|
+
raise Error.new(code, "#{subject} gives a waits_on that is not one line", REPAIR)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# What a kept finding waits on once its decision is applied, this being the line the decision gave or, when it
|
|
42
|
+
# gave none, the one it carried: had, the waits_on and waits_on_since it carried, when the line is unchanged,
|
|
43
|
+
# so an ask keeps its date; this line alone, dated by the audit that records it, when new; nothing when the
|
|
44
|
+
# finding is no longer deferred or the decision gave null.
|
|
45
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
46
|
+
def kept_over(had)
|
|
47
|
+
return {} unless deferred?
|
|
48
|
+
|
|
49
|
+
DatedLine.new(@line, field: "waits_on").kept_over(had)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
#: () -> bool
|
|
55
|
+
def deferred? = @action == "deferred"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|