samagotchi 0.157.0 → 0.158.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/README.md +1 -1
  4. data/bin/chi +3 -1
  5. data/docs/architecture.md +7 -6
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +2 -1
  8. data/docs/cli.md +21 -17
  9. data/docs/configuration.md +7 -5
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +8 -7
  12. data/docs/hooks.md +44 -80
  13. data/docs/internals/bridge.md +1 -1
  14. data/docs/internals/bundles.md +47 -34
  15. data/docs/internals/engine.md +7 -5
  16. data/docs/memory.md +39 -9
  17. data/docs/plugins.md +13 -9
  18. data/docs/releasing.md +6 -5
  19. data/docs/sessions.md +5 -5
  20. data/docs/testing.md +4 -4
  21. data/lib/samagotchi/bridge/card_places.rb +5 -13
  22. data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
  23. data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
  24. data/lib/samagotchi/bundle_command/check.rb +23 -10
  25. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  26. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  27. data/lib/samagotchi/bundle_command.rb +15 -43
  28. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  29. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  30. data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
  31. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  32. data/lib/samagotchi/context_command.rb +7 -5
  33. data/lib/samagotchi/context_sources.rb +4 -2
  34. data/lib/samagotchi/engine.rb +1 -6
  35. data/lib/samagotchi/extension_load.rb +20 -41
  36. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  37. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  38. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  39. data/lib/samagotchi/hook_routes.rb +2 -2
  40. data/lib/samagotchi/hooks.rb +2 -3
  41. data/lib/samagotchi/memory_bundle/asset_installer.rb +39 -119
  42. data/lib/samagotchi/memory_bundle/builder.rb +10 -34
  43. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  44. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  45. data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
  46. data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
  47. data/lib/samagotchi/memory_bundle/installer.rb +32 -14
  48. data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
  49. data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
  50. data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
  51. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  52. data/lib/samagotchi/memory_bundle/shipped_update.rb +30 -35
  53. data/lib/samagotchi/memory_bundle/status.rb +12 -10
  54. data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
  55. data/lib/samagotchi/memory_bundle/trash.rb +7 -12
  56. data/lib/samagotchi/memory_orphans.rb +4 -4
  57. data/lib/samagotchi/memory_paths.rb +102 -13
  58. data/lib/samagotchi/plugin/api.rb +1 -4
  59. data/lib/samagotchi/plugin/loader.rb +42 -5
  60. data/lib/samagotchi/project_command.rb +167 -0
  61. data/lib/samagotchi/project_init.rb +69 -0
  62. data/lib/samagotchi/project_scope.rb +6 -4
  63. data/lib/samagotchi/prompt_files.rb +10 -7
  64. data/lib/samagotchi/recap_place.rb +3 -4
  65. data/lib/samagotchi/run_options.rb +1 -0
  66. data/lib/samagotchi/session.rb +44 -18
  67. data/lib/samagotchi/session_access/contract.rb +1 -1
  68. data/lib/samagotchi/session_field.rb +5 -4
  69. data/lib/samagotchi/steer.rb +10 -0
  70. data/lib/samagotchi/system_prompt.rb +20 -11
  71. data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
  72. data/lib/samagotchi/testing/bundle.rb +5 -12
  73. data/lib/samagotchi/testing/install.rb +5 -5
  74. data/lib/samagotchi/testing/recorder.rb +1 -1
  75. data/lib/samagotchi/testing.rb +1 -1
  76. data/lib/samagotchi/tool_declarations.rb +1 -1
  77. data/lib/samagotchi/toolkit_binding.rb +1 -4
  78. data/lib/samagotchi/toolkit_command.rb +1 -4
  79. data/lib/samagotchi/tools/delegate.rb +12 -8
  80. data/lib/samagotchi/version.rb +1 -1
  81. data/lib/samagotchi/web/app.rb +35 -0
  82. data/lib/samagotchi/web/project_list.rb +3 -2
  83. data/lib/samagotchi/web/public/index.html +6 -1
  84. data/lib/samagotchi/web/public/project_picker.js +86 -4
  85. metadata +3 -2
  86. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
@@ -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,
@@ -5,7 +5,6 @@ require "digest"
5
5
  require "fileutils"
