samagotchi 0.157.0 → 0.159.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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -1
  3. data/README.md +1 -1
  4. data/bin/chi +5 -1
  5. data/docs/architecture.md +14 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +2 -1
  8. data/docs/cli.md +76 -17
  9. data/docs/configuration.md +16 -5
  10. data/docs/context.md +12 -4
  11. data/docs/guardrails.md +11 -8
  12. data/docs/hooks.md +47 -80
  13. data/docs/internals/bridge.md +2 -1
  14. data/docs/internals/bundles.md +51 -36
  15. data/docs/internals/engine.md +7 -5
  16. data/docs/memory.md +43 -9
  17. data/docs/plugins.md +13 -9
  18. data/docs/releasing.md +6 -5
  19. data/docs/scheduling.md +228 -0
  20. data/docs/sessions.md +30 -6
  21. data/docs/testing.md +4 -4
  22. data/lib/samagotchi/bridge/card_places.rb +5 -13
  23. data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
  24. data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
  25. data/lib/samagotchi/bridge.rb +26 -2
  26. data/lib/samagotchi/bundle_command/check.rb +23 -10
  27. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  28. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  29. data/lib/samagotchi/bundle_command.rb +53 -44
  30. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  31. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  32. data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
  33. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  34. data/lib/samagotchi/client_id.rb +13 -3
  35. data/lib/samagotchi/config.rb +12 -0
  36. data/lib/samagotchi/context_command.rb +7 -5
  37. data/lib/samagotchi/context_move.rb +74 -0
  38. data/lib/samagotchi/context_sources.rb +4 -2
  39. data/lib/samagotchi/engine.rb +1 -6
  40. data/lib/samagotchi/extension_load.rb +20 -41
  41. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  42. data/lib/samagotchi/guardrails/pair_boundary.rb +1 -1
  43. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  44. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  45. data/lib/samagotchi/hook_routes.rb +2 -2
  46. data/lib/samagotchi/hooks.rb +2 -3
  47. data/lib/samagotchi/memory_bundle/asset_installer.rb +112 -211
  48. data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
  49. data/lib/samagotchi/memory_bundle/builder.rb +10 -34
  50. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  51. data/lib/samagotchi/memory_bundle/file_actions.rb +51 -0
  52. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  53. data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
  54. data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
  55. data/lib/samagotchi/memory_bundle/installer.rb +32 -14
  56. data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
  57. data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
  58. data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
  59. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  60. data/lib/samagotchi/memory_bundle/shipped_update.rb +41 -41
  61. data/lib/samagotchi/memory_bundle/status.rb +30 -9
  62. data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
  63. data/lib/samagotchi/memory_bundle/trash.rb +7 -12
  64. data/lib/samagotchi/memory_bundle/uninstaller.rb +10 -4
  65. data/lib/samagotchi/memory_orphans.rb +4 -4
  66. data/lib/samagotchi/memory_paths.rb +102 -13
  67. data/lib/samagotchi/plugin/api.rb +1 -4
  68. data/lib/samagotchi/plugin/loader.rb +42 -5
  69. data/lib/samagotchi/project_command.rb +167 -0
  70. data/lib/samagotchi/project_init.rb +69 -0
  71. data/lib/samagotchi/project_scope.rb +6 -4
  72. data/lib/samagotchi/prompt_files.rb +10 -7
  73. data/lib/samagotchi/recap_place.rb +3 -4
  74. data/lib/samagotchi/run_options.rb +2 -0
  75. data/lib/samagotchi/schedule_command.rb +252 -0
  76. data/lib/samagotchi/schedules.rb +948 -0
  77. data/lib/samagotchi/session.rb +44 -18
  78. data/lib/samagotchi/session_access/contract.rb +1 -1
  79. data/lib/samagotchi/session_access/local.rb +20 -0
  80. data/lib/samagotchi/session_field.rb +5 -4
  81. data/lib/samagotchi/session_manager/spawn.rb +25 -11
  82. data/lib/samagotchi/session_manager/worker_command.rb +13 -13
  83. data/lib/samagotchi/steer.rb +10 -0
  84. data/lib/samagotchi/system_prompt.rb +20 -11
  85. data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
  86. data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
  87. data/lib/samagotchi/testing/bundle.rb +5 -12
  88. data/lib/samagotchi/testing/install.rb +5 -5
  89. data/lib/samagotchi/testing/recorder.rb +1 -1
  90. data/lib/samagotchi/testing.rb +1 -1
  91. data/lib/samagotchi/tool_declarations.rb +44 -1
  92. data/lib/samagotchi/toolkit_binding.rb +1 -4
  93. data/lib/samagotchi/toolkit_command.rb +1 -4
  94. data/lib/samagotchi/tools/builtin_calls.rb +2 -0
  95. data/lib/samagotchi/tools/builtins.rb +17 -0
  96. data/lib/samagotchi/tools/cancel_schedule.rb +43 -0
  97. data/lib/samagotchi/tools/delegate.rb +12 -8
  98. data/lib/samagotchi/tools/list_schedules.rb +43 -0
  99. data/lib/samagotchi/tools/schedule_task.rb +39 -0
  100. data/lib/samagotchi/version.rb +1 -1
  101. data/lib/samagotchi/web/app.rb +35 -0
  102. data/lib/samagotchi/web/project_list.rb +3 -2
  103. data/lib/samagotchi/web/public/chains.js +32 -8
  104. data/lib/samagotchi/web/public/index.html +10 -2
  105. data/lib/samagotchi/web/public/info_bar.js +4 -3
  106. data/lib/samagotchi/web/public/project_picker.js +86 -4
  107. data/lib/samagotchi/web/public/turn_events.js +4 -0
  108. data/lib/samagotchi/web/session_hub.rb +20 -0
  109. data/lib/samagotchi/worker.rb +24 -6
  110. data/lib/samagotchi/worker_boot.rb +3 -4
  111. data/lib/samagotchi/worker_inbound.rb +23 -0
  112. data/lib/samagotchi/worker_link.rb +5 -0
  113. data/lib/samagotchi/worker_wakes.rb +9 -1
  114. metadata +10 -2
  115. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
