samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
autoload :Log, File.expand_path("log", __dir__)
|
|
5
|
+
autoload :VERSION, File.expand_path("version", __dir__)
|
|
6
|
+
module MemoryBundle
|
|
7
|
+
autoload :Manifest, File.expand_path("memory_bundle/manifest", __dir__)
|
|
8
|
+
autoload :Provenance, File.expand_path("memory_bundle/provenance", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Declarative block renderers (docs/plugins.md, docs/internals/block-renderers.md):
|
|
12
|
+
# a bundle's manifest maps a fenced block's language to the scripts/ files the
|
|
13
|
+
# owner's page loads to draw it:
|
|
14
|
+
#
|
|
15
|
+
# block_renderers:
|
|
16
|
+
# - lang: mermaid
|
|
17
|
+
# load: [mermaid.tiny.js, mermaid_renderer.js]
|
|
18
|
+
#
|
|
19
|
+
# JS draws, in a sandboxed frame; Ruby only routes and serves, so chi web
|
|
20
|
+
# (which runs no plugins) can answer, and a reloaded page draws after the
|
|
21
|
+
# worker has gone. The installer records them in the bundle's provenance;
|
|
22
|
+
# .for_installed reads them from there.
|
|
23
|
+
module BlockRenderers
|
|
24
|
+
# A renderer as the manifest declares it.
|
|
25
|
+
Renderer = Data.define(:lang, :load) do
|
|
26
|
+
def to_h = { "lang" => lang, "load" => load }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# One the page may use: the bundle that won the language, and its files.
|
|
30
|
+
Installed = Data.define(:lang, :bundle, :files)
|
|
31
|
+
|
|
32
|
+
class Invalid < ArgumentError; end
|
|
33
|
+
|
|
34
|
+
# A language a block can carry: a fence's first word, downcased
|
|
35
|
+
# (MarkdownRenderer sets it as data-lang under this same pattern).
|
|
36
|
+
LANG = /\A[a-z0-9_+-]{1,32}\z/
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The manifest's block_renderers: list.
|
|
41
|
+
# @return [Array<Renderer>] [] when absent
|
|
42
|
+
# @raise [Invalid]
|
|
43
|
+
def parse_list(raw)
|
|
44
|
+
return [] if raw.nil?
|
|
45
|
+
raise Invalid, "block_renderers: must be a list of mappings" unless raw.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
renderers = raw.map { |item| parse(item) }
|
|
48
|
+
twice = renderers.map(&:lang).tally.select { |_lang, count| count > 1 }.keys
|
|
49
|
+
raise Invalid, "block_renderers: #{twice.join(", ")} listed more than once" unless twice.empty?
|
|
50
|
+
|
|
51
|
+
renderers
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @raise [Invalid]
|
|
55
|
+
def parse(item)
|
|
56
|
+
raise Invalid, "block_renderers: each item must be a mapping, not #{item.inspect}" unless item.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
data = item.transform_keys(&:to_s)
|
|
59
|
+
lang = data["lang"]
|
|
60
|
+
unless lang.is_a?(String) && lang.match?(LANG)
|
|
61
|
+
raise Invalid, "block_renderers: lang #{lang.inspect} isn't a fence's language (lowercase letters, digits, _ + -; at most 32)"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
load = data["load"]
|
|
65
|
+
unless load.is_a?(Array) && !load.empty? && load.all? { |file| file.is_a?(String) && file.match?(MemoryBundle::Manifest::SCRIPT_FILE) }
|
|
66
|
+
raise Invalid, "block_renderers: #{lang}'s load must be a non-empty list of file names in scripts/, not #{load.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Renderer.new(lang: lang, load: load)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# What the installed bundles draw: the first bundle by name wins a
|
|
73
|
+
# language. A bundle whose requires_chi this chi doesn't meet, or whose list
|
|
74
|
+
# doesn't parse (logged), is skipped; a record that doesn't read lists none.
|
|
75
|
+
# @return [Array<Installed>]
|
|
76
|
+
def for_installed
|
|
77
|
+
taken = {}
|
|
78
|
+
MemoryBundle::Provenance.each_installed do |bundle, record|
|
|
79
|
+
next unless record.block_renderers?
|
|
80
|
+
next if MemoryBundle::Manifest.requires_chi_failure(record.requires_chi, VERSION)
|
|
81
|
+
|
|
82
|
+
renderers(bundle, record).each do |renderer|
|
|
83
|
+
taken[renderer.lang] ||= Installed.new(lang: renderer.lang, bundle: bundle, files: renderer.load)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
taken.values
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The installed path of +file+, once a renderer .for_installed lists loads
|
|
90
|
+
# it from +bundle+ (neither name is ever joined into a path before that:
|
|
91
|
+
# they come from the URL) and it is as installed. This code runs in the
|
|
92
|
+
# owner's browser, so it is checked as a plugin's load and a provider's
|
|
93
|
+
# script are; a --dev install's links are not.
|
|
94
|
+
# @return [String, nil] nil when no such renderer loads such a file
|
|
95
|
+
# @raise [Invalid] the file is gone or changed
|
|
96
|
+
def script_path(bundle, file)
|
|
97
|
+
return nil unless for_installed.any? { |renderer| renderer.bundle == bundle && renderer.files.include?(file) }
|
|
98
|
+
|
|
99
|
+
provenance = MemoryBundle::Provenance.new(name: bundle)
|
|
100
|
+
path = File.join(provenance.scripts_dir, file)
|
|
101
|
+
recorded = provenance.record&.scripts&.fetch(file, nil)
|
|
102
|
+
return path if File.file?(path) && MemoryBundle::Provenance.sha_matches?(path, recorded)
|
|
103
|
+
|
|
104
|
+
raise Invalid, "bundle #{bundle}'s scripts/#{file} differs from the installed one (reinstall the bundle)"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def renderers(bundle, record)
|
|
108
|
+
parse_list(record.block_renderers)
|
|
109
|
+
rescue ArgumentError => e # Invalid is one; so are a record's bytes that aren't UTF-8
|
|
110
|
+
Log.warn(:web, "block_renderers_unreadable", bundle: bundle, msg: e.message)
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
private_class_method :renderers
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -477,11 +477,14 @@ module Samagotchi
|
|
|
477
477
|
|
|
478
478
|
def system_bundle_line(result, dry_run)
|
|
479
479
|
version = "v#{result.to || result.from}"
|
|
480
|
+
standing = MemoryBundle::FileStanding.clauses(MemoryBundle::SystemBundle::BUNDLE_NAME, nil, merged: Array(result.merged),
|
|
481
|
+
behind: Array(result.behind), dry_run: dry_run).join("; ")
|
|
480
482
|
text = case result.status
|
|
481
483
|
when :installed then dry_run ? "would install #{version}" : "#{version} installed"
|
|
482
|
-
when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}"
|
|
484
|
+
when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}#{" (#{standing})" unless standing.empty?}"
|
|
483
485
|
when :restored then "#{version} #{dry_run ? "would restore" : "restored"} missing files"
|
|
484
|
-
when :
|
|
486
|
+
when :merged then "#{version} #{standing}"
|
|
487
|
+
when :up_to_date then "#{version} #{standing.empty? ? "up to date" : standing}"
|
|
485
488
|
when :newer_installed then "v#{result.from} left as is (newer than this chi's v#{result.to})"
|
|
486
489
|
when :skipped then "skipped (#{result.error})"
|
|
487
490
|
else "failed (#{result.error})"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../steer"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
class Bridge
|
|
7
|
+
# Where a CardStore's entries sit in a session's messages: the turns the
|
|
8
|
+
# page draws after each. The messages count, with the rule the page and
|
|
9
|
+
# the recap share (Steer.turn_opening?); the store's turn log only orders
|
|
10
|
+
# the ids. So a turn with no opening (a reminder, a Continue, a failed or
|
|
11
|
+
# rolled-back one, a prompt stopped before the model) counts nothing, and
|
|
12
|
+
# an opening with no id (/continue no's reason, Qwen's recovery line) counts
|
|
13
|
+
# as it does on the page. Nothing orders such an opening against an entry, so
|
|
14
|
+
# an entry says how many it came after (+past_idless+, #idless_after). Pure:
|
|
15
|
+
# no Engine, no file.
|
|
16
|
+
class CardPlaces
|
|
17
|
+
# @param turn_log [Array<String>] the turn ids, oldest first
|
|
18
|
+
# @param messages [Array<Hash>] the messages the page draws
|
|
19
|
+
def initialize(turn_log, messages)
|
|
20
|
+
positions = turn_log.each_with_index.to_h
|
|
21
|
+
@ids = Array(messages).select { |message| Steer.turn_opening?(message) }.map { |message| message[:turn_id] || message["turn_id"] }
|
|
22
|
+
@openings = @ids.size
|
|
23
|
+
@opened = @ids.compact.to_set
|
|
24
|
+
@places = @ids.map { |id| positions[id] }
|
|
25
|
+
index_by_position(@places, turn_log.size)
|
|
26
|
+
@positions = positions
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The openings after the last one whose turn is at or before the entry's
|
|
30
|
+
# in the log (none: from the first one whose turn is later; none: 0),
|
|
31
|
+
# less the ones with no id it came after. An entry from an older file
|
|
32
|
+
# (+base_since+) has no id: all the log's turns are later, on top of what
|
|
33
|
+
# it had counted.
|
|
34
|
+
# @param entry [Hash] a store's entry: :turn_id, :base_since
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
def turns_since(entry)
|
|
37
|
+
position = @positions.fetch(entry[:turn_id], -1)
|
|
38
|
+
at_or_before = position >= 0 ? @last_at_or_before[position] : nil
|
|
39
|
+
since = if at_or_before
|
|
40
|
+
@openings - at_or_before - 1 - [entry[:past_idless].to_i, idless_run(at_or_before)].min
|
|
41
|
+
else
|
|
42
|
+
later = @first_after[position + 1]
|
|
43
|
+
later ? @openings - later : 0
|
|
44
|
+
end
|
|
45
|
+
entry[:base_since].to_i + since
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The openings with no id right after the one +turn_id+ counts from: what
|
|
49
|
+
# an entry shown now has come after. A rollback can only shorten the run, so
|
|
50
|
+
# a count read back against it never eats a later turn.
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
def idless_after(turn_id)
|
|
53
|
+
position = @positions.fetch(turn_id, -1)
|
|
54
|
+
at_or_before = position >= 0 ? @last_at_or_before[position] : nil
|
|
55
|
+
at_or_before ? idless_run(at_or_before) : 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Where a log may start and still read an entry at +position+ the same: at
|
|
59
|
+
# the opening that entry counts from, or at the entry's turn with none.
|
|
60
|
+
# @return [Integer] a log position
|
|
61
|
+
def log_start_for(position)
|
|
62
|
+
at_or_before = @last_at_or_before[position]
|
|
63
|
+
at_or_before ? @places[at_or_before] : position
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Boolean] whether that turn has an opening in the messages
|
|
67
|
+
def opened?(turn_id)
|
|
68
|
+
@opened.include?(turn_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def idless_run(opening)
|
|
74
|
+
@ids.drop(opening + 1).take_while(&:nil?).size
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# For each log position: the last opening at or before it, and the first
|
|
78
|
+
# one at or after it. One pass each, for the store's many entries.
|
|
79
|
+
def index_by_position(places, size)
|
|
80
|
+
@last_at_or_before = Array.new(size)
|
|
81
|
+
@first_after = Array.new(size + 1)
|
|
82
|
+
places.each_with_index do |place, opening|
|
|
83
|
+
next unless place
|
|
84
|
+
|
|
85
|
+
@last_at_or_before[place] = opening
|
|
86
|
+
@first_after[place] ||= opening
|
|
87
|
+
end
|
|
88
|
+
(1...size).each { |i| @last_at_or_before[i] = [@last_at_or_before[i - 1], @last_at_or_before[i]].compact.max }
|
|
89
|
+
(size - 1).downto(0) { |i| @first_after[i] = [@first_after[i], @first_after[i + 1]].compact.min }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|