6
6
  require_relative "../context_providers"
7
7
  require_relative "../version"
8
- require_relative "bundle_hook"
9
8
 
10
9
  module Samagotchi
11
10
  autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
@@ -66,6 +65,26 @@ module Samagotchi
66
65
  class Manifest
67
66
  class ValidationError < StandardError; end
68
67
 
68
+ # Class-file bundle hooks (hooks/*.rb, the manifest's hooks:) are
69
+ # retired: chi installs and loads none. WORDS is the one wording of
70
+ # it, wherever chi meets one (the Engine's first-turn line, install,
71
+ # chi bundle check and status, chi update's row). A strict read of a
72
+ # manifest with a hooks: entry raises it (.for_key's message); a
73
+ # tolerant read (a kept manifest.yml) ignores the key.
74
+ class HooksRetired < ValidationError
75
+ WORDS = "class-file bundle hooks are retired: chi loads no hooks/*.rb; a bundle hooks events from its " \
76
+ "plugin.rb with chi.on, see docs/plugins.md"
77
+
78
+ # The manifest's hooks: key.
79
+ def self.for_key = new("hooks: #{WORDS}")
80
+
81
+ # A source's hooks/*.rb (+files+, relative), with or without the key.
82
+ def self.for_files(files) = new("#{files.join(", ")}: #{WORDS}")
83
+
84
+ # The source's hooks/*.rb, relative and sorted ([] for none).
85
+ def self.files_in(dir) = Dir.glob(File.join(dir, "hooks", "*.rb")).map { |f| "hooks/#{File.basename(f)}" }.sort
86
+ end
87
+
69
88
  # A plugin file is a plain .rb name in the bundle's top directory.
70
89
  PLUGIN_FILE = /\A[A-Za-z0-9_][A-Za-z0-9_.-]*\.rb\z/
71
90
  # A script is a plain file name in the bundle's scripts/.
@@ -88,7 +107,7 @@ module Samagotchi
88
107
  # admits a chi that never saw the key. A key the first tag (v0.2.0)
89
108
  # already reads has no entry. `files.description` is a files: entry given
90
109
  # as a mapping, `*.types` a session_settings:/settings: given as a mapping
91
- # of keys to types, `hooks.events` a hook entry with an events: list. A key
110
+ # of keys to types. A key
92
111
  # whose release is not out yet waits here as a comment naming it, the way
93
112
  # Plugin::Api::SINCE does, until `release:bump` gives it its version
94
113
  # (docs/releasing.md).
@@ -100,16 +119,14 @@ module Samagotchi
100
119
  "session_settings" => "0.84.0",
101
120
  "session_settings.types" => "0.92.0",
102
121
  "settings.types" => "0.92.0",
103
- "hooks.events" => "0.98.0",
104
122
  "settings" => "0.143.0",
105
123
  "block_renderers" => "0.151.0",
106
124
  "plugin.required" => "0.157.0"
107
125
  }.freeze
108
126
 
109
127
  # files: name → "sha256:<hex>" (both forms); file_descriptions: name →
110
- # the description a mapping gave, for the files that have one; hooks:
111
- # name → BundleHook.
112
- attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :hooks, :trust_level, :plugin, :requires_chi, :needs, :includes,
128
+ # the description a mapping gave, for the files that have one.
129
+ attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :trust_level, :plugin, :requires_chi, :needs, :includes,
113
130
  :scripts, :context_providers, :block_renderers, :session_settings, :session_setting_types, :settings, :setting_types, :unknown_keys
114
131
 
115
132
  def initialize(path:)
@@ -131,7 +148,8 @@ module Samagotchi
131
148
  @scope = normalize_scope(raw["scope"])
132
149
  @description = (raw["description"] || "").to_s
133
150
  @files = parse_files(raw["files"] || {})
134
- @hooks = parse_hooks(raw["hooks"] || {})
151
+ raise HooksRetired.for_key if self.class.lists_hooks?(raw["hooks"])
152
+
135
153
  @file_descriptions = parse_file_descriptions(raw["files"])
136
154
  @trust_level = (raw["trust_level"] || "experimental").to_s
137
155
  @plugin = parse_plugin(raw["plugin"])