@@ -4,7 +4,6 @@ require "digest"
4
4
  require_relative "file_digest"
5
5
  require "fileutils"
6
6
  require "tmpdir"
7
- require_relative "bundle_hook"
8
7
  require_relative "manifest"
9
8
  require_relative "placeholder"
10
9
  require_relative "../memory_paths"
@@ -12,7 +11,9 @@ require_relative "../model_overlay"
12
11
 
13
12
  module Samagotchi
14
13
  module MemoryBundle
15
- # Builds local memories and installed hooks into a shareable bundle.
14
+ # Builds local memories and an installed bundle's rules, plugin and
15
+ # scripts into a shareable bundle (class-file hooks are retired: an older
16
+ # record's hooks/ isn't built).
16
17
  #
17
18
  # Inverse of Installer: reads from the resolved scope directory,
18
19
  # computes SHA256 checksums, writes manifest.yml via Manifest.write,
@@ -27,11 +28,10 @@ module Samagotchi
27
28
  # record and its dir), else from the scope dir:
28
29
  # record the InstalledBundle, nil when none is installed (or
29
30
  # it doesn't read)
30
- # hooks basename => BundleHook; hook_files basename => path
31
31
  # rules guardrails/*.yml paths
32
32
  # plugin the installed plugin file, nil without one
33
33
  # scripts file name => path; context_providers, block_renderers the parsed lists
34
- InstalledParts = Data.define(:record, :hooks, :hook_files, :rules, :plugin, :scripts, :context_providers, :block_renderers)
34
+ InstalledParts = Data.define(:record, :rules, :plugin, :scripts, :context_providers, :block_renderers)
35
35
 
36
36
  attr_reader :built_files, :warnings, :placeholder_warnings, :out_path, :staging_dir
37
37
 
@@ -138,12 +138,10 @@ module Samagotchi
138
138
  end
139
139
  end
140
140
 
141
- # The installed bundle's hooks (a recorded one missing on disk left
142
- # out; one with no recorded sha gets its file's), guardrail rules,
143
- # plugin, scripts, context providers and block renderers (its
144
- # InstalledManifest's). Without hooks or rules in a
145
- # record, the scope dir's hooks/*.rb and guardrails/*.yml (building
146
- # from a raw bundle dir that is also the memories dir).
141
+ # The installed bundle's guardrail rules, plugin, scripts, context
142
+ # providers and block renderers (its InstalledManifest's). Without
143
+ # rules in a record, the scope dir's guardrails/*.yml (building from a
144
+ # raw bundle dir that is also the memories dir).
147
145
  # @return [InstalledParts]
148
146
  def installed_parts(name, target_dir)
149
147
  prov = nil
