textus 0.54.2 → 0.55.1
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 +37 -0
- data/README.md +8 -1
- data/SPEC.md +27 -0
- data/docs/architecture/README.md +20 -8
- data/docs/reference/conventions.md +1 -1
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +23 -21
- data/lib/textus/action/audit.rb +24 -61
- data/lib/textus/action/base.rb +9 -9
- data/lib/textus/action/blame.rb +18 -36
- data/lib/textus/action/boot.rb +2 -4
- data/lib/textus/action/data_mv.rb +20 -31
- data/lib/textus/action/deps.rb +3 -18
- data/lib/textus/action/doctor.rb +2 -9
- data/lib/textus/action/drain.rb +11 -19
- data/lib/textus/action/enqueue.rb +14 -30
- data/lib/textus/action/get.rb +12 -56
- data/lib/textus/action/ingest.rb +74 -78
- data/lib/textus/action/jobs.rb +6 -15
- data/lib/textus/action/key_delete.rb +6 -16
- data/lib/textus/action/key_delete_prefix.rb +8 -17
- data/lib/textus/action/key_mv.rb +54 -61
- data/lib/textus/action/key_mv_prefix.rb +13 -22
- data/lib/textus/action/list.rb +7 -21
- data/lib/textus/action/propose.rb +16 -26
- data/lib/textus/action/published.rb +3 -5
- data/lib/textus/action/pulse.rb +19 -26
- data/lib/textus/action/put.rb +15 -29
- data/lib/textus/action/rdeps.rb +3 -18
- data/lib/textus/action/reject.rb +12 -21
- data/lib/textus/action/rule_explain.rb +12 -22
- data/lib/textus/action/rule_lint.rb +10 -16
- data/lib/textus/action/rule_list.rb +5 -9
- data/lib/textus/action/schema_envelope.rb +3 -10
- data/lib/textus/action/uid.rb +3 -17
- data/lib/textus/action/where.rb +3 -18
- data/lib/textus/boot.rb +7 -15
- data/lib/textus/contract/arg.rb +10 -0
- data/lib/textus/contract/dsl.rb +88 -0
- data/lib/textus/contract/spec.rb +25 -0
- data/lib/textus/contract.rb +0 -162
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +4 -7
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +6 -0
- data/lib/textus/format/base.rb +0 -4
- data/lib/textus/format/json.rb +5 -6
- data/lib/textus/format/markdown.rb +5 -6
- data/lib/textus/format/shared.rb +17 -0
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -6
- data/lib/textus/format.rb +6 -0
- data/lib/textus/gate/auth.rb +2 -17
- data/lib/textus/gate/binder.rb +50 -0
- data/lib/textus/gate.rb +64 -88
- data/lib/textus/init.rb +2 -4
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +3 -3
- data/lib/textus/manifest/entry/base.rb +1 -1
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/meta.rb +54 -0
- data/lib/textus/{ports → port}/audit_log.rb +44 -4
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{ports → port}/sentinel_store.rb +3 -3
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/{ports → port}/storage/file_store.rb +2 -2
- data/lib/textus/port/store.rb +93 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -1
- data/lib/textus/schema/tools.rb +11 -7
- data/lib/textus/store/compositor.rb +34 -0
- data/lib/textus/store/container.rb +43 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/envelope/reader.rb +43 -0
- data/lib/textus/store/envelope/writer.rb +195 -0
- data/lib/textus/store/geometry.rb +81 -0
- data/lib/textus/store/index/builder.rb +74 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +104 -0
- data/lib/textus/store/jobs/queue.rb +154 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention.rb +50 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/session.rb +37 -0
- data/lib/textus/store.rb +21 -13
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +10 -63
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +4 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +4 -4
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/{surfaces → surface}/cli/verb/put.rb +6 -11
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/catalog.rb +9 -26
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/server.rb +5 -5
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/surface/projector.rb +27 -0
- data/lib/textus/{surfaces → surface}/role_scope.rb +1 -1
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/value/envelope.rb +89 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/result.rb +26 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +4 -18
- data/lib/textus.rb +9 -10
- metadata +100 -63
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/role.rb +0 -36
- data/lib/textus/session.rb +0 -35
- data/lib/textus/types.rb +0 -15
|
@@ -3,7 +3,7 @@ require "json"
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module Textus
|
|
6
|
-
module
|
|
6
|
+
module Port
|
|
7
7
|
# Append-only audit log adapter: writes and rotates the on-disk audit JSONL
|
|
8
8
|
# under the store root. An instantiable class — it holds collaborators (the
|
|
9
9
|
# root path + size/keep config), so each store binds its own instance. It
|
|
@@ -16,7 +16,7 @@ module Textus
|
|
|
16
16
|
|
|
17
17
|
def initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP)
|
|
18
18
|
@root = root
|
|
19
|
-
@path = Textus::
|
|
19
|
+
@path = Textus::Store::Geometry.new(root).audit_log_path
|
|
20
20
|
@max_size = max_size
|
|
21
21
|
@keep = keep
|
|
22
22
|
end
|
|
@@ -72,6 +72,28 @@ module Textus
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Scan log files with optional filters. Returns parsed row hashes.
|
|
76
|
+
# Lane and timestamp filters are left to the caller (they need manifest
|
|
77
|
+
# resolution and Time parsing the port shouldn't know about).
|
|
78
|
+
def scan(seq_since: nil, key: nil, role: nil, verb: nil,
|
|
79
|
+
correlation_id: nil, limit: nil)
|
|
80
|
+
files = all_log_files
|
|
81
|
+
return [] if files.empty?
|
|
82
|
+
|
|
83
|
+
rows = []
|
|
84
|
+
files.each do |file|
|
|
85
|
+
File.foreach(file) do |line|
|
|
86
|
+
parsed = parse_row(line.chomp)
|
|
87
|
+
next unless parsed && matches?(parsed, seq_since:, key:, role:, verb:, correlation_id:)
|
|
88
|
+
|
|
89
|
+
rows << parsed
|
|
90
|
+
break if limit && rows.length >= limit
|
|
91
|
+
end
|
|
92
|
+
break if limit && rows.length >= limit
|
|
93
|
+
end
|
|
94
|
+
rows
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
# Returns an array of integrity-violation descriptors for the on-disk log.
|
|
76
98
|
# Each entry is { "lineno" => Integer, "reason" => String, "detail" => String }.
|
|
77
99
|
# Empty array means the log is well-formed (or doesn't exist yet).
|
|
@@ -115,11 +137,11 @@ module Textus
|
|
|
115
137
|
end
|
|
116
138
|
|
|
117
139
|
def rotated(n)
|
|
118
|
-
File.join(Textus::
|
|
140
|
+
File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.#{n}")
|
|
119
141
|
end
|
|
120
142
|
|
|
121
143
|
def rotated_meta(n)
|
|
122
|
-
File.join(Textus::
|
|
144
|
+
File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.#{n}.meta.json")
|
|
123
145
|
end
|
|
124
146
|
|
|
125
147
|
# Caller holds the flock. Returns the highest seq across the active log,
|
|
@@ -215,6 +237,24 @@ module Textus
|
|
|
215
237
|
nil
|
|
216
238
|
end
|
|
217
239
|
|
|
240
|
+
def matches?(row, seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil)
|
|
241
|
+
return false if seq_since && row["seq"] <= seq_since
|
|
242
|
+
return false if key && row["key"] != key
|
|
243
|
+
return false if role && row["role"] != role
|
|
244
|
+
return false if verb && row["verb"] != verb
|
|
245
|
+
return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
|
|
246
|
+
|
|
247
|
+
true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def all_log_files
|
|
251
|
+
rotated = Dir.glob(File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.*"))
|
|
252
|
+
.reject { |path| path.end_with?(".meta.json") }
|
|
253
|
+
.sort_by { |path| -path.scan(/\d+$/).first.to_i }
|
|
254
|
+
active_log = File.exist?(@path) ? [@path] : []
|
|
255
|
+
rotated + active_log
|
|
256
|
+
end
|
|
257
|
+
|
|
218
258
|
def check_line(stripped, lineno)
|
|
219
259
|
return nil if stripped.empty?
|
|
220
260
|
|
|
@@ -3,7 +3,7 @@ require "socket"
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module Textus
|
|
6
|
-
module
|
|
6
|
+
module Port
|
|
7
7
|
# Cross-process build lock: a pid/host-stamped lockfile under the store root
|
|
8
8
|
# that serializes converge's produce/sweep. An instantiable class — it holds
|
|
9
9
|
# the root and lock state; `self.with(root:)` is a convenience that constructs
|
|
@@ -18,7 +18,7 @@ module Textus
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def initialize(root:)
|
|
21
|
-
@path = Textus::
|
|
21
|
+
@path = Textus::Store::Geometry.new(root).lock_path("build")
|
|
22
22
|
@file = nil
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Port
|
|
5
5
|
# Publishes built artifacts from the store to repo-relative consumer paths.
|
|
6
6
|
# Publish = copy + sentinel. The in-store file is already the consumer-shaped
|
|
7
7
|
# artifact; no parsing or stripping.
|
|
8
8
|
#
|
|
9
|
-
# Sentinel I/O is delegated to Textus::
|
|
9
|
+
# Sentinel I/O is delegated to Textus::Port::SentinelStore. Sentinels live
|
|
10
10
|
# under `<store_root>/.run/sentinels/` (runtime, git-ignored — ADR 0070) and
|
|
11
11
|
# mirror the target's repo-relative layout so consumer directories aren't
|
|
12
12
|
# polluted with `.textus-managed.json` siblings.
|
|
@@ -18,7 +18,7 @@ module Textus
|
|
|
18
18
|
guard_clobber(source, target, store_root)
|
|
19
19
|
File.delete(target) if File.symlink?(target)
|
|
20
20
|
FileUtils.cp(source, target)
|
|
21
|
-
Textus::
|
|
21
|
+
Textus::Port::SentinelStore.new.write!(target: target, source: provenance_source, store_root: store_root)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Removes a previously-published file and its sentinel. No-op unless the
|
|
@@ -27,7 +27,7 @@ module Textus
|
|
|
27
27
|
return unless managed?(target, store_root)
|
|
28
28
|
|
|
29
29
|
FileUtils.rm_f(target)
|
|
30
|
-
sentinel = Textus::
|
|
30
|
+
sentinel = Textus::Port::SentinelStore.new.sentinel_path(target, store_root)
|
|
31
31
|
FileUtils.rm_f(sentinel)
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -53,7 +53,7 @@ module Textus
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def managed?(target, store_root)
|
|
56
|
-
File.exist?(Textus::
|
|
56
|
+
File.exist?(Textus::Port::SentinelStore.new.sentinel_path(target, store_root))
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -3,7 +3,7 @@ require "digest"
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module Textus
|
|
6
|
-
module
|
|
6
|
+
module Port
|
|
7
7
|
# Persistence adapter for sentinel files. Owns the on-disk JSON shape, the
|
|
8
8
|
# path layout (<store_root>/.run/sentinels/<target-rel-to-repo>.textus-managed.json
|
|
9
9
|
# — runtime, git-ignored, ADR 0070), and all File/FileUtils I/O.
|
|
@@ -39,14 +39,14 @@ module Textus
|
|
|
39
39
|
def sentinel_path(target, store_root)
|
|
40
40
|
repo_root = File.dirname(store_root)
|
|
41
41
|
rel = relative_to(target, repo_root) || File.basename(target)
|
|
42
|
-
File.join(Textus::
|
|
42
|
+
File.join(Textus::Store::Geometry.new(store_root).sentinels_root, rel + SUFFIX)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Absolute target paths of every sentinel recorded under `target_dir`.
|
|
46
46
|
def targets_under(target_dir, store_root)
|
|
47
47
|
repo_root = File.dirname(store_root)
|
|
48
48
|
rel = relative_to(target_dir, repo_root) or return []
|
|
49
|
-
root = Textus::
|
|
49
|
+
root = Textus::Store::Geometry.new(store_root).sentinels_root
|
|
50
50
|
sdir = File.join(root, rel)
|
|
51
51
|
return [] unless File.directory?(sdir)
|
|
52
52
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Port
|
|
5
5
|
module Storage
|
|
6
6
|
# Pure filesystem I/O port. Wraps File/FileUtils/Etag with no knowledge
|
|
7
7
|
# of envelopes, entries, schemas, or audit.
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
|
|
20
20
|
def exists?(path) = File.exist?(path)
|
|
21
21
|
|
|
22
|
-
def etag(path) = Etag.for_file(path)
|
|
22
|
+
def etag(path) = Value::Etag.for_file(path)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "sqlite3"
|
|
5
|
+
|
|
6
|
+
module Textus
|
|
7
|
+
module Port
|
|
8
|
+
# SQLite-backed runtime store for textus state. Owns the connection,
|
|
9
|
+
# schema setup, WAL mode, and transaction boundary for the index and queue.
|
|
10
|
+
class Store
|
|
11
|
+
attr_reader :path, :connection
|
|
12
|
+
|
|
13
|
+
def initialize(root:)
|
|
14
|
+
@root = root
|
|
15
|
+
@path = Textus::Store::Geometry.new(root).store_db_path
|
|
16
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
17
|
+
@connection = SQLite3::Database.new(@path)
|
|
18
|
+
@connection.results_as_hash = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute(sql, params = [])
|
|
22
|
+
@connection.execute(sql, params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def query_value(sql, params = [])
|
|
26
|
+
@connection.get_first_value(sql, params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def setup!
|
|
30
|
+
execute("PRAGMA journal_mode=WAL")
|
|
31
|
+
execute("PRAGMA foreign_keys=ON")
|
|
32
|
+
connection.execute_batch(<<~SQL)
|
|
33
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
34
|
+
key TEXT PRIMARY KEY,
|
|
35
|
+
lane TEXT NOT NULL,
|
|
36
|
+
format TEXT NOT NULL,
|
|
37
|
+
etag TEXT,
|
|
38
|
+
content TEXT,
|
|
39
|
+
extra TEXT,
|
|
40
|
+
indexed_at TEXT NOT NULL
|
|
41
|
+
) STRICT;
|
|
42
|
+
|
|
43
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
|
44
|
+
key, lane, content,
|
|
45
|
+
content=entries, content_rowid=rowid
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
type TEXT NOT NULL,
|
|
51
|
+
args TEXT NOT NULL,
|
|
52
|
+
state TEXT NOT NULL DEFAULT 'ready',
|
|
53
|
+
role TEXT NOT NULL,
|
|
54
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
56
|
+
errors TEXT,
|
|
57
|
+
lease TEXT,
|
|
58
|
+
created_at TEXT NOT NULL,
|
|
59
|
+
updated_at TEXT NOT NULL
|
|
60
|
+
) STRICT;
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_entries_lane ON entries(lane);
|
|
64
|
+
SQL
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def transaction
|
|
69
|
+
connection.transaction
|
|
70
|
+
yield
|
|
71
|
+
connection.commit
|
|
72
|
+
rescue StandardError
|
|
73
|
+
connection.rollback if connection.transaction_active?
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def close
|
|
78
|
+
connection.close unless connection.closed?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.open(root)
|
|
82
|
+
store = new(root: root)
|
|
83
|
+
store.setup!
|
|
84
|
+
return store unless block_given?
|
|
85
|
+
|
|
86
|
+
yield store
|
|
87
|
+
ensure
|
|
88
|
+
store&.close
|
|
89
|
+
end
|
|
90
|
+
private :connection
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
require "socket"
|
|
4
4
|
|
|
5
5
|
module Textus
|
|
6
|
-
module
|
|
6
|
+
module Port
|
|
7
7
|
# Flock-based watcher presence lock. Held for the watcher's lifetime.
|
|
8
8
|
# Process death releases the flock automatically.
|
|
9
9
|
class WatcherLock
|
|
10
10
|
def initialize(root)
|
|
11
|
-
@path = Textus::
|
|
11
|
+
@path = Textus::Store::Geometry.new(root).lock_path("watcher")
|
|
12
12
|
@file = nil
|
|
13
13
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.running?(root)
|
|
17
|
-
path = Textus::
|
|
17
|
+
path = Textus::Store::Geometry.new(root).lock_path("watcher")
|
|
18
18
|
return false unless File.exist?(path)
|
|
19
19
|
|
|
20
20
|
File.open(path, "r+") do |file|
|
|
@@ -36,7 +36,7 @@ module Textus
|
|
|
36
36
|
entry = @container.manifest.resolver.resolve(key).entry
|
|
37
37
|
return unless entry.publish_tree || !Array(entry.publish_to).empty?
|
|
38
38
|
|
|
39
|
-
reader = Textus::Envelope::Reader.from(container: @container)
|
|
39
|
+
reader = Textus::Store::Envelope::Reader.from(container: @container)
|
|
40
40
|
entry_path = @container.manifest.resolver.resolve(key).path
|
|
41
41
|
return unless entry.publish_tree || File.exist?(entry_path)
|
|
42
42
|
|
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
module Tools
|
|
7
7
|
# textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
8
8
|
def self.init(store, name:, from:)
|
|
9
|
-
env = pure_get(store, Textus::Role::DEFAULT, from)
|
|
9
|
+
env = pure_get(store, Textus::Value::Role::DEFAULT, from)
|
|
10
10
|
meta = env.meta
|
|
11
11
|
schema = {
|
|
12
12
|
"name" => name,
|
|
@@ -14,8 +14,9 @@ module Textus
|
|
|
14
14
|
"optional" => [],
|
|
15
15
|
"fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
geom = Textus::Store::Geometry.new(store.root)
|
|
18
|
+
FileUtils.mkdir_p(geom.schemas_dir)
|
|
19
|
+
target = geom.schema_path(name)
|
|
19
20
|
File.write(target, YAML.dump(schema))
|
|
20
21
|
{ "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
|
|
21
22
|
end
|
|
@@ -25,7 +26,7 @@ module Textus
|
|
|
25
26
|
schema = load_schema(store, name)
|
|
26
27
|
drift = []
|
|
27
28
|
store.manifest.resolver.enumerate.each do |row|
|
|
28
|
-
env = pure_get(store, Textus::Role::DEFAULT, row[:key])
|
|
29
|
+
env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
|
|
29
30
|
begin
|
|
30
31
|
schema.validate!(env.meta)
|
|
31
32
|
rescue SchemaViolation => e
|
|
@@ -85,9 +86,12 @@ module Textus
|
|
|
85
86
|
# while inspecting/migrating entries (ADR 0062).
|
|
86
87
|
def self.pure_get(store, role, key)
|
|
87
88
|
scope = store.as(role)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
Value::Result.unwrap(
|
|
90
|
+
Textus::Action::Get.call(
|
|
91
|
+
container: scope.container,
|
|
92
|
+
call: Textus::Value::Call.build(role: role),
|
|
93
|
+
key: key,
|
|
94
|
+
),
|
|
91
95
|
)
|
|
92
96
|
end
|
|
93
97
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
class Compositor
|
|
6
|
+
def initialize(container)
|
|
7
|
+
@container = container
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(key, mentry:, payload:, call:, if_etag: nil)
|
|
11
|
+
Textus::Store::Envelope::Writer.from(container: @container, call: call)
|
|
12
|
+
.put(key, mentry: mentry, payload: payload, if_etag: if_etag)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(key)
|
|
16
|
+
Textus::Store::Envelope::Reader.from(container: @container).read(key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def delete(key, call:, mentry: nil, if_etag: nil)
|
|
20
|
+
Textus::Store::Envelope::Writer.from(container: @container, call: call)
|
|
21
|
+
.delete(key, mentry: mentry, if_etag: if_etag)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def move(from_key:, to_key:, new_mentry:, call:, if_etag: nil)
|
|
25
|
+
Textus::Store::Envelope::Writer.from(container: @container, call: call)
|
|
26
|
+
.move(from_key: from_key, to_key: to_key, new_mentry: new_mentry, if_etag: if_etag)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def exists?(key)
|
|
30
|
+
Textus::Store::Envelope::Reader.from(container: @container).exists?(key)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
class Container
|
|
6
|
+
Infrastructure = Data.define(:file_store, :schemas, :audit_log, :job_store, :geometry)
|
|
7
|
+
Coordination = Data.define(:manifest, :workflows, :gate, :compositor)
|
|
8
|
+
|
|
9
|
+
def self.attribute_names
|
|
10
|
+
@attribute_names ||= [:root] + Infrastructure.members + Coordination.members
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(infra, coord)
|
|
14
|
+
@infra = infra
|
|
15
|
+
@coord = coord
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :infra, :coord
|
|
19
|
+
|
|
20
|
+
def root
|
|
21
|
+
@infra.geometry.root
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Infrastructure.members.each do |name|
|
|
25
|
+
define_method(name) { @infra.public_send(name) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Coordination.members.each do |name|
|
|
29
|
+
define_method(name) { @coord.public_send(name) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def wire_gate!(gate, compositor)
|
|
33
|
+
@coord = Coordination.new(
|
|
34
|
+
manifest: @coord.manifest,
|
|
35
|
+
workflows: @coord.workflows,
|
|
36
|
+
gate:,
|
|
37
|
+
compositor:,
|
|
38
|
+
)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
# Per-role cursor cache under <root>/.state/cursors/<role>. A convenience so
|
|
6
|
+
# `textus pulse` (no --since) means "since I last looked". Gitignored;
|
|
7
|
+
# losing it just re-emits recent deltas, never corrupts the store. ADR 0036/0038.
|
|
8
|
+
class Cursor
|
|
9
|
+
def initialize(root:, role:)
|
|
10
|
+
@path = Store::Geometry.new(root).cursor_path(role)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read
|
|
14
|
+
Integer(File.read(@path).strip)
|
|
15
|
+
rescue Errno::ENOENT, ArgumentError
|
|
16
|
+
0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(seq)
|
|
20
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
21
|
+
File.write(@path, seq.to_s)
|
|
22
|
+
seq
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Store
|
|
3
|
+
module Envelope
|
|
4
|
+
# Read-only counterpart to EnvelopeWriter. Resolves a key, reads the
|
|
5
|
+
# bytes, parses them via the format strategy, and hands back an
|
|
6
|
+
# Envelope. Used by Mv (pre-move inspection) and by EnvelopeWriter
|
|
7
|
+
# (existing-meta lookup for the uid/sources preservation step in #put).
|
|
8
|
+
#
|
|
9
|
+
# No audit, no events, no permission checks — those live one layer up.
|
|
10
|
+
class Reader
|
|
11
|
+
def self.from(container:)
|
|
12
|
+
new(file_store: container.file_store, manifest: container.manifest,
|
|
13
|
+
geometry: container.geometry)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(file_store:, manifest:, geometry:)
|
|
17
|
+
@file_store = file_store
|
|
18
|
+
@manifest = manifest
|
|
19
|
+
@geometry = geometry
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(key)
|
|
23
|
+
res = @manifest.resolver.resolve(key)
|
|
24
|
+
path = res.path
|
|
25
|
+
return nil unless @file_store.exists?(path)
|
|
26
|
+
|
|
27
|
+
mentry = res.entry
|
|
28
|
+
raw = @file_store.read(path)
|
|
29
|
+
parsed = Format.for(mentry.format).parse(raw, path: path)
|
|
30
|
+
Textus::Value::Envelope.build(
|
|
31
|
+
key: key, mentry: mentry, path: path,
|
|
32
|
+
meta: parsed["_meta"], body: parsed["body"],
|
|
33
|
+
etag: Value::Etag.for_bytes(raw), content: parsed["content"]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def exists?(key)
|
|
38
|
+
@file_store.exists?(@manifest.resolver.resolve(key).path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|