Class: Woods::IndexArtifact
- Inherits:
-
Object
- Object
- Woods::IndexArtifact
- Defined in:
- lib/woods/index_artifact.rb
Overview
Whole Value for the on-disk artifact layout under output_dir.
Centralises all path derivation and atomic write operations so that no
caller ever assembles paths by hand. The dumps/latest pointer file
provides cross-artifact atomicity: consumers always read the pointer
first; the pointer is flipped last, after the dump directory is fully
fsynced.
Instance Method Summary collapse
-
#config_path ⇒ Pathname
Path to the resolved config snapshot written by the embed run.
-
#dumps_root ⇒ Pathname
Root of the per-run dump directories.
-
#fresh? ⇒ Boolean
Returns true when the artifact has never been populated —
woods.jsonis absent AND thedumps/latestpointer does not exist. -
#initialize(output_dir) ⇒ IndexArtifact
constructor
A new instance of IndexArtifact.
-
#latest_dump_path ⇒ Pathname?
Path to the latest complete dump directory, or
nil. -
#latest_pointer_path ⇒ Pathname
Path to the
dumps/latestpointer file (may not exist yet). -
#new_dump_dir(now: Time.now.utc) ⇒ Pathname
Creates a new timestamped dump directory and returns its path.
-
#output_dir ⇒ Pathname
The extraction output directory root.
-
#promote(dump_dir) ⇒ void
Atomically flips the
latestpointer to the given dump directory. -
#read_config ⇒ Hash?
Reads and parses
woods.json, returning the raw hash. -
#write_config(resolved_config_hash) ⇒ void
Atomically writes a resolved config hash as
woods.json.
Constructor Details
#initialize(output_dir) ⇒ IndexArtifact
Returns a new instance of IndexArtifact.
25 26 27 |
# File 'lib/woods/index_artifact.rb', line 25 def initialize(output_dir) @root = Pathname.new(output_dir.to_s) end |
Instance Method Details
#config_path ⇒ Pathname
Path to the resolved config snapshot written by the embed run.
39 40 41 |
# File 'lib/woods/index_artifact.rb', line 39 def config_path @root.join('woods.json') end |
#dumps_root ⇒ Pathname
Root of the per-run dump directories.
46 47 48 |
# File 'lib/woods/index_artifact.rb', line 46 def dumps_root @root.join('dumps') end |
#fresh? ⇒ Boolean
Returns true when the artifact has never been populated — woods.json
is absent AND the dumps/latest pointer does not exist.
Once either file is present the artifact is considered non-fresh and the Bootstrapper loads from it. A fresh artifact boots in pattern-only mode by default, raising MCP::MissingArtifact only under WOODS_REQUIRE_INDEX=1.
65 66 67 |
# File 'lib/woods/index_artifact.rb', line 65 def fresh? !config_path.exist? && !latest_pointer_path.exist? end |
#latest_dump_path ⇒ Pathname?
Path to the latest complete dump directory, or nil.
Returns nil if the pointer file does not exist, if the pointer content
is blank, or if the directory it names no longer exists (stale pointer).
75 76 77 78 79 80 81 82 83 |
# File 'lib/woods/index_artifact.rb', line 75 def latest_dump_path return nil unless latest_pointer_path.exist? dirname = latest_pointer_path.read.strip return nil if dirname.empty? dir = dumps_root.join(dirname) dir.exist? ? dir : nil end |
#latest_pointer_path ⇒ Pathname
Path to the dumps/latest pointer file (may not exist yet).
53 54 55 |
# File 'lib/woods/index_artifact.rb', line 53 def latest_pointer_path dumps_root.join('latest') end |
#new_dump_dir(now: Time.now.utc) ⇒ Pathname
Creates a new timestamped dump directory and returns its path.
The directory is created immediately so callers can begin writing into
it. dumps_root is created on demand. Directory names use dashes in
place of colons for filesystem compatibility (+%H-%M-%SZ+ not
%H:%M:%SZ).
107 108 109 110 111 112 113 |
# File 'lib/woods/index_artifact.rb', line 107 def new_dump_dir(now: Time.now.utc) dirname = now.strftime('%Y-%m-%dT%H-%M-%SZ') dir = dumps_root.join(dirname) FileUtils.mkdir_p(dumps_root) Dir.mkdir(dir.to_s) dir end |
#output_dir ⇒ Pathname
The extraction output directory root.
32 33 34 |
# File 'lib/woods/index_artifact.rb', line 32 def output_dir @root end |
#promote(dump_dir) ⇒ void
This method returns an undefined value.
Atomically flips the latest pointer to the given dump directory.
Uses a temp file + File.rename so a crash mid-flip leaves the previous
pointer intact. dump_dir must exist and resolve to a path inside
dumps_root.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/woods/index_artifact.rb', line 124 def promote(dump_dir) target = Pathname.new(dump_dir.to_s) root_real = dumps_root..to_s target_real = target.exist? ? target.realpath.to_s : '' # Resolve symlinks on both sides before comparing (handles macOS /tmp → /private/var) root_resolved = Pathname.new(root_real).exist? ? Pathname.new(root_real).realpath.to_s : root_real # Prefix must end at a path boundary — a bare start_with? would accept # sibling directories like "dumps-backup" or "dumpsevil", and the # basename-only pointer written below would then name a directory that # doesn't exist under dumps_root. unless target.exist? && target_real.start_with?("#{root_resolved}#{File::SEPARATOR}") raise ArgumentError, 'dump_dir must exist inside dumps_root. ' \ "Got: #{dump_dir.inspect}, dumps_root: #{dumps_root}" end atomic_write(latest_pointer_path, target.basename.to_s) end |
#read_config ⇒ Hash?
Reads and parses woods.json, returning the raw hash.
Returns nil when the file does not exist. Schema-version validation
is the caller's responsibility (typically ResolvedConfig.from_hash).
91 92 93 94 95 |
# File 'lib/woods/index_artifact.rb', line 91 def read_config return nil unless config_path.exist? JSON.parse(config_path.read) end |
#write_config(resolved_config_hash) ⇒ void
This method returns an undefined value.
Atomically writes a resolved config hash as woods.json.
Accepts either a ResolvedConfig (responds to #to_snapshot_json) or
a plain Hash. When #to_snapshot_json returns a Hash, it is
serialized to JSON automatically — callers need not pre-serialize.
151 152 153 154 155 156 157 158 159 |
# File 'lib/woods/index_artifact.rb', line 151 def write_config(resolved_config_hash) raw = if resolved_config_hash.respond_to?(:to_snapshot_json) resolved_config_hash.to_snapshot_json else resolved_config_hash end json = raw.is_a?(String) ? raw : JSON.pretty_generate(raw) atomic_write(config_path, json) end |