@@ -156,23 +154,6 @@ module Samagotchi
156
154
  record = nil
157
155
  end
158
156
 
159
- hooks = {}
160
- hook_files = {}
161
- if record&.hooks&.any?
162
- record.hooks.each do |basename, hook|
163
- src = File.join(prov.hooks_dir, basename)
164
- next unless File.exist?(src)
165
-
166
- hooks[basename] = hook.sha256.empty? ? hook.with(sha256: BundleHook.of_file(src).sha256) : hook
167
- hook_files[basename] = src
168
- end
169
- else
170
- Dir.glob(File.join(target_dir, "hooks", "*.rb")).sort.each do |src|
171
- hooks[File.basename(src)] = BundleHook.of_file(src)
172
- hook_files[File.basename(src)] = src
173
- end
174
- end
175
-
176
157
  rules_dir = record&.guardrails&.any? ? prov.guardrails_dir : File.join(target_dir, "guardrails")
177
158
  plugin = record ? prov.plugin_path(record) : nil
178
159
  manifest = record&.manifest
@@ -181,7 +162,7 @@ module Samagotchi
181
162
  raise BuildError, "the installed #{name}'s record: #{problem}" if problem
182
163
 
183
164
  InstalledParts.new(
184
- record: record, hooks: hooks, hook_files: hook_files,
165
+ record: record,
185
166
  rules: Dir.exist?(rules_dir) ? Dir.glob(File.join(rules_dir, "*.yml")).sort : [],
186
167
  plugin: plugin && File.file?(plugin) ? plugin : nil,
187
168
  scripts: installed_scripts(prov, record),
@@ -190,14 +171,10 @@ module Samagotchi
190
171
  )
191
172
  end
192
173
 
193
- # The memories, hooks/, guardrails/, the plugin and scripts/ into the
174
+ # The memories, guardrails/, the plugin and scripts/ into the
194
175
  # staging dir, laid out as Installer reads a bundle.
195
176
  def stage_files(staging, candidates, parts)
196
177
  candidates.each { |src| FileUtils.cp(src, File.join(staging, File.basename(src))) }
197
- unless parts.hook_files.empty?
198
- FileUtils.mkdir_p(File.join(staging, "hooks"))
199
- parts.hook_files.each { |basename, src| FileUtils.cp(src, File.join(staging, "hooks", basename)) }
200
- end
201
178
  unless parts.rules.empty?
202
179
  FileUtils.mkdir_p(File.join(staging, "guardrails"))
203
180
  parts.rules.each { |src| FileUtils.cp(src, File.join(staging, "guardrails", File.basename(src))) }
@@ -225,7 +202,6 @@ module Samagotchi
225
202
  scope: @scope,
226
203
  description: description,
227
204
  files: files_map,
228
- hooks: parts.hooks.empty? ? nil : parts.hooks,
229
205
  trust_level: trust_level,
230
206
  plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin),
231
207
  required: manifest&.plugin_required },
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "file_digest"
4
-
5
3
  module Samagotchi
6
4
  module MemoryBundle
7
- # One hook's metadata, as a bundle's manifest.yml declares it and its
8
- # installed record (manifest.json) keeps it: the file's sha256
5
+ # One class-file hook's metadata, as an older chi's installed record
6
+ # (manifest.json) keeps it (chi no longer installs or loads them:
7
+ # Manifest::HooksRetired; it reads them to retire, uninstall and name
8
+ # them, and the trash selection takes #sha256): the file's sha256
9
9
  # ("sha256:<hex>", "" when none is recorded), the events it registers
10
10
  # for (manifest `event:` for one, `events:` a list; none: it isn't
11
- # loaded), on_error (skip, log or fail_closed) and priority. One
12
- # instance of the hook serves every event it lists. The one place the
13
- # defaults live.
11
+ # loaded), on_error (skip, log or fail_closed) and priority. The one
12
+ # place the defaults live.
14
13
  class BundleHook < Data.define(:sha256, :events, :on_error, :priority)
15
14
  DEFAULT_ON_ERROR = "skip"
16
15
  DEFAULT_PRIORITY = 100
@@ -37,35 +36,11 @@ module Samagotchi
37
36
  priority: (h["priority"] || DEFAULT_PRIORITY).to_i)
38
37
  end
39
38
 
