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; the Bootstrapper uses this to decide whether to raise MCP::MissingArtifact or proceed with loading.
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 |
# 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 unless target.exist? && target_real.start_with?(root_resolved) 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.
147 148 149 150 151 152 153 154 155 |
# File 'lib/woods/index_artifact.rb', line 147 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 |