samagotchi 0.156.0 → 0.157.0
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 +4 -4
- data/CHANGELOG.md +44 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +16 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +9 -9
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +29 -3
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +321 -439
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/tools/delegate.rb +48 -43
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +31 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +20 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_state"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module MemoryBundle
|
|
7
|
+
# What each command that acts on a bundle memory does in each FileState:
|
|
8
|
+
# one table per command, so a reader sees one command on one screen. A cell
|
|
9
|
+
# is keyed [kind, mark] or [kind, :any]; a dropped file another bundle
|
|
10
|
+
# lists too is [kind, :shared] (FileState#shared_with, asked only there).
|
|
11
|
+
# A dry run reads its command's row and doesn't execute it.
|
|
12
|
+
#
|
|
13
|
+
# A new kind or a new command adds a row in each table it reaches
|
|
14
|
+
# (docs/internals/bundles.md).
|
|
15
|
+
module FileActions
|
|
16
|
+
# do: what happens to the file's bytes
|
|
17
|
+
# :write_incoming the bundle's text replaces it (or is put there)
|
|
18
|
+
# :merge the text merge's result replaces it (Merger::Outcome)
|
|
19
|
+
# :decide not a function of kind and mark: the caller asks
|
|
20
|
+
# Merger::Upgrade#decision, and FileActions.decided
|
|
21
|
+
# turns its Outcome into the action
|
|
22
|
+
# :keep left as it is (already the bundle's, or kept)
|
|
23
|
+
# :skip left as it is, the bundle doesn't take it
|
|
24
|
+
# :move_to_trash moved to the trash (a removal)
|
|
25
|
+
# :refuse the command stops
|
|
26
|
+
# :restore the bundle's text put back (SystemBundle at its version)
|
|
27
|
+
# save_first: a copy of the old bytes goes to the trash before a write
|
|
28
|
+
# (when they differ from what is written)
|
|
29
|
+
# next_base: :on_disk (the file as the command leaves it), :incoming,
|
|
30
|
+
# :recorded (the base it has; none stays none), :none (no entry left)
|
|
31
|
+
# -- Merger::Outcome#base's vocabulary
|
|
32
|
+
# word: the report bucket the file is on (Installer#summary)
|
|
33
|
+
# record: the entry afterwards: :own (rebuilt, no mark), :keep_entry
|
|
34
|
+
# (kept with its base, no mark), :mark (marked behind), :hold_mark (as
|
|
35
|
+
# it was, mark and all), :clear_mark, :drop, :not_owned (never recorded)
|
|
36
|
+
Action = Data.define(:do, :save_first, :next_base, :word, :record) do
|
|
37
|
+
def initialize(do:, word:, next_base: :recorded, record: :own, save_first: false) = super
|
|
38
|
+
|
|
39
|
+
# The file stays behind the bundle, marked (a conflict to the report).
|
|
40
|
+
def behind? = record == :mark
|
|
41
|
+
|
|
42
|
+
# The bundle's text goes into the file: written (installed, forced or
|
|
43
|
+
# fast-forwarded) or merged into a local edit. The source file is the
|
|
44
|
+
# download, so the manifest's sha checks it (Installer's integrity
|
|
45
|
+
# check). :restore is SystemBundle's; it checks its own copy.
|
|
46
|
+
def takes_source? = %i[write_incoming merge].include?(self.do)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
WRITE = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk)
|
|
50
|
+
UP_TO_DATE = Action.new(do: :keep, word: :up_to_date, next_base: :on_disk)
|
|
51
|
+
USER_SAME = Action.new(do: :keep, word: :up_to_date, next_base: :none, record: :not_owned)
|
|
52
|
+
USERS_OWN = Action.new(do: :skip, word: :kept_own, next_base: :none, record: :not_owned)
|
|
53
|
+
KEPT_OWN = Action.new(do: :skip, word: :kept_own, record: :keep_entry)
|
|
54
|
+
HELD = Action.new(do: :skip, word: :kept_own, record: :hold_mark)
|
|
55
|
+
FAST_FORWARD = Action.new(do: :write_incoming, word: :updated, next_base: :on_disk)
|
|
56
|
+
KEEP = Action.new(do: :keep, word: :kept)
|
|
57
|
+
# No base: no merge is possible, so the file stays behind, marked.
|
|
58
|
+
BEHIND = Action.new(do: :keep, word: :behind, record: :mark)
|
|
59
|
+
DECIDE = Action.new(do: :decide, word: nil)
|
|
60
|
+
REFUSE = Action.new(do: :refuse, word: nil, record: :hold_mark)
|
|
61
|
+
FORCED = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk, save_first: true)
|
|
62
|
+
# The bundle dropped it: a plain install keeps it and its entry (the
|
|
63
|
+
# upgrade it points at moves it), an upgrade moves it unless edited.
|
|
64
|
+
DROPPED_KEPT = Action.new(do: :keep, word: nil, record: :keep_entry)
|
|
65
|
+
DROPPED_GONE = Action.new(do: :skip, word: nil, next_base: :none, record: :drop)
|
|
66
|
+
REMOVE = Action.new(do: :move_to_trash, word: :removed, next_base: :none, record: :drop)
|
|
67
|
+
KEPT_PRUNED = Action.new(do: :keep, word: :kept_pruned, record: :keep_entry)
|
|
68
|
+
KEPT_SHARED = Action.new(do: :keep, word: :kept_shared, next_base: :none, record: :drop)
|
|
69
|
+
|
|
70
|
+
# Rows every installing command shares: files the bundle doesn't own.
|
|
71
|
+
NOT_OWNED = {
|
|
72
|
+
%i[new any] => WRITE, %i[user_same any] => USER_SAME, %i[user_file any] => USERS_OWN,
|
|
73
|
+
%i[missing any] => WRITE, %i[level any] => UP_TO_DATE, %i[unreadable any] => REFUSE
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# A plain install (a re-install over the record, or a fresh one).
|
|
77
|
+
INSTALL = NOT_OWNED.merge(
|
|
78
|
+
# A file never edited takes the source's text, as an upgrade's
|
|
79
|
+
# fast-forward does (D4); an edited one stays (an upgrade merges it).
|
|
80
|
+
%i[pristine none] => FAST_FORWARD, %i[pristine_no_base none] => FAST_FORWARD,
|
|
81
|
+
%i[edited none] => KEPT_OWN, %i[diverged none] => KEPT_OWN, %i[edited_no_base none] => KEPT_OWN,
|
|
82
|
+
# A marked file it skips has missed the bundle's change as much as before.
|
|
83
|
+
%i[pristine any] => HELD, %i[edited any] => HELD, %i[diverged any] => HELD,
|
|
84
|
+
%i[pristine_no_base any] => HELD, %i[edited_no_base any] => HELD,
|
|
85
|
+
%i[dropped_pristine any] => DROPPED_KEPT, %i[dropped_edited any] => DROPPED_KEPT,
|
|
86
|
+
%i[dropped_missing any] => DROPPED_GONE
|
|
87
|
+
).freeze
|
|
88
|
+
|
|
89
|
+
# install --force and upgrade --force: the bundle's text everywhere, an
|
|
90
|
+
# edit copied to the trash first. Its dropped rows are upgrade --force's
|
|
91
|
+
# (a plain install --force never prunes: its dropped files are INSTALL's).
|
|
92
|
+
FORCE = NOT_OWNED.merge(
|
|
93
|
+
# A file that already has the bundle's text is written all the same
|
|
94
|
+
# (nothing to save): it is installed, and checked, as any other.
|
|
95
|
+
%i[new any] => FORCED, %i[user_same any] => FORCED, %i[user_file any] => FORCED, %i[level any] => FORCED,
|
|
96
|
+
%i[pristine any] => FORCED, %i[edited any] => FORCED, %i[diverged any] => FORCED,
|
|
97
|
+
%i[pristine_no_base any] => FORCED, %i[edited_no_base any] => FORCED,
|
|
98
|
+
%i[dropped_pristine any] => REMOVE, %i[dropped_edited any] => REMOVE, %i[dropped_missing any] => DROPPED_GONE,
|
|
99
|
+
%i[dropped_pristine shared] => KEPT_SHARED, %i[dropped_edited shared] => KEPT_SHARED
|
|
100
|
+
).freeze
|
|
101
|
+
|
|
102
|
+
# An upgrade's 3-way view (Merger::Upgrade#outcome).
|
|
103
|
+
UPGRADE = NOT_OWNED.merge(
|
|
104
|
+
%i[pristine none] => FAST_FORWARD,
|
|
105
|
+
# A marked file that equals its base takes the update; a copy is kept.
|
|
106
|
+
%i[pristine current] => FAST_FORWARD.with(save_first: true),
|
|
107
|
+
# An older chi's mark: fast-forward unless it takes a line out and
|
|
108
|
+
# ShippedHistory can't prove the base (the legacy guard).
|
|
109
|
+
%i[pristine legacy] => DECIDE,
|
|
110
|
+
%i[edited any] => KEEP,
|
|
111
|
+
# The text merge: merged, a conflict, or already merged.
|
|
112
|
+
%i[diverged any] => DECIDE,
|
|
113
|
+
# No base: no merge. One never edited (the checksum vouches for it)
|
|
114
|
+
# takes the update as a file equal to its base would (U4); a marked
|
|
115
|
+
# one, or an edit, stays behind.
|
|
116
|
+
%i[pristine_no_base none] => FAST_FORWARD,
|
|
117
|
+
%i[pristine_no_base any] => BEHIND, %i[edited_no_base any] => BEHIND,
|
|
118
|
+
%i[dropped_pristine any] => REMOVE, %i[dropped_edited any] => KEPT_PRUNED, %i[dropped_missing any] => DROPPED_GONE,
|
|
119
|
+
%i[dropped_pristine shared] => KEPT_SHARED, %i[dropped_edited shared] => KEPT_SHARED
|
|
120
|
+
).freeze
|
|
121
|
+
|
|
122
|
+
# Remerge: the retry of a marked file at the same version. Unmarked
|
|
123
|
+
# files are never asked. Before the table, a mark whose source moved
|
|
124
|
+
# stays behind as it is (FileState#source_moved?).
|
|
125
|
+
REMERGE = {
|
|
126
|
+
%i[level any] => Action.new(do: :keep, word: :settled, next_base: :on_disk, record: :clear_mark),
|
|
127
|
+
%i[edited any] => Action.new(do: :keep, word: :settled, record: :clear_mark),
|
|
128
|
+
%i[diverged any] => DECIDE,
|
|
129
|
+
# It matches its old base: the next upgrade takes the update.
|
|
130
|
+
%i[pristine current] => BEHIND,
|
|
131
|
+
%i[pristine legacy] => DECIDE,
|
|
132
|
+
%i[pristine_no_base any] => BEHIND, %i[edited_no_base any] => BEHIND,
|
|
133
|
+
# Not shipped now: behind, nothing to merge.
|
|
134
|
+
%i[dropped_pristine any] => BEHIND, %i[dropped_edited any] => BEHIND,
|
|
135
|
+
%i[dropped_missing any] => Action.new(do: :skip, word: nil, record: :hold_mark),
|
|
136
|
+
# The next upgrade or a restore puts it back.
|
|
137
|
+
%i[missing any] => Action.new(do: :skip, word: nil, record: :hold_mark),
|
|
138
|
+
%i[unreadable any] => Action.new(do: :keep, word: :behind, record: :hold_mark)
|
|
139
|
+
}.freeze
|
|
140
|
+
|
|
141
|
+
# SystemBundle at its version: a memory not on disk is put back, one the
|
|
142
|
+
# record doesn't own too (research Q2, today's behaviour).
|
|
143
|
+
RESTORE = {
|
|
144
|
+
%i[missing any] => Action.new(do: :restore, word: :restored, next_base: :on_disk),
|
|
145
|
+
%i[new any] => Action.new(do: :restore, word: :restored, next_base: :on_disk)
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
148
|
+
# Uninstall, on the source-less kinds: an edit refuses (Uninstaller's
|
|
149
|
+
# --force moves it instead); a file another bundle lists stays.
|
|
150
|
+
UNINSTALL = {
|
|
151
|
+
%i[pristine any] => REMOVE, %i[pristine_no_base any] => REMOVE,
|
|
152
|
+
%i[edited any] => REFUSE, %i[edited_no_base any] => REFUSE, %i[unreadable any] => REFUSE,
|
|
153
|
+
%i[missing any] => DROPPED_GONE,
|
|
154
|
+
%i[pristine shared] => KEPT_SHARED, %i[pristine_no_base shared] => KEPT_SHARED,
|
|
155
|
+
%i[edited shared] => KEPT_SHARED, %i[edited_no_base shared] => KEPT_SHARED
|
|
156
|
+
}.freeze
|
|
157
|
+
|
|
158
|
+
TABLES = { install: INSTALL, force: FORCE, upgrade: UPGRADE, remerge: REMERGE, restore: RESTORE,
|
|
159
|
+
uninstall: UNINSTALL }.freeze
|
|
160
|
+
# The kinds whose row depends on another bundle listing the file.
|
|
161
|
+
SHARED_ASKED = %i[dropped_pristine dropped_edited pristine pristine_no_base edited edited_no_base].freeze
|
|
162
|
+
|
|
163
|
+
# The command's action for +state+: [kind, :shared] when another bundle
|
|
164
|
+
# lists a file that has such a row, else [kind, mark], then [kind, :any].
|
|
165
|
+
# @param command [Symbol] a TABLES key
|
|
166
|
+
# @return [Action, nil] nil: the command never meets the state
|
|
167
|
+
def self.for(command, state)
|
|
168
|
+
table = TABLES.fetch(command)
|
|
169
|
+
if SHARED_ASKED.include?(state.kind) && table.key?([state.kind, :shared]) && state.shared_with.any?
|
|
170
|
+
return table.fetch([state.kind, :shared])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
table[[state.kind, state.mark]] || table[[state.kind, :any]]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# A :decide cell's answer: the action Merger::Upgrade#decision's Outcome
|
|
177
|
+
# stands for. Remerge never fast-forwards (the next upgrade does) and
|
|
178
|
+
# settles a file with nothing left to miss.
|
|
179
|
+
# @param outcome [Merger::Outcome]
|
|
180
|
+
# @param command [Symbol] :upgrade or :remerge
|
|
181
|
+
def self.decided(outcome, command = :upgrade)
|
|
182
|
+
case outcome.status
|
|
183
|
+
when :merged then Action.new(do: :merge, word: :merged, next_base: :incoming, save_first: true)
|
|
184
|
+
when :conflict then BEHIND
|
|
185
|
+
when :fast_forward then command == :remerge ? BEHIND : FAST_FORWARD.with(save_first: true)
|
|
186
|
+
when :keep, :noop
|
|
187
|
+
command == :remerge ? Action.new(do: :keep, word: :settled, next_base: outcome.base, record: :clear_mark) : KEEP.with(next_base: outcome.base)
|
|
188
|
+
else raise ArgumentError, "no action for outcome #{outcome.status.inspect}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -26,6 +26,19 @@ module Samagotchi
|
|
|
26
26
|
hex = hex(path)
|
|
27
27
|
hex && "sha256:#{hex}"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# The integrity check's one warning: +path+'s bytes against the sha
|
|
31
|
+
# hex a manifest declares for it. nil when they match, or when there is
|
|
32
|
+
# nothing to check (no sha declared, no file).
|
|
33
|
+
# @param label [String] what the file is ("a.md", "hook guard.rb")
|
|
34
|
+
def self.mismatch(label, path, expected)
|
|
35
|
+
return nil if expected.nil? || expected.empty?
|
|
36
|
+
|
|
37
|
+
actual = hex(path)
|
|
38
|
+
return nil if actual.nil? || actual == expected
|
|
39
|
+
|
|
40
|
+
"Checksum mismatch for #{label}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
41
|
+
end
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_digest"
|
|
4
|
+
require_relative "installed_bundle"
|
|
5
|
+
require_relative "provenance"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
module MemoryBundle
|
|
9
|
+
# One bundle memory as a command meets it: which of a few states it is in,
|
|
10
|
+
# computed once from the file (D), its base (B), the bundle's text (I) and
|
|
11
|
+
# the record's entry (R). Derived, never stored. FileActions says what each
|
|
12
|
+
# acting command does in each state.
|
|
13
|
+
#
|
|
14
|
+
# kind, with a source (install, upgrade, remerge, restore, plan):
|
|
15
|
+
# :new not the bundle's, not on disk
|
|
16
|
+
# :user_same not the bundle's, on disk with I's bytes
|
|
17
|
+
# :user_file not the bundle's, other bytes (the user's own file)
|
|
18
|
+
# :missing the bundle's, not on disk
|
|
19
|
+
# :level the bundle's, D == I
|
|
20
|
+
# :pristine never edited (#edited?: D == R's checksum, or with
|
|
21
|
+
# none D == B): the bundle's text as installed
|
|
22
|
+
# :edited edited, D != B, I == B: the user changed it, the
|
|
23
|
+
# bundle didn't
|
|
24
|
+
# :diverged edited, D != B, I != B: both changed it
|
|
25
|
+
# :pristine_no_base no B, never edited (D == R's checksum)
|
|
26
|
+
# :edited_no_base no B, edited (or no checksum to tell); or a B equal
|
|
27
|
+
# to D that the checksum calls edited: no base to
|
|
28
|
+
# merge from
|
|
29
|
+
# :dropped_pristine / :dropped_edited / :dropped_missing the bundle no
|
|
30
|
+
# longer ships it (no I): unedited, edited, gone
|
|
31
|
+
# :unreadable on disk, but it can't be read
|
|
32
|
+
# Edited or not is always the one rule (#edited?, the checksum first);
|
|
33
|
+
# the base then says what the user changed.
|
|
34
|
+
#
|
|
35
|
+
# kind, with no source (status, uninstall, diff): I is unknown, so only
|
|
36
|
+
# :pristine :edited :pristine_no_base :edited_no_base :missing :unreadable,
|
|
37
|
+
# from #edited? alone.
|
|
38
|
+
#
|
|
39
|
+
# mark: :none, :current (a mark this chi wrote) or :legacy (an older
|
|
40
|
+
# chi's, InstalledBundle.legacy_mark?).
|
|
41
|
+
FileState = Data.define(:key, :kind, :mark, :digests, :entry, :paths, :claimants) do
|
|
42
|
+
# Provenance.unchanged?'s answer negated: the one edited rule (checksum
|
|
43
|
+
# first, then the base). A file it can't read counts as edited.
|
|
44
|
+
def edited?
|
|
45
|
+
return false if entry.nil?
|
|
46
|
+
|
|
47
|
+
!Provenance.unchanged?(paths[:current], entry, paths[:base])
|
|
48
|
+
rescue SystemCallError
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def marked? = mark != :none
|
|
53
|
+
|
|
54
|
+
# A marked file whose source moved since the mark: the mark speaks of
|
|
55
|
+
# other text than the bundle ships now (Remerge's first question). A mark
|
|
56
|
+
# that names no text (an older chi's) never moved.
|
|
57
|
+
def source_moved?
|
|
58
|
+
marked? && !digests.marked_incoming.nil? && !digests.incoming.nil? && digests.marked_incoming != digests.incoming
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The other installed bundles that list the file too. Asked lazily: only
|
|
62
|
+
# a dropped file and an uninstall need it (Provenance.claimants reads
|
|
63
|
+
# every record).
|
|
64
|
+
def shared_with = claimants.call
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The digests of the file's texts, each read once: disk (:unreadable when
|
|
68
|
+
# it can't be read), base, incoming (nil with no source), the record's
|
|
69
|
+
# checksum, and the text a mark speaks of (nil without a mark or for an
|
|
70
|
+
# older chi's mark).
|
|
71
|
+
FileState::Digests = Data.define(:disk, :base, :incoming, :recorded, :marked_incoming)
|
|
72
|
+
|
|
73
|
+
class << FileState
|
|
74
|
+
# @param record [InstalledBundle, nil] the bundle's record
|
|
75
|
+
# @param provenance [Provenance] the record's (for the base path)
|
|
76
|
+
# @param source_dir [String, nil] the bundle's incoming dir; nil: the
|
|
77
|
+
# source-less kinds
|
|
78
|
+
# @param claimants [#call, nil] key => other bundles listing it
|
|
79
|
+
# (Provenance.claimants), called at most once
|
|
80
|
+
# @param shipped [Boolean] false: the bundle no longer ships the key
|
|
81
|
+
# (a dropped kind), whatever its source dir holds by that name
|
|
82
|
+
# @return [FileState]
|
|
83
|
+
def of(key, record:, provenance:, target_dir:, source_dir:, claimants: nil, shipped: true)
|
|
84
|
+
entry = record&.files&.fetch(key, nil)
|
|
85
|
+
paths = { base: provenance.base_path(key), current: File.join(target_dir, key),
|
|
86
|
+
incoming: source_dir && File.join(source_dir, key) }
|
|
87
|
+
mark = mark_of(entry)
|
|
88
|
+
digests = FileState::Digests.new(
|
|
89
|
+
disk: digest(paths[:current]), base: entry ? digest(paths[:base]) : nil,
|
|
90
|
+
incoming: source_dir && shipped ? digest(paths[:incoming]) : nil, recorded: entry && Provenance.recorded_sha(entry[:checksum]),
|
|
91
|
+
marked_incoming: mark == :none ? nil : entry[:incoming_sha]
|
|
92
|
+
)
|
|
93
|
+
shared = nil
|
|
94
|
+
lookup = -> { shared ||= claimants ? claimants.call(key) : [] }
|
|
95
|
+
state = FileState.new(key: key, kind: nil, mark: mark, digests: digests, entry: entry, paths: paths, claimants: lookup)
|
|
96
|
+
state.with(kind: source_dir ? kind_with_source(state) : kind_without_source(state))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def mark_of(entry)
|
|
102
|
+
return :none unless entry && entry[:conflict]
|
|
103
|
+
|
|
104
|
+
InstalledBundle.legacy_mark?(entry) ? :legacy : :current
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def digest(path)
|
|
108
|
+
FileDigest.hex(path)
|
|
109
|
+
rescue SystemCallError
|
|
110
|
+
:unreadable
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def kind_with_source(state)
|
|
114
|
+
d = state.digests
|
|
115
|
+
return not_owned(d) unless state.entry
|
|
116
|
+
return :unreadable if d.disk == :unreadable
|
|
117
|
+
return dropped(state) if d.incoming.nil?
|
|
118
|
+
return :missing if d.disk.nil?
|
|
119
|
+
return :level if d.disk == d.incoming
|
|
120
|
+
return state.edited? ? :edited_no_base : :pristine_no_base if d.base.nil?
|
|
121
|
+
return :pristine unless state.edited?
|
|
122
|
+
# Edited by the one rule, yet equal to its base: the base can't say
|
|
123
|
+
# what the user changed (it isn't the text the record vouches for),
|
|
124
|
+
# so there is none to merge from.
|
|
125
|
+
return :edited_no_base if d.disk == d.base
|
|
126
|
+
|
|
127
|
+
d.incoming == d.base ? :edited : :diverged
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def kind_without_source(state)
|
|
131
|
+
d = state.digests
|
|
132
|
+
return :unreadable if d.disk == :unreadable
|
|
133
|
+
return :missing if d.disk.nil?
|
|
134
|
+
return state.edited? ? :edited_no_base : :pristine_no_base if d.base.nil?
|
|
135
|
+
|
|
136
|
+
state.edited? ? :edited : :pristine
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def not_owned(digests)
|
|
140
|
+
return :new if digests.disk.nil?
|
|
141
|
+
|
|
142
|
+
digests.disk == digests.incoming ? :user_same : :user_file
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def dropped(state)
|
|
146
|
+
return :dropped_missing if state.digests.disk.nil?
|
|
147
|
+
|
|
148
|
+
state.edited? ? :dropped_edited : :dropped_pristine
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -93,7 +93,8 @@ module Samagotchi
|
|
|
93
93
|
return nil unless bundle.plugin
|
|
94
94
|
|
|
95
95
|
file = bundle.plugin.file
|
|
96
|
-
{ file: file, sha256: copy(provenance.plugin_path(bundle), File.join(dir, file), bundle.plugin.sha256)
|
|
96
|
+
{ file: file, sha256: copy(provenance.plugin_path(bundle), File.join(dir, file), bundle.plugin.sha256),
|
|
97
|
+
required: bundle.manifest.plugin_required }
|
|
97
98
|
end
|
|
98
99
|
private_class_method :plugin
|
|
99
100
|
|
|
@@ -50,6 +50,11 @@ module Samagotchi
|
|
|
50
50
|
# as Manifest reads them (settings nil when
|
|
51
51
|
# the manifest said nothing of them)
|
|
52
52
|
# file_descriptions file key => the memory's description
|
|
53
|
+
# plugin_required plugin: required: true (a guardrail plugin:
|
|
54
|
+
# one that fails to load denies every tool
|
|
55
|
+
# call): the record's (PluginRef#required,
|
|
56
|
+
# recorded at install) or the kept manifest's,
|
|
57
|
+
# so a lost or edited kept file keeps it
|
|
53
58
|
# includes a profile's recorded members (the record's
|
|
54
59
|
# identity, never the kept file's), nil for a plain bundle
|
|
55
60
|
# unknown_keys the kept manifest's keys this chi doesn't know
|
|
@@ -59,7 +64,7 @@ module Samagotchi
|
|
|
59
64
|
class InstalledManifest < Data.define(:name, :version, :requires_chi, :trust_level, :description, :hooks, :needs,
|
|
60
65
|
:script_names, :context_providers, :block_renderers, :session_settings,
|
|
61
66
|
:session_setting_types, :settings, :setting_types, :file_descriptions,
|
|
62
|
-
:includes, :unknown_keys, :floor_keys, :problems, :origin)
|
|
67
|
+
:plugin_required, :includes, :unknown_keys, :floor_keys, :problems, :origin)
|
|
63
68
|
# The key problems names the kept file's standing under.
|
|
64
69
|
KEPT_FILE = "manifest.yml"
|
|
65
70
|
# How many manifests the cache holds before it starts again.
|
|
@@ -133,6 +138,7 @@ module Samagotchi
|
|
|
133
138
|
session_settings: session.session_settings, session_setting_types: session.session_setting_types,
|
|
134
139
|
settings: settings.settings, setting_types: settings.setting_types,
|
|
135
140
|
file_descriptions: pick.call("files", kept.file_descriptions, mirrored.file_descriptions),
|
|
141
|
+
plugin_required: kept.plugin&.dig(:required) == true || mirrored.plugin_required,
|
|
136
142
|
includes: bundle.includes,
|
|
137
143
|
unknown_keys: kept.unknown_keys.map(&:to_s), floor_keys: floors(kept), problems: problems.freeze, origin: :kept)
|
|
138
144
|
end
|
|
@@ -168,6 +174,7 @@ module Samagotchi
|
|
|
168
174
|
settings: list(raw["settings"]),
|
|
169
175
|
setting_types: types(raw["setting_types"]),
|
|
170
176
|
file_descriptions: file_descriptions(bundle.files),
|
|
177
|
+
plugin_required: bundle.plugin&.required == true,
|
|
171
178
|
includes: bundle.includes, unknown_keys: [], floor_keys: {}, problems: problems.freeze, origin: :record)
|
|
172
179
|
end
|
|
173
180
|
|