40
- # A hook known only by its file (no declared event): its sha, the defaults.
41
- # Callers pass a file that is there (FileDigest is nil for one that isn't).
42
- def self.of_file(path)
43
- new(sha256: FileDigest.sha256(path))
44
- end
45
-
46
39
  # "sha256:<hex>" from either form, "" for none.
47
40
  def self.prefixed(sha)
48
41
  sha = sha.to_s.strip
49
42
  sha.empty? || sha.start_with?("sha256:") ? sha : "sha256:#{sha}"
50
43
  end
51
-
52
- # The sha's hex, "" when none is recorded.
53
- def hex = sha256.delete_prefix("sha256:")
54
-
55
- # Whether the hook fails closed: on_error fail_closed applies to its
56
- # before_tool_call registration only.
57
- def fail_closed?(event) = on_error == "fail_closed" && event.to_s == "before_tool_call"
58
-
59
- # How `chi bundle status` / `diff` name its events: `event=x`, or
60
- # `events=x,y` for more than one.
61
- def events_field = events.size > 1 ? "events=#{events.join(",")}" : "event=#{events.first}"
62
-
63
- # The string-keyed form manifest.yml and manifest.json hold: `event`
64
- # for one event (as an older chi reads it), `events` for more.
65
- def to_record
66
- listed = events.size > 1 ? { "events" => events.dup } : { "event" => events.first.to_s }
67
- { "sha256" => sha256, **listed, "on_error" => on_error, "priority" => priority }
68
- end
69
44
  end
70
45
  end
71
46
  end
@@ -22,6 +22,8 @@ module Samagotchi
22
22
  # :keep left as it is (already the bundle's, or kept)
23
23
  # :skip left as it is, the bundle doesn't take it
24
24
  # :move_to_trash moved to the trash (a removal)
25
+ # :remove deleted (an asset: save_first copies an edit to the
26
+ # trash first)
25
27
  # :refuse the command stops
26
28
  # :restore the bundle's text put back (SystemBundle at its version)
27
29
  # save_first: a copy of the old bytes goes to the trash before a write
@@ -157,6 +159,55 @@ module Samagotchi
157
159
 
158
160
  TABLES = { install: INSTALL, force: FORCE, upgrade: UPGRADE, remerge: REMERGE, restore: RESTORE,
159
161
  uninstall: UNINSTALL }.freeze
162
+
163
+ # A bundle's own folder (rules, the plugin, scripts: AssetState), which
164
+ # every install replaces whole: the source's files go in, the rest go,
165
+ # an edit (AssetState#edited?) copied to the trash first. A cell is keyed
166
+ # [state, kind_of_file] or [state, :any]. A file the source has is
167
+ # written even when it has its bytes already (and so checked against the
168
+ # manifest's sha, as any other).
169
+ ASSET_WRITE = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk)
170
+ ASSET_REMOVE = Action.new(do: :remove, word: nil, next_base: :none, record: :drop)
171
+ ASSET_GONE = Action.new(do: :skip, word: nil, next_base: :none, record: :drop)
172
+
173
+ # A plain install, install --force, and an upgrade with no record yet.
174
+ ASSET_INSTALL = {
175
+ %i[new any] => ASSET_WRITE, %i[unchanged any] => ASSET_WRITE.with(word: :up_to_date),
176
+ %i[pristine any] => ASSET_WRITE, %i[edited any] => ASSET_WRITE.with(save_first: true),
177
+ %i[linked any] => ASSET_WRITE,
178
+ # The source dropped it: it goes with the folder, without a word.
179
+ %i[dropped_pristine any] => ASSET_REMOVE, %i[dropped_edited any] => ASSET_REMOVE.with(save_first: true),
180
+ %i[missing any] => ASSET_GONE
181
+ }.freeze
182
+
183
+ # An upgrade (--force too) over the record: a file it replaces is
184
+ # updated, except a rule (an upgrade lists a replaced rule as installed:
185
+ # the corpus's rule-installed).
186
+ ASSET_UPGRADE = ASSET_INSTALL.merge(
187
+ %i[pristine any] => ASSET_WRITE.with(word: :updated),
188
+ %i[edited any] => ASSET_WRITE.with(word: :updated, save_first: true),
189
+ %i[linked any] => ASSET_WRITE.with(word: :updated),
190
+ %i[pristine rule] => ASSET_WRITE, %i[edited rule] => ASSET_WRITE.with(save_first: true),
191
+ %i[linked rule] => ASSET_WRITE
192
+ ).freeze
193
+
194
+ # Uninstall: the folder goes (no source, so every file there is a
195
+ # dropped_* or a link), an edit copied to the trash first, --force or not.
196
+ ASSET_UNINSTALL = {
197
+ %i[dropped_pristine any] => ASSET_REMOVE, %i[dropped_edited any] => ASSET_REMOVE.with(save_first: true),
198
+ %i[linked any] => ASSET_REMOVE, %i[missing any] => ASSET_GONE
199
+ }.freeze
200
+
201
+ ASSET_TABLES = { install: ASSET_INSTALL, upgrade: ASSET_UPGRADE, uninstall: ASSET_UNINSTALL }.freeze
202
+
203
+ # The command's action for an AssetState: [state, kind_of_file], then
204
+ # [state, :any].
205
+ # @param command [Symbol] an ASSET_TABLES key
206
+ # @return [Action, nil] nil: the command never meets the state
207
+ def self.for_asset(command, asset)
208
+ table = ASSET_TABLES.fetch(command)
209
+ table[[asset.state, asset.kind_of_file]] || table[[asset.state, :any]]
210
+ end
160
211
  # The kinds whose row depends on another bundle listing the file.
