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,249 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "audit_start"
|
|
5
|
+
require_relative "cited_files"
|
|
6
|
+
require_relative "decided_prior_findings"
|
|
7
|
+
require_relative "open_finding"
|
|
8
|
+
require_relative "principles_package"
|
|
9
|
+
require_relative "recorded_findings"
|
|
10
|
+
require_relative "recorded_members"
|
|
11
|
+
require_relative "json_digest"
|
|
12
|
+
require_relative "write_up_repoint"
|
|
13
|
+
require_relative "../data/schema_check"
|
|
14
|
+
|
|
15
|
+
module Principles
|
|
16
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
17
|
+
module Models
|
|
18
|
+
# The committed record of a principle's last audit: what it began from, each member's hash when it was
|
|
19
|
+
# made and each finding's outcome with its problem (D34). Outside git it also keeps the hashes the audit
|
|
20
|
+
# began from, which git keeps otherwise, and an open record keeps the last completed one, whose deferred
|
|
21
|
+
# and accepted findings the next audit must still decide (D35).
|
|
22
|
+
class AuditRecord
|
|
23
|
+
# The record an audit that began at start writes, with each member's hash, the result reported for its
|
|
24
|
+
# check, if any (D30), and each finding's outcome. The hashes the audit began from are kept only outside
|
|
25
|
+
# git, where start has no base commit.
|
|
26
|
+
#: (AuditStart, status: String, audited_at: String, members: Hash[String, String], findings: Array[untyped])
|
|
27
|
+
#| -> AuditRecord
|
|
28
|
+
def self.written(start, status:, audited_at:, members:, findings:)
|
|
29
|
+
fields = { "principle_id" => start.id, "status" => status, "map_version" => start.map_version,
|
|
30
|
+
"base_commit" => start.base, "audited_at" => audited_at,
|
|
31
|
+
"members" => start.recorded(members, assessed_at: audited_at), "findings" => findings }
|
|
32
|
+
new(start.base ? fields : fields.merge("base_members" => start.before))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: (Hash[String, untyped]) -> void
|
|
36
|
+
def initialize(fields)
|
|
37
|
+
@fields = fields.freeze #: Hash[String, untyped]
|
|
38
|
+
@members = RecordedMembers.new(fields.fetch("members")) #: RecordedMembers
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The record with only the fields this tool knows, for printing.
|
|
43
|
+
#: () -> Hash[String, untyped]
|
|
44
|
+
def shown
|
|
45
|
+
Data::SchemaCheck.new("audit-record").known(@fields)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The record as its file holds it.
|
|
49
|
+
#: () -> Hash[String, untyped]
|
|
50
|
+
def to_h
|
|
51
|
+
@fields
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A digest of the whole record. A correction carries the last completed record's from when its request was
|
|
55
|
+
# printed, so it is refused once that record changed (#646). A formatter that only reorders its keys changes
|
|
56
|
+
# neither what the record says nor its revision, as same_as? already compares it (#656).
|
|
57
|
+
#: () -> String
|
|
58
|
+
def revision = JsonDigest.new(@fields).to_s
|
|
59
|
+
|
|
60
|
+
#: () -> String
|
|
61
|
+
def principle_id
|
|
62
|
+
@fields.fetch("principle_id")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The hashes the audit began from, kept outside git only.
|
|
66
|
+
#: () -> Hash[String, String]?
|
|
67
|
+
def base_members
|
|
68
|
+
@fields["base_members"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether other says what this record says, apart from when it and its assessments were made.
|
|
72
|
+
#: (AuditRecord?) -> bool
|
|
73
|
+
def same_as?(other)
|
|
74
|
+
!other.nil? && other.undated == undated
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# What this record says, without when it and each of its assessments were made.
|
|
78
|
+
#: () -> Hash[String, untyped]
|
|
79
|
+
def undated = @fields.except("audited_at").merge("members" => members.undated)
|
|
80
|
+
|
|
81
|
+
# The commit the audit began from, or nil outside git.
|
|
82
|
+
#: () -> String?
|
|
83
|
+
def base_commit
|
|
84
|
+
@fields["base_commit"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The record's status, when and from which commit it was made, and whether the map changed since.
|
|
88
|
+
#: (map_version: String) -> Hash[String, untyped]
|
|
89
|
+
def last_audit(map_version:)
|
|
90
|
+
{ "status" => @fields["status"], "audited_at" => @fields["audited_at"], "base_commit" => @fields["base_commit"],
|
|
91
|
+
"map_changed" => @fields["map_version"] != map_version }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Why a one-member audit cannot judge member's change against this record, or nil when it can (D33): the
|
|
95
|
+
# record must be completed and have assessed the member, the member's text must differ from the one it
|
|
96
|
+
# assessed, and the member must have no check. The one owner of these preconditions, which list-member-problems
|
|
97
|
+
# asks before offering the audit that Acknowledgement refuses by them (#626, #632); git is the callers' to know.
|
|
98
|
+
#: (Member, now_sha256: String?) -> Symbol?
|
|
99
|
+
def acknowledgement_refusal(member, now_sha256:)
|
|
100
|
+
recorded = members.hash_for(member.key)
|
|
101
|
+
return :not_completed unless completed?
|
|
102
|
+
return :not_assessed unless recorded
|
|
103
|
+
return :unchanged if recorded == now_sha256
|
|
104
|
+
|
|
105
|
+
:checked if member.check
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whether the audit this record describes completed, rather than being left open outside git.
|
|
109
|
+
#: () -> bool
|
|
110
|
+
def completed?
|
|
111
|
+
@fields["status"] == "completed"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# This open record, keeping the last completed record it replaces outside git, so the next audit still
|
|
115
|
+
# decides that record's findings (D35); itself when there is none.
|
|
116
|
+
#: (AuditRecord?) -> AuditRecord
|
|
117
|
+
def keeping(last_completed)
|
|
118
|
+
last_completed ? AuditRecord.new(@fields.merge("last_completed" => last_completed.to_h)) : self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# This record, saying what its assessments were judged against, as AuditStart#judged_against digests it (D49).
|
|
122
|
+
#: (String) -> AuditRecord
|
|
123
|
+
def judged(against) = AuditRecord.new(@fields.merge("judged_against" => against))
|
|
124
|
+
|
|
125
|
+
# This record, keeping the release of principles its audit ran under, when the principle keeps one (D68).
|
|
126
|
+
#: (PrinciplesPackage?) -> AuditRecord
|
|
127
|
+
def packaged(package) = package ? AuditRecord.new(@fields.merge("principles_package" => package.to_h)) : self
|
|
128
|
+
|
|
129
|
+
# The release of principles its audit ran under, or nil when it kept none.
|
|
130
|
+
#: () -> PrinciplesPackage?
|
|
131
|
+
def package = PrinciplesPackage.kept(@fields["principles_package"])
|
|
132
|
+
|
|
133
|
+
# Whether this record kept its members' assessments, which a record written before D49 did not.
|
|
134
|
+
#: () -> bool
|
|
135
|
+
def kept_assessments? = !@fields["judged_against"].nil?
|
|
136
|
+
|
|
137
|
+
# Whether this record's assessments answered what against digests, as AuditStart#judged_against does (D49).
|
|
138
|
+
#: (String) -> bool
|
|
139
|
+
def judged_against?(against) = @fields["judged_against"] == against
|
|
140
|
+
|
|
141
|
+
# This record with each assessment its answer carried forward taken from last, the record it replaces (D49).
|
|
142
|
+
#: (AuditRecord?) -> AuditRecord
|
|
143
|
+
def carrying(last)
|
|
144
|
+
AuditRecord.new(@fields.merge("members" => members.carrying(last&.last_completed&.members).to_h))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The last completed audit's record: this one, or the one an open record keeps, or nil (D35).
|
|
148
|
+
#: () -> AuditRecord?
|
|
149
|
+
def last_completed
|
|
150
|
+
return self if completed?
|
|
151
|
+
|
|
152
|
+
kept = @fields["last_completed"]
|
|
153
|
+
kept ? AuditRecord.new(kept) : nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# This record with the member known by key moved to change's to_sha256, and change kept beside it (D33).
|
|
157
|
+
# Nothing else changes: the map version, base commit and findings stay the audit's own.
|
|
158
|
+
#: (String, Hash[String, untyped]) -> AuditRecord
|
|
159
|
+
def acknowledged(key, change)
|
|
160
|
+
AuditRecord.new(@fields.merge("members" => members.acknowledged(key, change).to_h))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# This record with each finding a repoint names moved by it at at (D59); the last repoint naming a finding
|
|
164
|
+
# wins.
|
|
165
|
+
#: (Array[WriteUpRepoint], at: String) -> AuditRecord
|
|
166
|
+
def repointed(repoints, at:)
|
|
167
|
+
AuditRecord.new(@fields.merge("findings" => findings.repointed(repoints, at: at).to_a))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The ids of the findings it leaves open whose recorded_at points elsewhere than earlier's did, as a repoint
|
|
171
|
+
# moved them (D59, T16).
|
|
172
|
+
#: (AuditRecord) -> Array[String]
|
|
173
|
+
def write_ups_moved_since(earlier)
|
|
174
|
+
before = earlier.left_open.to_h { |finding| [finding.id, finding.recorded_at] }
|
|
175
|
+
left_open.select { |finding| before.key?(finding.id) && before[finding.id] != finding.recorded_at }.map(&:id)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Each finding the last completed audit deferred or accepted, which the next full audit must decide (D35):
|
|
179
|
+
# this record's own, or, while it is open outside git, those of the completed record it keeps, since its own
|
|
180
|
+
# wait on its correction (#655). The one owner of which record's findings are open.
|
|
181
|
+
#: () -> Array[OpenFinding]
|
|
182
|
+
def left_open
|
|
183
|
+
return findings.left_open if completed?
|
|
184
|
+
|
|
185
|
+
last_completed&.left_open || []
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The ids of the findings it leaves open, as list-open-findings lists them: deferred or accepted, its own and any
|
|
189
|
+
# prior finding kept (#714).
|
|
190
|
+
#: () -> Array[String]
|
|
191
|
+
def open_ids = left_open.map(&:id)
|
|
192
|
+
|
|
193
|
+
# The ids of the findings its audit resolved: those it fixed, and each prior finding decided resolved, never
|
|
194
|
+
# one withdrawn (#714).
|
|
195
|
+
#: (DecidedPriorFindings) -> Array[String]
|
|
196
|
+
def resolved_ids(decided)
|
|
197
|
+
findings.fixed_ids + decided.resolved_ids
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Each finding left open, as list-open-findings prints it (D35, brief finding 13).
|
|
201
|
+
#: () -> Array[Hash[String, untyped]]
|
|
202
|
+
def open_findings = left_open.map(&:listed)
|
|
203
|
+
|
|
204
|
+
# Each finding left open, as the next full audit's request lists it (D35).
|
|
205
|
+
#: () -> Array[Hash[String, untyped]]
|
|
206
|
+
def prior_findings = left_open.map(&:for_next_audit)
|
|
207
|
+
|
|
208
|
+
# The path of each file this record points at, through its assessments' evidence and its findings' evidence
|
|
209
|
+
# and write-ups, which a clone must hold for the record to be checked there (#724).
|
|
210
|
+
#: () -> Array[String]
|
|
211
|
+
def cited_paths = (members.cited_paths + findings.cited_paths).uniq
|
|
212
|
+
|
|
213
|
+
# The files this record cites, as history holds them now: which go with it when its completed audit is
|
|
214
|
+
# committed, compared with HEAD only in git, and which no longer exist (#724, #861).
|
|
215
|
+
#: (ProjectHistory) -> CitedFiles
|
|
216
|
+
def cited_files(history) = CitedFiles.new(cited_paths, history, in_git: !base_commit.nil?)
|
|
217
|
+
|
|
218
|
+
# The members this record assessed, each with its hash, check, loading decision and acknowledgements.
|
|
219
|
+
#: RecordedMembers
|
|
220
|
+
attr_reader :members
|
|
221
|
+
|
|
222
|
+
# The command this record ran for the member known by key's check, as its RecordedMembers answers it, or nil
|
|
223
|
+
# when it gave that check no exit status.
|
|
224
|
+
#: (String) -> String?
|
|
225
|
+
def check_run_for(key) = members.check_run_for(key)
|
|
226
|
+
|
|
227
|
+
# The line saying what the review of the check this record ran for the member known by key rested on, with the
|
|
228
|
+
# date it was first kept: its own review_basis_since, or else this record's audited_at, which dates a line kept
|
|
229
|
+
# here first (D63, D52). Nothing when the record ran no check for it or kept no line.
|
|
230
|
+
#: (String) -> Hash[String, untyped]
|
|
231
|
+
def check_review_for(key)
|
|
232
|
+
kept = members.check_review_as_kept(key)
|
|
233
|
+
return {} unless kept.key?("review_basis")
|
|
234
|
+
|
|
235
|
+
{ "review_basis_since" => @fields["audited_at"] }.merge(kept)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
# The findings its own audit recorded, whatever it did about each, with the principle, audit time and members
|
|
241
|
+
# it recorded them under.
|
|
242
|
+
#: () -> RecordedFindings
|
|
243
|
+
def findings
|
|
244
|
+
RecordedFindings.new(@fields.fetch("findings"), principle_id: principle_id, audited_at: @fields["audited_at"],
|
|
245
|
+
members: members)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../data/audit_files"
|
|
6
|
+
require_relative "audit_record"
|
|
7
|
+
require_relative "project_history"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# Every principle's audit record, loaded and saved through Data::AuditFiles, and read as a commit held it
|
|
13
|
+
# through ProjectHistory.
|
|
14
|
+
class AuditRecords
|
|
15
|
+
#: (Data::AuditFiles, ProjectHistory) -> void
|
|
16
|
+
def initialize(files, history)
|
|
17
|
+
@files = files
|
|
18
|
+
@history = history
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The file id's record was in at commit, and that record, or nil when commit holds it in neither the folder
|
|
22
|
+
# nor its earlier name, as outside git (D53). It is read only at a commit an agent names; nothing walks history.
|
|
23
|
+
#: (String, String) -> [String, AuditRecord]?
|
|
24
|
+
def at(id, commit)
|
|
25
|
+
[file(id), earlier_file(id)].each do |path|
|
|
26
|
+
text = @history.text_at(commit, path) or next
|
|
27
|
+
return [path, AuditRecord.new(@files.parse(id, text))]
|
|
28
|
+
end
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# id's record, or nil when it has none; Data::AuditFiles refuses one that is unparseable.
|
|
33
|
+
#: (String) -> AuditRecord?
|
|
34
|
+
def find(id)
|
|
35
|
+
fields = @files.read(id)
|
|
36
|
+
fields && AuditRecord.new(fields)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The findings id's record leaves open: none when it has no record, and none when its record cannot be read,
|
|
40
|
+
# which list-member-problems reports, so a command reading other principles' findings beside its own is not
|
|
41
|
+
# refused for theirs (#772).
|
|
42
|
+
#: (String) -> Array[OpenFinding]
|
|
43
|
+
def left_open_or_none(id)
|
|
44
|
+
find(id)&.left_open || []
|
|
45
|
+
rescue Error
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The record of each of ids that has one.
|
|
50
|
+
#: (Array[String]) -> Array[AuditRecord]
|
|
51
|
+
def of(ids) = ids.filter_map { |id| find(id) }
|
|
52
|
+
|
|
53
|
+
# id's record, refused when it has none.
|
|
54
|
+
#: (String) -> AuditRecord
|
|
55
|
+
def fetch(id)
|
|
56
|
+
find(id) || raise(Error.new("unknown_audit", "#{id} has no audit record",
|
|
57
|
+
"To audit it: #{Error::EXECUTABLE} start-audit #{id}", file(id)))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: (String) -> bool
|
|
61
|
+
def exist?(id)
|
|
62
|
+
@files.exist?(id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The record as a change to write, unless it says what the record on disk says, apart from when.
|
|
66
|
+
#: (AuditRecord) -> Array[AuditRecord]
|
|
67
|
+
def changes(record)
|
|
68
|
+
record.same_as?(find(record.principle_id)) ? [] : [record]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# What record's completed audit asks to commit: its own file, then the files the record says go with it, of
|
|
72
|
+
# changed and those it cites, with what reported cites that the record does not keep (#855, #860).
|
|
73
|
+
#: (AuditRecord, Array[String], ?AssessmentReport) -> Array[String]
|
|
74
|
+
def files_to_commit(record, changed, reported = AssessmentReport::NONE)
|
|
75
|
+
[file(record.principle_id), *record.cited_files(@history).committed_with(changed, reported)]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Each file record cites that no longer exists, which files_to_commit leaves out (#861).
|
|
79
|
+
#: (AuditRecord) -> Array[String]
|
|
80
|
+
def cited_files_missing(record) = record.cited_files(@history).missing
|
|
81
|
+
|
|
82
|
+
# Writes record's file and names it, relative to the project.
|
|
83
|
+
#: (AuditRecord) -> String
|
|
84
|
+
def save(record)
|
|
85
|
+
id = record.principle_id
|
|
86
|
+
@files.write(id, record.to_h)
|
|
87
|
+
file(id)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The file id's record is kept in, relative to the project.
|
|
91
|
+
#: (String) -> String
|
|
92
|
+
def file(id)
|
|
93
|
+
@files.relative(id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The file id's record was kept in under the folder's earlier name, relative to the project (#536).
|
|
97
|
+
#: (String) -> String
|
|
98
|
+
def earlier_file(id)
|
|
99
|
+
@files.earlier_relative(id)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "assessment_report"
|
|
5
|
+
require_relative "json_digest"
|
|
6
|
+
require_relative "member_map"
|
|
7
|
+
|
|
8
|
+
module Principles
|
|
9
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
10
|
+
module Models
|
|
11
|
+
# What an audit began from: the principle and its map, the commit it was based on (nil outside git) and
|
|
12
|
+
# each member's hash then, and what the assessment reported beyond its findings, as its AssessmentReport holds it.
|
|
13
|
+
class AuditStart
|
|
14
|
+
#: String?
|
|
15
|
+
attr_reader :base
|
|
16
|
+
#: Hash[String, String]
|
|
17
|
+
attr_reader :before
|
|
18
|
+
|
|
19
|
+
# What the assessment reported beyond its findings, such as each member's record entry fields.
|
|
20
|
+
#: AssessmentReport
|
|
21
|
+
attr_reader :reported
|
|
22
|
+
|
|
23
|
+
# The map of the principle the audit is of.
|
|
24
|
+
#: MemberMap
|
|
25
|
+
attr_reader :map
|
|
26
|
+
|
|
27
|
+
#: (map: MemberMap, base: String?, before: Hash[String, String], ?reported: AssessmentReport) -> void
|
|
28
|
+
def initialize(map:, base:, before:, reported: AssessmentReport::NONE)
|
|
29
|
+
@map = map
|
|
30
|
+
@base = base
|
|
31
|
+
@before = before
|
|
32
|
+
@reported = reported
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The principle the audit is of, as its map names it.
|
|
37
|
+
#: () -> String
|
|
38
|
+
def id = @map.id
|
|
39
|
+
|
|
40
|
+
# Each member's entry in the record, by id, from its hash in members: the hash, its key when it has moved,
|
|
41
|
+
# and what the assessment reported for it, if anything, its assessment kept as made at assessed_at. A member
|
|
42
|
+
# added during the audit has no entry, since no assessment read it: the record gives it no baseline, so the
|
|
43
|
+
# next audit judges it (T69).
|
|
44
|
+
#: (Hash[String, String], assessed_at: String) -> Hash[String, Hash[String, untyped]]
|
|
45
|
+
def recorded(members, assessed_at:)
|
|
46
|
+
keys = @map.members.moved_keys
|
|
47
|
+
members.select { |id, _| began_with?(id) }.to_h do |id, sha|
|
|
48
|
+
entry = { "sha256" => sha }.merge(keys.key?(id) ? { "key" => keys[id] } : {})
|
|
49
|
+
[id, with_assessment(entry.merge(@reported.entry_fields_of(id)), assessed_at, changed: @before[id] != sha)]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Whether the audit began with member id, rather than a correction adding it (T69).
|
|
56
|
+
#: (String) -> bool
|
|
57
|
+
def began_with?(id) = @before.key?(id)
|
|
58
|
+
|
|
59
|
+
# The entry with its assessment dated assessed_at, and an observed sighting the answer gave no observed_at for
|
|
60
|
+
# dated then too (#680), or a carried one left for completion to fill; when the member changed during the
|
|
61
|
+
# audit, as a fix in its correction changes it, no assessment read the text it is recorded at, so it keeps none
|
|
62
|
+
# and says why (D49).
|
|
63
|
+
#: (Hash[String, untyped], String, changed: bool) -> Hash[String, untyped]
|
|
64
|
+
def with_assessment(entry, assessed_at, changed:)
|
|
65
|
+
kept = entry["assessment"] or return entry
|
|
66
|
+
return entry.except("assessment").merge("changed_during_last_audit" => true) if changed
|
|
67
|
+
return entry if kept["carried_forward"]
|
|
68
|
+
|
|
69
|
+
seen = kept["delivery"] == "observed" ? assessed_at : nil
|
|
70
|
+
entry.merge("assessment" => { "assessed_at" => assessed_at, "observed_at" => seen }.compact.merge(kept))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
public
|
|
74
|
+
|
|
75
|
+
# A digest of what this audit's assessments answered: the principle's description and question, the question
|
|
76
|
+
# this tool asked. When either changes, the next full audit carries no assessment forward (D49).
|
|
77
|
+
#: (String) -> String
|
|
78
|
+
def judged_against(question) = JsonDigest.new("description" => @map.description, "question" => question).to_s
|
|
79
|
+
|
|
80
|
+
# The map's version: the digest of its JSON, so any change to it gives a new version.
|
|
81
|
+
#: () -> String
|
|
82
|
+
def map_version = @map.version
|
|
83
|
+
|
|
84
|
+
# The members in now that the audit did not begin with, as a correction adds a member a finding said was
|
|
85
|
+
# missing (T69).
|
|
86
|
+
#: (now: Hash[String, String]) -> Array[String]
|
|
87
|
+
def added(now:) = now.keys.reject { |id| began_with?(id) }
|
|
88
|
+
|
|
89
|
+
# The members whose hash in now differs from their hash when the audit began.
|
|
90
|
+
#: (now: Hash[String, String]) -> Array[String]
|
|
91
|
+
def changed(now:)
|
|
92
|
+
now.reject { |member, sha| @before[member] == sha }.keys
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "evidence_item"
|
|
6
|
+
require_relative "located_text"
|
|
7
|
+
require_relative "line_span"
|
|
8
|
+
|
|
9
|
+
module Principles
|
|
10
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
11
|
+
module Models
|
|
12
|
+
# The lines a full audit's members span now, each with its file, against which each evidence item of another
|
|
13
|
+
# principle's open finding is said to share lines or not (#792, D64). These are lines, not a verdict on whether
|
|
14
|
+
# the finding is about the same problem, which the agent judges.
|
|
15
|
+
class AuditedMemberLines
|
|
16
|
+
# located gives, under each audited member's id, the text found for it now: its path and its lines.
|
|
17
|
+
#: (Hash[String, LocatedText]) -> void
|
|
18
|
+
def initialize(located)
|
|
19
|
+
@spans = located.transform_values { |found| [found.path, found.span] } #: Hash[String, [String, LineSpan]]
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The item as the record keeps it, with what its lines are now beside these members': the members whose lines it
|
|
24
|
+
# shares, or that it shares none, with its lines; that it names only a whole file; or that locate, which finds
|
|
25
|
+
# it in its file now, can no longer find it, so one moved write-up never refuses the request.
|
|
26
|
+
#: (EvidenceItem) { (Locator) -> LocatedText } -> Hash[String, untyped]
|
|
27
|
+
def facts_of(item, &locate)
|
|
28
|
+
return item.with_facts("whole_file" => true) if item.whole_file?
|
|
29
|
+
|
|
30
|
+
span = locate.call(item.locator).span
|
|
31
|
+
item.with_facts("first_line" => span.first_line, "last_line" => span.last_line, **sharing(item.path, span))
|
|
32
|
+
rescue Error
|
|
33
|
+
item.with_facts("not_located" => true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
#: (String, LineSpan) -> Hash[String, untyped]
|
|
39
|
+
def sharing(path, span)
|
|
40
|
+
ids = @spans.select { |_, (on, member)| on == path && member.shares_a_line_with?(span) }.keys
|
|
41
|
+
ids.empty? ? { "shares_no_lines" => true } : { "shares_lines_with" => ids }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
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 instruction files list-candidate-files offers, shortened without judging what is guidance. A
|
|
8
|
+
# skill's templates, references and examples once filled most of a 10KB inventory (alpha 011
|
|
9
|
+
# friction 2). So Markdown files and harness settings are listed one by one, except inside a skill,
|
|
10
|
+
# where only SKILL.md, the file the harness loads, is listed. Every other file is counted under its
|
|
11
|
+
# directory. The agent still sees where every file is, and decides what to read.
|
|
12
|
+
class CandidateFiles
|
|
13
|
+
# A file a harness reads as settings, listed by name like guidance.
|
|
14
|
+
SETTINGS = %r{(?:\A|/)\.claude/settings[^/]*\.json\z}
|
|
15
|
+
MARKDOWN = /\.md\z/i
|
|
16
|
+
# A file inside a skill's folder, and whether it is the skill's entry point.
|
|
17
|
+
IN_SKILL = %r{(?:\A|/)\.claude/skills/[^/]+/}
|
|
18
|
+
SKILL_ENTRY = %r{(?:\A|/)\.claude/skills/[^/]+/SKILL\.md\z}
|
|
19
|
+
|
|
20
|
+
#: (Array[Hash[String, untyped]]) -> void
|
|
21
|
+
def initialize(files)
|
|
22
|
+
@files = files #: Array[Hash[String, untyped]]
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The inventory's files, the other files counted by directory, and those test-data hints that name a
|
|
27
|
+
# listed file.
|
|
28
|
+
#: (Array[Hash[String, untyped]]) -> Hash[String, untyped]
|
|
29
|
+
def listing(test_data_hints)
|
|
30
|
+
paths = listed.map { |file| file.fetch("path") }
|
|
31
|
+
{ "files" => listed, "other_files_by_directory" => other_files_by_directory,
|
|
32
|
+
"possibly_test_data" => test_data_hints.select { |hint| paths.include?(hint.fetch("path")) } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Each Markdown or settings file, with its size.
|
|
36
|
+
#: () -> Array[Hash[String, untyped]]
|
|
37
|
+
def listed
|
|
38
|
+
@files.select { |file| named?(file.fetch("path")) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Each directory holding other files, and how many.
|
|
42
|
+
#: () -> Array[Hash[String, untyped]]
|
|
43
|
+
def other_files_by_directory
|
|
44
|
+
others = @files.map { |file| file.fetch("path") }.reject { |path| named?(path) }
|
|
45
|
+
others.group_by { |path| directory(path) }.sort.map do |directory, paths|
|
|
46
|
+
{ "directory" => directory, "files" => paths.length }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
#: (String) -> String
|
|
53
|
+
def directory(path)
|
|
54
|
+
parent = path.rpartition("/").first
|
|
55
|
+
parent.empty? ? "." : parent
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: (String) -> bool
|
|
59
|
+
def named?(path)
|
|
60
|
+
return path.match?(SKILL_ENTRY) if path.match?(IN_SKILL)
|
|
61
|
+
|
|
62
|
+
path.match?(MARKDOWN) || path.match?(SETTINGS)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "candidate_listing"
|
|
5
|
+
require_relative "project_history"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, which loads and saves itself through data/. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# What list-candidate-files found: the inventory of candidate files as CandidateListing lists them, and in git
|
|
11
|
+
# which of them, or of the files their links lead to, git does not track. Each list that names a file is
|
|
12
|
+
# something the agent asks the project's owner about before mapping, and only those are asked, since asks for
|
|
13
|
+
# empty lists hid the one that applied (#405).
|
|
14
|
+
class CandidateInventory
|
|
15
|
+
# The lists the agent asks the owner about, in the order the asks are put.
|
|
16
|
+
OWNER_ASK_KEYS = %w[possibly_test_data untracked ignored ignored_folders].freeze
|
|
17
|
+
|
|
18
|
+
# The most paths an ask names inline: a short list can be pasted to the owner as it stands, while a
|
|
19
|
+
# longer one would bury the ask, so the agent reads it from its list instead (#412).
|
|
20
|
+
NAMED_INLINE_AT_MOST = 3
|
|
21
|
+
|
|
22
|
+
# What the project's inventory lists, and which listed files, or files their links lead to, history says
|
|
23
|
+
# git does not track.
|
|
24
|
+
#: (Hash[String, untyped], ProjectHistory) -> CandidateInventory
|
|
25
|
+
def self.found(inventory, history)
|
|
26
|
+
listed = CandidateListing.new(inventory).to_h
|
|
27
|
+
new(listed, untracked: history.untracked(listed.fetch("files").map { it.fetch("path") }))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The listing, as list-candidate-files prints it.
|
|
31
|
+
#: Hash[String, untyped]
|
|
32
|
+
attr_reader :listing
|
|
33
|
+
|
|
34
|
+
# The listed files, and the files their links lead to, git does not track; nil outside git, where nothing
|
|
35
|
+
# checked them.
|
|
36
|
+
#: Array[String]?
|
|
37
|
+
attr_reader :untracked
|
|
38
|
+
|
|
39
|
+
#: (Hash[String, untyped], untracked: Array[String]?) -> void
|
|
40
|
+
def initialize(listing, untracked:)
|
|
41
|
+
@listing = listing
|
|
42
|
+
@untracked = untracked
|
|
43
|
+
@lists = listing.merge("untracked" => untracked) #: Hash[String, untyped]
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: () -> Hash[String, untyped]
|
|
48
|
+
def to_h = @listing
|
|
49
|
+
|
|
50
|
+
# Each list that names a file, in OWNER_ASK_KEYS' order, with the paths its ask names inline: all of
|
|
51
|
+
# them when there are at most NAMED_INLINE_AT_MOST, and none beyond.
|
|
52
|
+
#: () -> Hash[String, Array[String]]
|
|
53
|
+
def owner_asks
|
|
54
|
+
OWNER_ASK_KEYS.select { |key| @lists[key]&.any? }.to_h do |key|
|
|
55
|
+
paths = @lists.fetch(key).map { |entry| named(entry) }
|
|
56
|
+
[key, paths.length > NAMED_INLINE_AT_MOST ? [] : paths]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The path one list entry names: a file's path, a folder, or the path itself.
|
|
63
|
+
#: (untyped) -> String
|
|
64
|
+
def named(entry) = entry.is_a?(Hash) ? entry.fetch("path") { entry.fetch("folder") } : entry
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "candidate_files"
|
|
5
|
+
require_relative "ignored_candidates"
|
|
6
|
+
|
|
7
|
+
module Principles
|
|
8
|
+
# The domain logic, and the mediation of persistence. models/CLAUDE.md owns the boundary.
|
|
9
|
+
module Models
|
|
10
|
+
# The inventory of candidate files as list-candidate-files lists it: the instruction files agent hosts load
|
|
11
|
+
# by convention, the other files counted by directory, the listed files that look like test data, and in git
|
|
12
|
+
# the ignored ones, unread, each folder git ignores counted once.
|
|
13
|
+
class CandidateListing
|
|
14
|
+
#: (Hash[String, untyped]) -> void
|
|
15
|
+
def initialize(inventory)
|
|
16
|
+
@inventory = inventory
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: () -> Hash[String, untyped]
|
|
21
|
+
def to_h
|
|
22
|
+
files = CandidateFiles.new(@inventory.fetch("files"))
|
|
23
|
+
listed = @inventory.except("ignored", "ignored_ancestor_folders")
|
|
24
|
+
.merge(files.listing(@inventory.fetch("possibly_test_data")))
|
|
25
|
+
return listed unless @inventory.key?("ignored")
|
|
26
|
+
|
|
27
|
+
listed.merge(IgnoredCandidates.new(paths: @inventory.fetch("ignored"),
|
|
28
|
+
folders: @inventory.fetch("ignored_ancestor_folders")).listing)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|