@@ -170,8 +188,8 @@ module Samagotchi
170
188
  # chi (a newer chi tightened it, or an older one wrote it) costs only
171
189
  # that key, which reads as empty with its message in the problems:
172
190
  # name, version (never checked: identity is the record's), scope;
173
- # files and their descriptions, scripts, plugin; hooks (one bad entry
174
- # fails the whole key); needs, context_providers; block_renderers
191
+ # files and their descriptions, scripts, plugin (hooks: is ignored,
192
+ # HooksRetired); needs, context_providers; block_renderers
175
193
  # (against +fallback_scripts+ when scripts: failed); includes (after
176
194
  # what its check reads); session_settings with its types, settings
177
195
  # with theirs (one raw value each, so one problem); requires_chi,
@@ -186,6 +204,10 @@ module Samagotchi
186
204
  [manifest, manifest.send(:read_tolerantly, path, fallback_scripts)]
187
205
  end
188
206
 
207
+ # Whether a manifest's hooks: value lists anything (none, {} and []
208
+ # don't: a manifest may keep an empty key).
209
+ def self.lists_hooks?(raw) = !(raw.nil? || ((raw.is_a?(Hash) || raw.is_a?(Array)) && raw.empty?))
210
+
189
211
  def self.read(dir:)
190
212
  manifest_path = File.join(dir, "manifest.yml")
191
213
  raise ValidationError, "manifest.yml not found in #{dir}" unless File.exist?(manifest_path)
@@ -204,13 +226,6 @@ module Samagotchi
204
226
  hex unless hex.nil? || hex.empty?
205
227
  end
206
228
 
207
- # Returns sha256 hex for a hook basename (e.g. "guardrails.rb"), nil
208
- # when it declares none (nothing to verify).
209
- def checksum_for_hook(basename)
210
- hex = @hooks[basename]&.hex
211
- hex unless hex.nil? || hex.empty?
212
- end
213
-
214
229
  # @return [String, nil] the plugin's sha256 hex, "sha256:" stripped
215
230
  def checksum_for_plugin
216
231
  sha = @plugin && @plugin[:sha256].to_s
@@ -242,8 +257,8 @@ module Samagotchi
242
257
 
243
258
  # The SINCE keys this manifest actually uses, each with the chi release
244
259
  # that first read it: the plain keys, and the shapes only a newer chi
245
- # reads — a files: entry given as a mapping, a hook entry with an events:
246
- # list, a session_settings:/settings: given as a mapping of keys to types.
260
+ # reads — a files: entry given as a mapping, a session_settings:/settings:
261
+ # given as a mapping of keys to types.
247
262
  # @return [Hash{String => String}]
248
263
  def floor_keys
249
264
  raw = @raw
@@ -258,15 +273,12 @@ module Samagotchi
258
273
  found["files.description"] = SINCE["files.description"] if raw["files"].is_a?(Hash) && raw["files"].values.any?(Hash)
259
274
  found["session_settings.types"] = SINCE["session_settings.types"] if raw["session_settings"].is_a?(Hash)
260
275
  found["settings.types"] = SINCE["settings.types"] if raw["settings"].is_a?(Hash)
261
- if raw["hooks"].is_a?(Hash) && raw["hooks"].values.any? { |entry| entry.is_a?(Hash) && entry.key?("events") }
262
- found["hooks.events"] = SINCE["hooks.events"]
263
- end
264
276
  found
265
277
  end
266
278
 
267
279
  # Writes a fresh manifest.yml from computed checksums at `dir`.
268
280
  # +file_descriptions+ (name → text) writes those files as mappings.
269
- def self.write(dir:, name:, version:, files:, scope: nil, description: "", hooks: nil, trust_level: nil,
281
+ def self.write(dir:, name:, version:, files:, scope: nil, description: "", trust_level: nil,
270
282
  plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], block_renderers: [], file_descriptions: {},
271
283
  session_settings: [], session_setting_types: {}, settings: nil, setting_types: {})
272
284
  FileUtils.mkdir_p(dir)
@@ -280,7 +292,6 @@ module Samagotchi
280
292
  text = file_descriptions[key].to_s.strip