161
212
  SHARED_ASKED = %i[dropped_pristine dropped_edited pristine pristine_no_base edited edited_no_base].freeze
162
213
 
@@ -119,7 +119,7 @@ module Samagotchi
119
119
  # recorded is gone (:missing). false with no dir.
120
120
  def manifest_unkept? = !dir.nil? && (manifest_sha.to_s.empty? || !File.file?(kept_manifest_path))
121
121
 
122
- # What its manifest says (requires_chi, hook metadata, needs, providers,
122
+ # What its manifest says (requires_chi, needs, providers,
123
123
  # renderers, settings…): the one way to read manifest data of an
124
124
  # installed bundle.
125
125
  # @return [InstalledManifest]
@@ -3,27 +3,28 @@
3
3
  require "fileutils"
4
4
  require "tmpdir"
5
5
  require "yaml"
6
- require_relative "bundle_hook"
7
6
  require_relative "manifest"
8
7
  require_relative "provenance"
9
8
 
10
9
  module Samagotchi
11
10
  module MemoryBundle
12
- # An installed bundle laid out as a source folder again: the plugin, hooks,
11
+ # An installed bundle laid out as a source folder again: the plugin,
13
12
  # rules and scripts as they are installed (a dev install's links
14
13
  # followed), the memories as their bases (the bundle's bytes, not the
15
14
  # user's edits). A bundle's source may be gone or be a URL; this is what
16
15
  # `chi update` can check of what really runs.
17
16
  #
18
- # manifest.yml: its files:, hooks:, plugin: and scripts: are the record's
19
- # and the copy's (what is installed: a memory the install left out, or one
20
- # with no base, and a manifest hook the record lacks aren't listed, so the
21
- # check finds no missing-file for them). Every other key is the kept
17
+ # manifest.yml: its files:, plugin: and scripts: are the record's and
18
+ # the copy's (what is installed: a memory the install left out, or one
19
+ # with no base, isn't listed, so the check finds no missing-file for it).
20
+ # It has no hooks: and no hooks/, whatever the record or the kept
21
+ # manifest says: class-file hooks don't load (InstalledCheck names a
22
+ # record's once). Every other key is the kept
22
23
  # manifest.yml's when it is read (InstalledManifest origin :kept), so the
23
24
  # check sees its unknown keys too; else the record's mirror's.
24
25
  #
25
26
  # The shas are the files' own, so an edited plugin is checked as written.
26
- # A plugin, hook or script the record lists that isn't there stays
27
+ # A plugin or script the record lists that isn't there stays
27
28
  # listed, for the check's missing-file.
28
29
  module InstalledCopy
29
30
  module_function
@@ -45,7 +46,7 @@ module Samagotchi
45
46
  dir: dir, name: bundle.name, version: bundle.version.to_s.empty? ? "0" : bundle.version, trust_level: manifest.trust_level,
46
47
  files: memories(bundle, provenance, dir),
47
48
  file_descriptions: manifest.file_descriptions,
48
- hooks: hooks(bundle, provenance, dir), plugin: plugin(bundle, provenance, dir),
49
+ plugin: plugin(bundle, provenance, dir),
49
50
  scripts: copies(bundle.scripts, provenance.scripts_dir, File.join(dir, "scripts")),
50
51
  requires_chi: manifest.requires_chi, needs: manifest.needs,