281
293
  [key, text.empty? ? sha : { "sha256" => sha, "description" => text }]
282
294
  end
283
- manifest["hooks"] = hooks.to_h { |k, v| [k.to_s, BundleHook.parse(v).to_record] } if hooks && !hooks.empty?
284
295
  manifest["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
285
296
  if plugin
286
297
  sha = plugin[:sha256].to_s
@@ -389,7 +400,7 @@ module Samagotchi
389
400
  end
390
401
  @scripts = tolerate(problems, "scripts", {}) { parse_scripts(raw["scripts"]) }
391
402
  @plugin = tolerate(problems, "plugin", nil) { parse_plugin(raw["plugin"]) }
392
- @hooks = tolerate(problems, "hooks", {}) { parse_hooks(raw["hooks"] || {}) }
403
+ # An old kept manifest's hooks: is no problem: it isn't read (HooksRetired).
393
404
  @needs = tolerate(problems, "needs", []) { self.class.parse_needs(raw["needs"]) }
394
405
  @context_providers = tolerate(problems, "context_providers", []) { parse_context_providers(raw["context_providers"]) }
395
406
  script_names = problems.key?("scripts") ? fallback_scripts.map(&:to_s) : @scripts.keys
@@ -546,8 +557,8 @@ module Samagotchi
546
557
 
547
558
  name
548
559
  end.uniq
549
- if !names.empty? && (!@files.empty? || !@hooks.empty? || @plugin || !@scripts.empty?)
550
- raise ValidationError, "a bundle with includes: holds only its includes (no files, hooks, plugin or scripts)"
560
+ if !names.empty? && (!@files.empty? || @plugin || !@scripts.empty?)
561
+ raise ValidationError, "a bundle with includes: holds only its includes (no files, plugin or scripts)"
551
562
  end
552
563
 
553
564
  names
@@ -559,34 +570,6 @@ module Samagotchi
559
570
  text = raw.to_s.strip
560
571
  text.empty? ? nil : text
561
572
  end
562
-
563
- def parse_hooks(raw)
564
- return {} unless raw.is_a?(Hash)
565
-
566
- raw.each_with_object({}) do |(k, v), acc|
567
- next unless k.is_a?(String) && !k.empty?
568
- next unless v.is_a?(Hash)
569
-
570
- check_hook_events(k, v)
571
- acc[k] = BundleHook.parse(v)
572
- end
573
- end
574
-
575
- HOOK_EVENT_NAME = /\A[a-z][a-z0-9_]*\z/
576
-
577
- # A hook's events: list holds event names (one instance registered
578
- # for each); it and event: are one or the other.
579
- def check_hook_events(file, entry)
580
- return unless entry.key?("events")
581
-
582
- raw = entry["events"]
583
- raise ValidationError, "hooks: #{file}: give event: or events:, not both" if entry.key?("event")
584
- raise ValidationError, "hooks: #{file}: events: must be a list of event names" unless raw.is_a?(Array) && !raw.empty?
585
-
586
- raw.each do |item|
587
- raise ValidationError, "hooks: #{file}: events: #{item.inspect} is not an event name" unless item.is_a?(String) && item.strip.match?(HOOK_EVENT_NAME)
588
- end
589
- end
590
573
  end
591
574
  end
592
575
  end
@@ -8,7 +8,6 @@ require "tmpdir"
8
8
  require_relative "../atomic_file"
9
9
  require_relative "../memory_paths"
10
10
  require_relative "../version"
11
- require_relative "bundle_hook"
12
11
  require_relative "file_digest"
13
12
  require_relative "installed_bundle"
14
13
  require_relative "manifest"
@@ -195,7 +194,6 @@ module Samagotchi
195
194
  # the record. nil keeps an entry with no base (a kept edit that has
196
195
  # none: the user's file must never become it), with its recorded
197
196
  # checksum.
198
- # @param hooks_files [Hash] basename => path for hook file snapshots (optional)
199
197
  # @param guardrails_files [Hash] basename => installed rule file; the
200
198
  # sha256 of each is recorded (the Engine checks it at load)
201
199
  # @param plugin_file [String, nil] the installed plugin file; its name
@@ -229,7 +227,9 @@ module Samagotchi
229
227
  # (requires_chi … setting_types) are still written from the kwargs:
230
228
  # an older chi sharing this memories dir reads only them
231
229
  # (docs/internals/bundles.md); a key a later manifest gains is never added
232
- def write(files:, scope:, version:, source_path:, hooks: {}, trust_level: nil, source_commit: nil, hooks_files: {},
230
+ # The record has no hooks ("hooks" => {}: class-file hooks are retired)
231
+ # and an older record's hook bases go.
232
+ def write(files:, scope:, version:, source_path:, trust_level: nil, source_commit: nil,
233
233
  guardrails_files: {}, plugin_file: nil, plugin_required: false, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
234
234
  scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
235
235
  dev: false, manifest_from: nil)
@@ -275,40 +275,8 @@ module Samagotchi
275
275
  end
276
276
  end
277
277
 
278
- # Hooks provenance: persist metadata and base snapshots for hooks
279
- hooks_map = {}
280
- # Incoming hooks: basename => BundleHook (or its metadata hash)
281
- normalized_hooks = {}
282
- if hooks.is_a?(Hash)
283
- hooks.each do |k, v|
284
- next unless k.is_a?(String) && !k.empty?
285
- next unless v.is_a?(Hash) || v.is_a?(BundleHook)
286
-
287
- normalized_hooks[k] = BundleHook.parse(v)
288
- end
289
- end
290
- # Prune stale hook bases
291
- existing&.hooks&.each_key do |old_key_str|
292
- FileUtils.rm_f(File.join(bases_dir, old_key_str)) unless normalized_hooks.key?(old_key_str)
293
- end
294
- # Save base snapshots for hooks where a file path was provided, and
295
- # record the sha256 of the installed file itself: the load-time check
296
- # compares against it, and a declared sha can be wrong (installing
297
- # only warns about that).
298
- hooks_files = {} unless hooks_files.is_a?(Hash)
299
- normalized_hooks.each do |k, hook|
300
- src = hooks_files[k] || hooks_files[k.to_sym]
301
- hook = hook.with(sha256: BundleHook.of_file(src.to_s).sha256) if src && File.exist?(src.to_s)
302
- hook = hook.with(sha256: DEV_SHA) if dev
303
- hooks_map[k] = hook.to_record
304
- next unless src && File.exist?(src.to_s)
305
-
306
- begin
307
- File.write(File.join(bases_dir, k), File.read(src.to_s))
308
- rescue StandardError
309
- nil
310
- end
311
- end
278
+ # An older chi's hooks: their bases go with them (AssetInstaller#retire_hooks).
279
+ existing&.hooks&.each_key { |old_key_str| FileUtils.rm_f(File.join(bases_dir, old_key_str)) }
312
280
 
313
281
  manifest_data = {
314
282
  "name" => @name,
@@ -317,7 +285,7 @@ module Samagotchi
317
285
  "source" => source_path.to_s,
318
286
  "installed_at" => DateTime.now.iso8601,
319
287
  "files" => merged_entries,
320
- "hooks" => hooks_map
288
+ "hooks" => {}
321
289
  }
322
290
  manifest_data["dev"] = true if dev
323
291
  if guardrails_files.is_a?(Hash) && !guardrails_files.empty?
@@ -460,7 +428,7 @@ module Samagotchi
460
428
  # wrote it, or the file is gone), when +dir+ is the source of what's
461
429
  # installed: its manifest names this bundle at the recorded version and
462
430
  # says what the record keeps (#manifest_difference), and every sha the
463
- # record has for a memory, plugin, hook, script or rule is the source
431
+ # record has for a memory, plugin, script or rule is the source
464
432
  # file's. Then the file is copied to #kept_manifest_path and the
465
433
  # record gains its manifest_sha, and nothing else changes (installed_at,
466
434
  # the mirror, installed_by). A dev record is never adopted: its next
@@ -585,8 +553,8 @@ module Samagotchi
585
553
  # The first manifest key in which +manifest+ says other than what the
586
554
  # record keeps, nil when none: each mirrored key as #write records it (one
587
555
  # the record lacks is fine only when an older chi couldn't have known it:
588
- # Manifest::SINCE has it), then each recorded hook's events, on_error and
589
- # priority.
556
+ # Manifest::SINCE has it). An older record's hooks don't count: they
557
+ # no longer load, and the source can't list any (Manifest::HooksRetired).
590
558
  def manifest_difference(bundle, manifest)
591
559
  recorded = plain(bundle.mirror)
592
560
  source = plain(self.class.mirror_fields(
@@ -595,18 +563,11 @@ module Samagotchi
595
563
  session_settings: manifest.session_settings, session_setting_types: manifest.session_setting_types,
596
564
  settings: manifest.settings, setting_types: manifest.setting_types
597
565
  ))
598
- differing = InstalledBundle::MIRROR_KEYS.find do |key|
566
+ InstalledBundle::MIRROR_KEYS.find do |key|
599
567
  next false if recorded[key] == source[key]
600
568
 
601
569
  !(recorded[key].nil? && Manifest::SINCE.key?(MIRROR_SINCE.fetch(key, key)))
602
570
  end
603
- return differing if differing
604
-
605
- same_hooks = bundle.hooks.all? do |file, hook|
606
- theirs = manifest.hooks.fetch(file, BundleHook.new)
607
- [hook.events, hook.on_error, hook.priority] == [theirs.events, theirs.on_error, theirs.priority]
608
- end
609
- same_hooks ? nil : "hooks"
610
571
  end
611
572
  private :manifest_difference
612
573
 
@@ -614,7 +575,7 @@ module Samagotchi
614
575
  private :plain
615
576
 
616
577
  # Each file the record has a sha for, by its path in the bundle's source
617
- # => that sha: the plugin, hooks, scripts, rules, and each memory whose
578
+ # => that sha: the plugin, scripts, rules, and each memory whose
618
579
  # entry has a checksum and no conflict mark (its base is the bundle's text).
619
580
  def installed_shas(bundle, manifest)
620
581
  shas = bundle.files.each_with_object({}) do |(key, entry), acc|
@@ -624,7 +585,6 @@ module Samagotchi
624
585
  file = manifest.plugin && manifest.plugin[:file].to_s
625
586
  shas[file && File.basename(file) == bundle.plugin.file ? file : bundle.plugin.file] = bundle.plugin.sha256
626
587
  end
627
- bundle.hooks.each { |file, hook| shas["hooks/#{file}"] = hook.sha256 }
628
588
  bundle.scripts.each { |file, sha| shas["scripts/#{file}"] = sha }
629
589
  bundle.guardrails.each { |file, sha| shas["guardrails/#{file}"] = sha }
630
590
  shas
@@ -4,6 +4,7 @@ require_relative "index_updater"
4
4
  require_relative "manifest"
5
5
  require_relative "memory_files"
6
6
  require_relative "file_actions"
7
+ require_relative "file_digest"
7
8
  require_relative "merger"
8
9
  require_relative "provenance"
9
10
  require_relative "trash"
@@ -77,7 +78,8 @@ module Samagotchi
77
78
  @behind = []
78
79
  @warnings = []
79
80
  @trash = Trash.new(@name)
80
- shipped = MemoryFiles.select(@source_dir, Manifest.read(dir: @source_dir))
81
+ @manifest = Manifest.read(dir: @source_dir)
82
+ shipped = MemoryFiles.select(@source_dir, @manifest)
81
83
  marked.each { |key, entry| retry_file(key, entry, shipped) }
82
84
  settle unless dry_run
83
85
  Result.new(merged: @merged, settled: @settled, stamped: @stamped.map(&:file), behind: @behind, warnings: @warnings, trash_copy: @trash.copy_line)
@@ -111,7 +113,7 @@ module Samagotchi
111
113
  def retry_row(key, entry, state, action)
112
114
  decision = view.decision(key, state) if action.do == :decide || action.behind?
113
115
  action = FileActions.decided(decision.outcome, :remerge) if action.do == :decide
114
- if action.do == :merge then merge_file(key, entry, decision.outcome)
116
+ if action.do == :merge then merge_file(key, entry, state, decision.outcome)
115
117
  elsif action.behind?
116
118
  waits = decision.outcome.status == :fast_forward
117
119
  mark(key, entry, waits ? behind_for(key, FileStanding::WAITS_FOR_UPGRADE) : view.behind(key, decision.outcome, @record.version))
@@ -141,7 +143,9 @@ module Samagotchi
141
143
  @stamped << behind unless entry[:incoming_sha] && @record.behind_entry(key, entry) == behind && File.exist?(@provenance.incoming_path(key))
142
144
  end
143
145
 
144
- def merge_file(key, entry, outcome)
146
+ def merge_file(key, entry, state, outcome)
147
+ warning = FileDigest.mismatch(key, state.paths[:incoming], @manifest.checksum_for(key))
148
+ @warnings << warning if warning
145
149
  return @merged << key if @dry_run
146
150
  return @merged << key if Merger.write_merged(view.paths(key)[:current], outcome, @trash)
147
151
 
@@ -14,7 +14,7 @@ module Samagotchi
14
14
 
15
15
  # A sha line that doesn't match its file: +old+ is the hex the line
16
16
  # holds ("" for a placeholder `sha256: sha256:`), nil when the entry
17
- # has no sha line at all (a plugin:, hooks: or scripts: entry the
17
+ # has no sha line at all (a plugin: or scripts: entry the
18
18
  # installer then takes unchecked; .refresh can't write one).
19
19
  Stale = Data.define(:file, :old, :new)
20
20
 
@@ -30,7 +30,6 @@ module Samagotchi
30
30
  # names, the layout otherwise untouched. The shapes:
31
31
  # files: a.md: sha256:<hex> (dir/a.md)
32
32
  # a.md:\n sha256: sha256:<hex> (dir/a.md, with description:)
33
- # hooks: h.rb:\n sha256: sha256:<hex> (dir/hooks/h.rb)
34
33
  # plugin: file: p.rb\n sha256: sha256:<hex> (dir/p.rb)
35
34
  # scripts: s.rb: sha256:<hex> (dir/scripts/s.rb)
36
35
  # Returns [text, [[relative file, old sha, new sha], ...]] for the lines
@@ -43,8 +42,9 @@ module Samagotchi
43
42
  [text, changes]
44
43
  end
45
44
 
46
- # The stale sha lines of the bundle at +dir+, then the plugin:, hooks:
47
- # and scripts: entries with no sha line, as [Stale].
45
+ # The stale sha lines of the bundle at +dir+, then the plugin: and
46
+ # scripts: entries with no sha line, as [Stale]. (A hooks: key, which
47
+ # chi refuses, has no sha lines here.)
48
48
  def stale(dir, text = File.read(File.join(dir, "manifest.yml")))
49
49
  _, changes, unlined = scan(dir, text)
50
50
  changes.map { |file, old, new| Stale.new(file, old, new) } +
@@ -86,11 +86,10 @@ module Samagotchi
86
86
  # take a placeholder (`chi bundle check`) parse this text.
87
87
  def parseable(text) = text.gsub(/^(\s*(?:["']?[^"':\s][^"':]*["']?: )?)sha256:(?=[ \t]*(?:#[^\r\n]*)?\r?$)/, "\\1sha256:0")
88
88
 
89
- # The code files the manifest names (plugin, hooks, scripts), as paths
89
+ # The code files the manifest names (plugin, scripts), as paths
90
90
  # relative to the bundle dir.
91
91
  def coded(parsed, plugin_file)
92
92
  (plugin_file.is_a?(String) ? [plugin_file] : []) +
93
- keys(parsed["hooks"]).map { |h| File.join("hooks", h) } +
94
93
  keys(parsed["scripts"]).map { |s| File.join("scripts", s) }
95
94
  end
96
95
  private_class_method :coded
@@ -114,8 +113,6 @@ module Samagotchi
114
113
  when "files"
115
114
  m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and return sub && [sub.delete("\"'"), m[1]]
116
115
  m = line.match(/\A( ["']?([^"':]+)["']?: )sha256:\h*#{TAIL}/) and [m[2], m[1]]
117
- when "hooks"
118
- m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and sub and [File.join("hooks", sub.delete("\"'")), m[1]]
119
116
  when "plugin"
120
117
  m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and plugin_file and [plugin_file, m[1]]
121
118
  when "scripts"