51
52
  context_providers: manifest.context_providers, block_renderers: manifest.block_renderers,
@@ -57,7 +58,8 @@ module Samagotchi
57
58
  end
58
59
  private_class_method :write
59
60
 
60
- # What is installed, which the copy's manifest.yml takes from the record.
61
+ # What is installed, which the copy's manifest.yml takes from the
62
+ # record (hooks: none, so the kept manifest's isn't taken either).
61
63
  INSTALLED_KEYS = %w[name version files hooks plugin scripts].freeze
62
64
 
63
65
  # The written manifest's INSTALLED_KEYS and +failed+ keys (the value
@@ -80,15 +82,6 @@ module Samagotchi
80
82
  end
81
83
  private_class_method :memories
82
84
 
83
- # The recorded hooks with the metadata they load with (InstalledManifest#hooks).
84
- def hooks(bundle, provenance, dir)
85
- bundle.manifest.hooks.to_h do |file, hook|
86
- sha = copy(File.join(provenance.hooks_dir, file), File.join(dir, "hooks", file), hook.sha256)
87
- [file, hook.with(sha256: sha)]
88
- end
89
- end
90
- private_class_method :hooks
91
-
92
85
  def plugin(bundle, provenance, dir)
93
86
  return nil unless bundle.plugin
94
87
 
@@ -39,9 +39,6 @@ module Samagotchi
39
39
  #
40
40
  # requires_chi, trust_level the strings as recorded, nil when none
41
41
  # description the kept manifest's, nil from the record
42
- # hooks basename => BundleHook: each recorded hook,
43
- # the kept manifest's events, on_error and
44
- # priority with the record's sha
45
42
  # needs Manifest.parse_needs'
46
43
  # script_names the scripts/ files it lists
47
44
  # context_providers Array<ContextProviders::Provider>
@@ -61,7 +58,7 @@ module Samagotchi
61
58
  # floor_keys the kept manifest's Manifest#floor_keys (key =>
62
59
  # the first chi that read it), {} from the record
63
60
  # origin :kept or :record
64
- class InstalledManifest < Data.define(:name, :version, :requires_chi, :trust_level, :description, :hooks, :needs,
61
+ class InstalledManifest < Data.define(:name, :version, :requires_chi, :trust_level, :description, :needs,
65
62
  :script_names, :context_providers, :block_renderers, :session_settings,
66
63
  :session_setting_types, :settings, :setting_types, :file_descriptions,
67
64
  :plugin_required, :includes, :unknown_keys, :floor_keys, :problems, :origin)
@@ -130,7 +127,6 @@ module Samagotchi
130
127
  new(name: bundle.name, version: bundle.version,
131
128
  requires_chi: pick.call("requires_chi", kept.requires_chi, mirrored.requires_chi),
132
129
  trust_level: kept.trust_level, description: kept.description.empty? ? nil : kept.description,
133
- hooks: pick.call("hooks", kept_hooks(bundle.hooks, kept.hooks), bundle.hooks),
134
130
  needs: pick.call("needs", kept.needs, mirrored.needs),
135
131
  script_names: pick.call("scripts", kept.scripts.keys, mirrored.script_names),
136
132
  context_providers: pick.call("context_providers", kept.context_providers, mirrored.context_providers),
@@ -150,21 +146,11 @@ module Samagotchi
150
146
  {}
151
147
  end
152
148
 
153
- # Each recorded hook (what is installed): the kept manifest's metadata
154
- # for it with the record's sha; one the manifest doesn't list (a found
155
- # hooks/*.rb) as recorded. A manifest hook the record lacks isn't installed.
156
- def self.kept_hooks(recorded, declared)
157
- recorded.to_h do |file, hook|
158
- [file, declared.key?(file) ? declared[file].with(sha256: hook.sha256) : hook]
159
- end
160
- end
161
-
162
149
  def self.from_record(bundle)
163
150
  raw = recorded(bundle)
164
151
  problems = {}
165
152
  new(name: bundle.name, version: bundle.version,
166
153
  requires_chi: raw["requires_chi"]&.to_s, trust_level: raw["trust_level"]&.to_s, description: nil,
167
- hooks: bundle.hooks,
168
154
  needs: parsed(problems, "needs", [], Manifest::ValidationError) { Manifest.parse_needs(raw["needs"]) },
169
155
  script_names: bundle.scripts.keys,
170
156
  context_providers: parsed(problems, "context_providers", []) { ContextProviders.parse_list(raw["context_providers"]) },
@@ -204,7 +190,7 @@ module Samagotchi
204
190
  acc[key] = text unless text.empty?
205
191
  end
206
192
  end
207
- private_class_method :deep_freeze, :kept_stat, :build, :standing, :from_kept, :floors, :kept_hooks, :from_record, :recorded, :parsed,
193
+ private_class_method :deep_freeze, :kept_stat, :build, :standing, :from_kept, :floors, :from_record, :recorded, :parsed,
208
194
  :list, :types, :file_descriptions
209
195
 
210
196
  # Why the kept manifest.yml wasn't read: :missing, :stale or :edited (the class
@@ -34,8 +34,9 @@ module Samagotchi
34
34
  class Installer
35
35
  class InstallError < StandardError; end
36
36
 
37
- # What the run did to one file (a memory, or a hook, rule, plugin or
38
- # script: AssetInstaller writes them too), as #results holds it. word is
37
+ # What the run did to one file (a memory, or a rule, plugin or script:
38
+ # AssetInstaller writes them too, and removes an older chi's hooks), as
39
+ # #results holds it. word is
39
40
  # the report bucket (#summary): a real run's (:installed, :updated,
40
41
  # :merged, :kept, :kept_pruned, :kept_shared, :kept_own (a file left as
41
42
  # the user's), :up_to_date, :conflict, :removed) or a dry run's
@@ -166,8 +167,10 @@ module Samagotchi
166
167
  would_install = with_word(:would_install)
167
168
  would_update = with_word(:would_update)
168
169
  fast_forward = with_word(:fast_forward)
169
- removed = with_word(:removed)
170
- would_remove = with_word(:would_remove)
170
+ # An older chi's class-file hooks this run removes: a line of their
171
+ # own, with why (AssetInstaller#retire_hooks).
172
+ retired, removed = with_word(:removed).partition { |key| @results[key].note == AssetInstaller::RETIRED }
173
+ would_retire, would_remove = with_word(:would_remove).partition { |key| @results[key].note == AssetInstaller::RETIRED }
171
174
 
172
175
  lines << "Installed: #{installed.join(", ")}" unless installed.empty?
173
176
  lines << "Updated: #{updated.join(", ")}" unless updated.empty?
@@ -178,6 +181,8 @@ module Samagotchi
178
181
  lines << FileStanding.merged_line(would_merge, @manifest&.version, dry_run: true) unless would_merge.empty?
179
182
  lines << "Removed (no longer in the bundle): #{removed.join(", ")}#{" (moved to #{trash_dir})" if trash_dir}" unless removed.empty?
180
183
  lines << "Would remove (no longer in the bundle): #{would_remove.join(", ")}" unless would_remove.empty?
184
+ lines << "Removed (#{AssetInstaller::RETIRED}): #{retired.join(", ")}" unless retired.empty?
185
+ lines << "Would remove (#{AssetInstaller::RETIRED}): #{would_retire.join(", ")}" unless would_retire.empty?
181
186
  lines << "Kept (local edits preserved): #{kept.join(", ")}" unless kept.empty?
182
187
  lines << "Up to date: #{up_to_date.join(", ")}" unless up_to_date.empty?
183
188
  lines << "Skipped: #{skipped.join(", ")}" unless skipped.empty?
@@ -213,11 +218,11 @@ module Samagotchi
213
218
  def with_word(*words) = @results.select { |_, result| words.include?(result.word) }.keys
214
219
 
215
220
  # A plain install over a bundle that is already installed from the same
216
- # source: it rewrote the bundle's hooks, rules, plugin, scripts and its
221
+ # source: it rewrote the bundle's rules, plugin, scripts and its
217
222
  # record, and left a memory the user wrote as it was. So the line says
218
223
  # what changed, and points at the upgrade for the memories it kept.
219
224
  def reinstall_line
220
- line = "#{@name} was already installed from #{@source}: its hooks, rules, plugin and scripts " \
225
+ line = "#{@name} was already installed from #{@source}: its rules, plugin and scripts " \
221
226
  "now match it and its record was rewritten"
222
227
  # :kept_own is the only way a memory is left out of a plain install
223
228
  # (its KEPT_OWN or HELD row): its bytes differ from the bundle's.
@@ -256,10 +261,10 @@ module Samagotchi
256
261
  # #ref; a path resolved); one it can't place (an older record's
257
262
  # relative path) never refuses.
258
263
  # - a name chi ships comes from a shipped dir only, unless --force
259
- # (install and upgrade): the plugin and hooks it replaces are chi's;
264
+ # (install and upgrade): the plugin and rules it replaces are chi's;
260
265
  # - a plain install over a bundle from another source is refused,
261
266
  # --force too (that overwrites files, it doesn't switch sources):
262
- # it would delete the plugin and hooks and rewrite the record;
267
+ # it would delete the plugin and rules and rewrite the record;
263
268
  # - an upgrade may switch sources (a URL and a local copy of it are
264
269
  # one bundle, an archive's file name has its version), and
265
270
  # #source_change says so (the summary's first line).
@@ -351,19 +356,32 @@ module Samagotchi
351
356
  @bundle_dir = @source_dir = prepared.dir
352
357
  @source_owned = prepared.owned
353
358
  @source_commit = prepared.commit
359
+ refuse_hooks
354
360
  @manifest = Manifest.read(dir: @bundle_dir)
355
361
  clash = @manifest.session_settings_clash
356
362
  raise InstallError, clash if clash
357
363
  rescue SourceNormalizer::UnknownSourceError => e
358
364
  raise InstallError, "Source normalization failed: #{e.message}"
365
+ rescue Manifest::HooksRetired => e
366
+ # A rule, not a strictness option: refused strict or not, a dry run too.
367
+ raise InstallError, e.message
359
368
  rescue Manifest::ValidationError => e
360
369
  raise InstallError, e.message if @strict
361
370
 
362
371
  @warnings << "No manifest found — proceeding without strict manifest validation"
363
372
  end
364
373
 
374
+ # A source that ships class-file hooks (hooks/*.rb, listed or not) is
375
+ # refused before its manifest is read; its manifest's hooks: is
376
+ # Manifest.read's refusal (Manifest::HooksRetired either way).
377
+ # @raise [Manifest::HooksRetired]
378
+ def refuse_hooks
379
+ files = Manifest::HooksRetired.files_in(@bundle_dir)
380
+ raise Manifest::HooksRetired.for_files(files) unless files.empty?
381
+ end
382
+
365
383
  # A bundle whose requires_chi this chi doesn't meet is not installed at
366
- # all: --force is what installs it, and its plugin, hooks and rules then
384
+ # all: --force is what installs it, and its plugin and rules then
367
385
  # warn that they won't load (AssetInstaller). The words come from
368
386
  # Manifest.requires_chi_failure, so the requirement is said one way.
369
387
  # A dry run is refused the same way: nothing there would load either.
@@ -555,8 +573,11 @@ module Samagotchi
555
573
  # differs from its manifest's sha256. One kept or skipped isn't checked
556
574
  # (it takes nothing from the bundle). What is checked is the source
557
575
  # file, the bytes the write or merge takes, so a dry run checks what it
558
- # would bring in, never the user's bytes on disk.
576
+ # would bring in, never the user's bytes on disk. A dev install follows
577
+ # its own tree; chi bundle check flags its stale shas instead.
559
578
  def verify_memory_checksums
579
+ return if @dev
580
+
560
581
  @bundle_md.each do |file_key|
561
582
  result = @results[file_key]
562
583
  next unless result&.action&.takes_source?
@@ -579,7 +600,7 @@ module Samagotchi
579
600
 
580
601
  # The install's record (Provenance#write), only with a manifest and
581
602
  # not on a dry run. An upgrade with conflicts writes it too: the new
582
- # version, the plugin's and hooks' new shas, and the old base for each
603
+ # version, the plugin's new sha, and the old base for each
583
604
  # conflicted file, marked conflict (chi bundle status shows it).
584
605
  # Skipping it left the replaced plugin failing its sha check.
585
606
  # @param installed [AssetInstaller::Installed]
@@ -589,11 +610,8 @@ module Samagotchi
589
610
  scope: @target_scope,
590
611
  version: @manifest.version,
591
612
  source_path: @source,
592
- # AssetInstaller added the discovered hooks/*.rb to manifest.hooks.
593
- hooks: @manifest.hooks,
594
613
  trust_level: @manifest.trust_level,
595
614
  source_commit: @source_commit,
596
- hooks_files: installed.hook_files,
597
615
  guardrails_files: installed.guardrail_files,
598
616
  plugin_file: installed.plugin_file,
599
617
  plugin_required: @manifest.plugin&.dig(:required) == true,