textus 0.10.5 → 0.14.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +318 -3
  4. data/README.md +34 -27
  5. data/SPEC.md +226 -145
  6. data/docs/conventions.md +8 -8
  7. data/lib/textus/application/context.rb +4 -0
  8. data/lib/textus/application/reads/blame.rb +1 -1
  9. data/lib/textus/application/reads/deps.rb +15 -0
  10. data/lib/textus/application/reads/freshness.rb +4 -4
  11. data/lib/textus/application/reads/get.rb +9 -12
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/policy_explain.rb +2 -2
  14. data/lib/textus/application/reads/published.rb +15 -0
  15. data/lib/textus/application/reads/rdeps.rb +15 -0
  16. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  17. data/lib/textus/application/reads/stale.rb +15 -0
  18. data/lib/textus/application/reads/uid.rb +15 -0
  19. data/lib/textus/application/reads/validate_all.rb +15 -0
  20. data/lib/textus/application/reads/where.rb +15 -0
  21. data/lib/textus/application/refresh/all.rb +2 -2
  22. data/lib/textus/application/refresh/orchestrator.rb +1 -1
  23. data/lib/textus/application/refresh/worker.rb +8 -8
  24. data/lib/textus/application/writes/accept.rb +26 -8
  25. data/lib/textus/application/writes/build.rb +12 -49
  26. data/lib/textus/application/writes/delete.rb +1 -1
  27. data/lib/textus/application/writes/mv.rb +144 -0
  28. data/lib/textus/application/writes/publish.rb +42 -10
  29. data/lib/textus/application/writes/put.rb +1 -1
  30. data/lib/textus/application/writes/reject.rb +37 -0
  31. data/lib/textus/builder/pipeline.rb +1 -1
  32. data/lib/textus/builder/renderer/json.rb +1 -1
  33. data/lib/textus/builder/renderer/yaml.rb +1 -1
  34. data/lib/textus/cli/group/key.rb +1 -1
  35. data/lib/textus/cli/group/refresh.rb +21 -0
  36. data/lib/textus/cli/group/rule.rb +11 -0
  37. data/lib/textus/cli/verb/accept.rb +1 -2
  38. data/lib/textus/cli/verb/audit.rb +3 -3
  39. data/lib/textus/cli/verb/blame.rb +1 -2
  40. data/lib/textus/cli/verb/build.rb +6 -2
  41. data/lib/textus/cli/verb/delete.rb +1 -2
  42. data/lib/textus/cli/verb/deps.rb +1 -1
  43. data/lib/textus/cli/verb/freshness.rb +1 -2
  44. data/lib/textus/cli/verb/get.rb +2 -3
  45. data/lib/textus/cli/verb/hook_run.rb +3 -2
  46. data/lib/textus/cli/verb/hooks.rb +1 -1
  47. data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
  48. data/lib/textus/cli/verb/list.rb +1 -1
  49. data/lib/textus/cli/verb/mv.rb +1 -1
  50. data/lib/textus/cli/verb/published.rb +1 -1
  51. data/lib/textus/cli/verb/put.rb +3 -3
  52. data/lib/textus/cli/verb/rdeps.rb +1 -1
  53. data/lib/textus/cli/verb/refresh.rb +1 -2
  54. data/lib/textus/cli/verb/reject.rb +1 -1
  55. data/lib/textus/cli/verb/{policy_explain.rb → rule_explain.rb} +2 -3
  56. data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
  57. data/lib/textus/cli/verb/schema.rb +1 -1
  58. data/lib/textus/cli/verb/uid.rb +1 -1
  59. data/lib/textus/cli/verb/where.rb +1 -1
  60. data/lib/textus/cli/verb.rb +9 -3
  61. data/lib/textus/cli.rb +6 -6
  62. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  63. data/lib/textus/doctor/check/illegal_keys.rb +39 -16
  64. data/lib/textus/doctor/check/intake_registration.rb +4 -4
  65. data/lib/textus/doctor/check/protocol_version.rb +47 -0
  66. data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
  67. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  68. data/lib/textus/doctor.rb +6 -5
  69. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  70. data/lib/textus/domain/permission.rb +4 -4
  71. data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
  72. data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
  73. data/lib/textus/domain/policy/promotion.rb +45 -0
  74. data/lib/textus/entry/base.rb +28 -0
  75. data/lib/textus/entry/json.rb +59 -0
  76. data/lib/textus/entry/markdown.rb +46 -0
  77. data/lib/textus/entry/text.rb +35 -0
  78. data/lib/textus/entry/yaml.rb +59 -0
  79. data/lib/textus/entry.rb +16 -0
  80. data/lib/textus/envelope.rb +44 -14
  81. data/lib/textus/errors.rb +24 -5
  82. data/lib/textus/hooks/builtin.rb +5 -5
  83. data/lib/textus/hooks/dispatcher.rb +1 -1
  84. data/lib/textus/hooks/dsl.rb +3 -10
  85. data/lib/textus/hooks/loader.rb +1 -2
  86. data/lib/textus/hooks/registry.rb +22 -21
  87. data/lib/textus/infra/refresh/detached.rb +1 -1
  88. data/lib/textus/init.rb +25 -34
  89. data/lib/textus/intro.rb +65 -9
  90. data/lib/textus/manifest/entry/parser.rb +84 -0
  91. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  92. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  93. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  94. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  95. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  96. data/lib/textus/manifest/entry/validators.rb +20 -0
  97. data/lib/textus/manifest/entry.rb +38 -189
  98. data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
  99. data/lib/textus/manifest/schema.rb +49 -0
  100. data/lib/textus/manifest.rb +50 -24
  101. data/lib/textus/migrate_keys.rb +1 -1
  102. data/lib/textus/operations/reads.rb +39 -0
  103. data/lib/textus/operations/refresh.rb +27 -0
  104. data/lib/textus/operations/writes.rb +21 -0
  105. data/lib/textus/operations.rb +44 -0
  106. data/lib/textus/projection.rb +9 -8
  107. data/lib/textus/refresh.rb +4 -5
  108. data/lib/textus/schema/tools.rb +8 -7
  109. data/lib/textus/store/reader.rb +1 -1
  110. data/lib/textus/store/staleness/intake_check.rb +1 -1
  111. data/lib/textus/store/validator.rb +3 -3
  112. data/lib/textus/store/writer.rb +5 -74
  113. data/lib/textus/store.rb +1 -55
  114. data/lib/textus/version.rb +2 -2
  115. data/lib/textus.rb +1 -0
  116. metadata +35 -10
  117. data/lib/textus/cli/group/policy.rb +0 -11
  118. data/lib/textus/composition.rb +0 -72
  119. data/lib/textus/proposal.rb +0 -10
  120. data/lib/textus/store/mover.rb +0 -167
@@ -1,19 +1,28 @@
1
1
  require "yaml"
2
+ require_relative "manifest/schema"
2
3
 
3
4
  module Textus
4
5
  class Manifest
5
- EXT_TO_FORMAT = {
6
- ".md" => "markdown",
7
- ".json" => "json",
8
- ".yaml" => "yaml",
9
- ".yml" => "yaml",
10
- ".txt" => "text",
11
- }.freeze
6
+ TEXTUS_2_HINT = "Install textus 0.11.x to run the migrator, then upgrade to this version. " \
7
+ "See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze
8
+
9
+ def self.version_hint_for(version)
10
+ version == "textus/2" ? TEXTUS_2_HINT : nil
11
+ end
12
+
13
+ private_class_method :version_hint_for
12
14
 
13
15
  attr_reader :root, :entries, :raw
14
16
 
15
17
  def zones
16
- @zones ||= Array(@raw["zones"]).to_h { |z| [z["name"], Array(z["writable_by"])] }
18
+ @zones ||= Array(@raw["zones"]).to_h { |z| [z["name"], Array(z["write_policy"])] }
19
+ end
20
+
21
+ def zone_readers
22
+ @zone_readers ||= Array(@raw["zones"]).to_h do |z|
23
+ rp = z["read_policy"]
24
+ [z["name"], rp.nil? ? :all : Array(rp)]
25
+ end
17
26
  end
18
27
 
19
28
  def zone_writers(zone_name)
@@ -23,18 +32,35 @@ module Textus
23
32
  def permission_for(zone_name)
24
33
  Textus::Domain::Permission.new(
25
34
  zone: zone_name,
26
- writable_by: zone_writers(zone_name),
27
- readable_by: :all,
35
+ write_policy: zone_writers(zone_name),
36
+ read_policy: zone_readers[zone_name] || :all,
28
37
  )
29
38
  end
30
39
 
40
+ def self.parse(yaml_text, root: ".")
41
+ raw = YAML.safe_load(yaml_text, aliases: false)
42
+ unless raw["version"] == PROTOCOL
43
+ raise BadFrontmatter.new(
44
+ "<string>",
45
+ "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
46
+ hint: version_hint_for(raw["version"]),
47
+ )
48
+ end
49
+
50
+ new(root, raw)
51
+ end
52
+
31
53
  def self.load(root)
32
54
  manifest_path = File.join(root, "manifest.yaml")
33
55
  raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
34
56
 
35
57
  raw = YAML.safe_load_file(manifest_path, aliases: false)
36
58
  unless raw["version"] == PROTOCOL
37
- raise BadFrontmatter.new(manifest_path, "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}")
59
+ raise BadFrontmatter.new(
60
+ manifest_path,
61
+ "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
62
+ hint: version_hint_for(raw["version"]),
63
+ )
38
64
  end
39
65
 
40
66
  new(root, raw)
@@ -45,16 +71,22 @@ module Textus
45
71
  @raw = raw
46
72
  raise BadFrontmatter.new(File.join(root, "manifest.yaml"), "manifest must declare zones:") if Array(raw["zones"]).empty?
47
73
 
48
- @entries = Array(raw["entries"]).map { |e| Manifest::Entry.new(self, e) }
74
+ Schema.validate!(raw)
75
+
76
+ @entries = Array(raw["entries"]).map do |e|
77
+ entry = Manifest::Entry::Parser.call(self, e)
78
+ Manifest::Entry::Validators.run_all(entry)
79
+ entry
80
+ end
49
81
  validate_declared_keys!
50
82
  end
51
83
 
52
- def policies
53
- @policies ||= Textus::Manifest::Policies.parse(@raw["policies"] || [])
84
+ def rules
85
+ @rules ||= Textus::Manifest::Rules.parse(@raw["rules"] || [])
54
86
  end
55
87
 
56
- def policies_for(key)
57
- policies.for(key)
88
+ def rules_for(key)
89
+ rules.for(key)
58
90
  end
59
91
 
60
92
  # Returns [Manifest::Entry, resolved_path, remaining_segments]
@@ -135,7 +167,7 @@ module Textus
135
167
 
136
168
  illegal = segs.find { |s| !valid_segment?(s) }
137
169
  if illegal
138
- warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key migrate --dry-run'")
170
+ warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key normalize --dry-run'")
139
171
  return nil
140
172
  end
141
173
 
@@ -158,13 +190,7 @@ module Textus
158
190
  end
159
191
 
160
192
  def nested_glob(format)
161
- case format
162
- when "markdown" then "**/*.md"
163
- when "json" then "**/*.json"
164
- when "yaml" then "**/*.{yaml,yml}"
165
- when "text" then "**/*.txt"
166
- else raise UsageError.new("unknown format #{format.inspect} for nested glob")
167
- end
193
+ Textus::Entry.for_format(format).nested_glob
168
194
  end
169
195
  end
170
196
  end
@@ -122,7 +122,7 @@ module Textus
122
122
  File.rename(from, to)
123
123
  new_key = compute_new_key(r, renames)
124
124
  audit.append(
125
- role: "script",
125
+ role: "runner",
126
126
  verb: "migrate-keys",
127
127
  key: new_key,
128
128
  etag_before: nil,
@@ -0,0 +1,39 @@
1
+ module Textus
2
+ class Operations
3
+ class Reads
4
+ def initialize(ctx)
5
+ @ctx = ctx
6
+ end
7
+
8
+ def get
9
+ Application::Reads::Get.new(ctx: @ctx, orchestrator: orchestrator)
10
+ end
11
+
12
+ def freshness = Application::Reads::Freshness.new(ctx: @ctx)
13
+ def audit = Application::Reads::Audit.new(ctx: @ctx)
14
+ def blame = Application::Reads::Blame.new(ctx: @ctx)
15
+ def policy_explain = Application::Reads::PolicyExplain.new(ctx: @ctx)
16
+ def list = Application::Reads::List.new(ctx: @ctx)
17
+ def where = Application::Reads::Where.new(ctx: @ctx)
18
+ def uid = Application::Reads::Uid.new(ctx: @ctx)
19
+ def schema_envelope = Application::Reads::SchemaEnvelope.new(ctx: @ctx)
20
+ def deps = Application::Reads::Deps.new(ctx: @ctx)
21
+ def rdeps = Application::Reads::Rdeps.new(ctx: @ctx)
22
+ def published = Application::Reads::Published.new(ctx: @ctx)
23
+ def stale = Application::Reads::Stale.new(ctx: @ctx)
24
+ def validate_all = Application::Reads::ValidateAll.new(ctx: @ctx)
25
+
26
+ private
27
+
28
+ def orchestrator
29
+ Application::Refresh::Orchestrator.new(
30
+ worker: Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus),
31
+ bus: @ctx.store.bus,
32
+ store_root: @ctx.store.root,
33
+ store: @ctx.store,
34
+ role: @ctx.role,
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ module Textus
2
+ class Operations
3
+ class Refresh
4
+ def initialize(ctx)
5
+ @ctx = ctx
6
+ end
7
+
8
+ def worker
9
+ Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus)
10
+ end
11
+
12
+ def orchestrator
13
+ Application::Refresh::Orchestrator.new(
14
+ worker: worker,
15
+ bus: @ctx.store.bus,
16
+ store_root: @ctx.store.root,
17
+ store: @ctx.store,
18
+ role: @ctx.role,
19
+ )
20
+ end
21
+
22
+ def all
23
+ Application::Refresh::All.new(ctx: @ctx, bus: @ctx.store.bus)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ class Operations
3
+ class Writes
4
+ def initialize(ctx)
5
+ @ctx = ctx
6
+ end
7
+
8
+ def put = Application::Writes::Put.new(ctx: @ctx, bus: bus)
9
+ def delete = Application::Writes::Delete.new(ctx: @ctx, bus: bus)
10
+ def mv = Application::Writes::Mv.new(ctx: @ctx, bus: bus)
11
+ def accept = Application::Writes::Accept.new(ctx: @ctx, bus: bus)
12
+ def build = Application::Writes::Build.new(ctx: @ctx, bus: bus)
13
+ def publish = Application::Writes::Publish.new(ctx: @ctx, bus: bus)
14
+ def reject = Application::Writes::Reject.new(ctx: @ctx, bus: bus)
15
+
16
+ private
17
+
18
+ def bus = @ctx.store.bus
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ module Textus
2
+ # Single canonical entrypoint for invoking application use-cases against a
3
+ # store. Mirrors the directory structure under `lib/textus/application/`:
4
+ #
5
+ # ops = Textus::Operations.for(store, role: "agent")
6
+ # ops.writes.put.call(key, body: "...")
7
+ # ops.reads.get.call(key)
8
+ # ops.refresh.worker.call(key)
9
+ #
10
+ # Replaces the prior `Textus::Composition` module (deleted in v0.12.2).
11
+ class Operations
12
+ def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
13
+ ctx = Application::Context.new(
14
+ store: store,
15
+ role: role,
16
+ correlation_id: correlation_id,
17
+ dry_run: dry_run,
18
+ )
19
+ new(ctx)
20
+ end
21
+
22
+ attr_reader :ctx
23
+
24
+ def initialize(ctx)
25
+ @ctx = ctx
26
+ end
27
+
28
+ def writes
29
+ @writes ||= Writes.new(@ctx)
30
+ end
31
+
32
+ def reads
33
+ @reads ||= Reads.new(@ctx)
34
+ end
35
+
36
+ def refresh
37
+ @refresh ||= Refresh.new(@ctx)
38
+ end
39
+
40
+ def with_role(role)
41
+ self.class.new(@ctx.with_role(role))
42
+ end
43
+ end
44
+ end
@@ -17,8 +17,8 @@ module Textus
17
17
  keys = collect_keys
18
18
  explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
19
19
  rows = keys.map do |key|
20
- env = @store.get(key)
21
- row = pluck(env["_meta"], env["body"])
20
+ env = Operations.for(@store).reads.get.call(key)
21
+ row = pluck(env.meta, env.body)
22
22
  explicit_pluck ? row : row.merge("_key" => key)
23
23
  end
24
24
  reduced = apply_reducer(rows)
@@ -38,19 +38,20 @@ module Textus
38
38
  private
39
39
 
40
40
  def apply_reducer(rows)
41
- name = @spec["reduce"] or return rows
42
- callable = @store.registry.rpc_callable(:reduce, name)
43
- view = Application::Context.new(store: @store, role: "human")
41
+ name = @spec["transform"] or return rows
42
+ callable = @store.registry.rpc_callable(:transform_rows, name)
43
+ view = Application::Context.system(@store)
44
44
  Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
45
- callable.call(store: view, rows: rows, config: @spec["reduce_config"] || {})
45
+ callable.call(store: view, rows: rows, config: @spec["transform_config"] || {})
46
46
  end
47
47
  rescue Timeout::Error
48
- raise UsageError.new("reduce '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
48
+ raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
49
49
  end
50
50
 
51
51
  def collect_keys
52
52
  prefixes = Array(@spec["select"])
53
- prefixes.flat_map { |p| @store.list(prefix: p).map { |row| row["key"] } }.uniq
53
+ ops = Operations.for(@store)
54
+ prefixes.flat_map { |p| ops.reads.list.call(prefix: p).map { |row| row["key"] } }.uniq
54
55
  end
55
56
 
56
57
  def pluck(frontmatter, _body)
@@ -1,13 +1,12 @@
1
1
  module Textus
2
2
  module Refresh
3
3
  def self.call(store, key, as:)
4
- ctx = Textus::Composition.context(store, role: as)
5
- Textus::Composition.refresh_worker(ctx).run(key)
4
+ Textus::Operations.for(store, role: as).refresh.worker.run(key)
6
5
  end
7
6
 
8
- def self.refresh_stale(store, prefix: nil, zone: nil, as: "script")
9
- ctx = Textus::Composition.context(store, role: as)
10
- Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
7
+ def self.refresh_stale(store, prefix: nil, zone: nil, as: "runner")
8
+ ops = Textus::Operations.for(store, role: as)
9
+ Textus::Application::Refresh::All.call(ops.ctx, prefix: prefix, zone: zone)
11
10
  end
12
11
 
13
12
  # Normalize the three accepted intake return shapes into the store's
@@ -6,8 +6,8 @@ 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 = store.get(from)
10
- meta = env["_meta"]
9
+ env = Textus::Operations.for(store).reads.get.call(from)
10
+ meta = env.meta
11
11
  schema = {
12
12
  "name" => name,
13
13
  "required" => meta.keys,
@@ -25,9 +25,9 @@ module Textus
25
25
  schema = load_schema(store, name)
26
26
  drift = []
27
27
  store.manifest.enumerate.each do |row|
28
- env = store.get(row[:key])
28
+ env = Textus::Operations.for(store).reads.get.call(row[:key])
29
29
  begin
30
- schema.validate!(env["_meta"])
30
+ schema.validate!(env.meta)
31
31
  rescue SchemaViolation => e
32
32
  drift << { "key" => row[:key], "details" => e.details }
33
33
  end
@@ -49,10 +49,11 @@ module Textus
49
49
  end
50
50
  raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
51
51
 
52
+ ops = Textus::Operations.for(store, role: "human")
52
53
  touched = []
53
54
  store.manifest.enumerate.each do |row|
54
- env = store.get(row[:key])
55
- meta = env["_meta"]
55
+ env = ops.reads.get.call(row[:key])
56
+ meta = env.meta.dup
56
57
  changed = false
57
58
  renames.each do |old, new|
58
59
  if meta.key?(old)
@@ -62,7 +63,7 @@ module Textus
62
63
  end
63
64
  next unless changed
64
65
 
65
- store.put(row[:key], meta: meta, body: env["body"], as: "human")
66
+ ops.writes.put.call(row[:key], meta: meta, body: env.body)
66
67
  touched << row[:key]
67
68
  end
68
69
  { "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
@@ -46,7 +46,7 @@ module Textus
46
46
  # Returns the Textus UID for a key (or nil if the entry has none yet).
47
47
  # Raises UnknownKey if the key doesn't resolve to a real file.
48
48
  def uid(key)
49
- get(key)["uid"]
49
+ get(key).uid
50
50
  end
51
51
 
52
52
  def deps(key) = Dependencies.deps_of(@manifest, key)
@@ -13,7 +13,7 @@ module Textus
13
13
  def rows_for(mentry)
14
14
  return [] unless mentry.intake_handler
15
15
 
16
- ttl = @manifest.policies_for(mentry.key).refresh&.ttl_seconds
16
+ ttl = @manifest.rules_for(mentry.key).refresh&.ttl_seconds
17
17
  return [] unless ttl
18
18
 
19
19
  path = Textus::Key::Path.resolve(@manifest, mentry)
@@ -54,7 +54,7 @@ module Textus
54
54
  last_writer = @audit_log.last_writer_for(key)
55
55
  return if last_writer.nil?
56
56
 
57
- env["_meta"].each_key do |field|
57
+ env.meta.each_key do |field|
58
58
  owner = schema.maintained_by(field)
59
59
  next if owner.nil? || last_writer == owner || last_writer == "human"
60
60
 
@@ -72,8 +72,8 @@ module Textus
72
72
 
73
73
  def validate_schema!(schema, envelope, format)
74
74
  payload = case format
75
- when "json", "yaml" then envelope["content"] || {}
76
- else envelope["_meta"] || {}
75
+ when "json", "yaml" then envelope.content || {}
76
+ else envelope.meta || {}
77
77
  end
78
78
  schema.validate!(payload)
79
79
  end
@@ -11,16 +11,6 @@ module Textus
11
11
  @reader = store.reader
12
12
  end
13
13
 
14
- # Backward-compat shim — orchestration now lives in Application::Writes::Put.
15
- # rubocop:disable Metrics/ParameterLists
16
- def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
17
- ctx = Textus::Application::Context.new(store: @store, role: as)
18
- Textus::Application::Writes::Put.new(ctx: ctx, bus: @store.bus).call(
19
- key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
20
- )
21
- end
22
- # rubocop:enable Metrics/ParameterLists
23
-
24
14
  # Pure I/O: validate, serialize, etag-check, write to disk, audit. No
25
15
  # permission check and no event firing — those are handled by the caller
26
16
  # (Application::Writes::Put).
@@ -76,56 +66,17 @@ module Textus
76
66
  end
77
67
 
78
68
  def ensure_uid(format, meta, content, existing_uid)
79
- case format
80
- when "markdown", "json", "yaml"
81
- m = meta.is_a?(Hash) ? meta.dup : {}
82
- m["uid"] = existing_uid || Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
83
- [m, content]
84
- else
85
- [meta, content]
86
- end
69
+ Textus::Entry.for_format(format).inject_uid(meta, content, existing_uid)
87
70
  end
88
71
 
89
72
  def enforce_name_match!(path, meta, format)
90
- return unless %w[markdown json yaml].include?(format)
91
- return unless meta.is_a?(Hash) && meta["name"]
92
-
93
- ext = Entry.for_format(format).extensions.first
94
- basename = File.basename(path, ext)
95
- return if meta["name"] == basename
96
-
97
- raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
73
+ Textus::Entry.for_format(format).enforce_name_match!(path, meta)
98
74
  end
99
75
 
100
76
  def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
101
- case mentry.format
102
- when "markdown", "text"
103
- bytes = strategy.serialize(meta: meta, body: body.to_s)
104
- [bytes, meta, body.to_s, nil]
105
- when "json", "yaml"
106
- raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
107
-
108
- if content.nil?
109
- begin
110
- parsed = strategy.parse(body.to_s, path: path)
111
- rescue BadFrontmatter => e
112
- raise BadContent.new(path, "bad_content: #{e.message}")
113
- end
114
- [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
115
- else
116
- bytes = strategy.serialize(meta: meta, body: "", content: content)
117
- [bytes, meta, bytes, content]
118
- end
119
- else
120
- raise UsageError.new("unknown format #{mentry.format.inspect}")
121
- end
122
- end
123
-
124
- # Backward-compat shim — orchestration now lives in Application::Writes::Delete.
125
- def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
126
- ctx = Textus::Application::Context.new(store: @store, role: as)
127
- Textus::Application::Writes::Delete.new(ctx: ctx, bus: @store.bus).call(
128
- key, if_etag: if_etag, suppress_events: suppress_events
77
+ _ = strategy
78
+ Textus::Entry.for_format(mentry.format).serialize_for_put(
79
+ meta: meta, body: body, content: content, path: path,
129
80
  )
130
81
  end
131
82
 
@@ -146,26 +97,6 @@ module Textus
146
97
  extras: ctx.correlation_id ? { "correlation_id" => ctx.correlation_id } : nil
147
98
  )
148
99
  end
149
-
150
- def accept(key, as:)
151
- Proposal.accept(@store, key, as: as)
152
- end
153
-
154
- def reject(pending_key, as: Role::DEFAULT)
155
- raise ProposalError.new("only human role can reject proposals; got '#{as}'") unless as == "human"
156
-
157
- mentry, = @store.manifest.resolve(pending_key)
158
- raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})") unless mentry.in_proposal_zone?
159
-
160
- env = @store.get(pending_key)
161
- proposal = env.dig("_meta", "proposal") or
162
- raise ProposalError.new("entry has no proposal block: #{pending_key}")
163
- target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
164
-
165
- delete(pending_key, as: as)
166
- @store.fire_event(:reject, key: pending_key, target_key: target_key)
167
- { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
168
- end
169
100
  end
170
101
  end
171
102
  end
data/lib/textus/store.rb CHANGED
@@ -45,7 +45,7 @@ module Textus
45
45
  load_hooks
46
46
  @reader = Reader.new(self)
47
47
  @writer = Writer.new(self)
48
- fire_event(:loaded)
48
+ @bus.publish(:store_loaded, store: Textus::Application::Context.system(self))
49
49
  end
50
50
 
51
51
  def load_hooks
@@ -75,60 +75,6 @@ module Textus
75
75
  end
76
76
  end
77
77
 
78
- def get(key, as: Textus::Role::DEFAULT)
79
- ctx = Textus::Composition.context(self, role: as)
80
- result = Textus::Composition.reads_get(ctx).call(key)
81
- raise UnknownKey.new(key, suggestions: manifest.suggestions_for(key)) if result.nil?
82
-
83
- result
84
- end
85
-
86
- def where(key) = @reader.where(key)
87
- def list(**) = @reader.list(**)
88
- def schema_envelope(key) = @reader.schema_envelope(key)
89
-
90
- # rubocop:disable Metrics/ParameterLists
91
- def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
92
- ctx = Textus::Composition.context(self, role: as)
93
- Textus::Composition.writes_put(ctx).call(
94
- key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
95
- )
96
- end
97
- # rubocop:enable Metrics/ParameterLists
98
-
99
- def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
100
- ctx = Textus::Composition.context(self, role: as)
101
- Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag, suppress_events: suppress_events)
102
- end
103
-
104
- def fire_event(event, **)
105
- view = Textus::Application::Context.new(store: self, role: "human")
106
- @bus.publish(event, store: view, **)
107
- end
108
-
109
- def accept(key, as: Role::DEFAULT)
110
- ctx = Textus::Composition.context(self, role: as)
111
- Textus::Composition.writes_accept(ctx).call(key)
112
- end
113
-
114
- def reject(...) = @writer.reject(...)
115
-
116
- def deps(key) = @reader.deps(key)
117
- def rdeps(key) = @reader.rdeps(key)
118
- def published = @reader.published
119
- def stale(**) = @reader.stale(**)
120
- def validate_all = @reader.validate_all
121
-
122
- def uid(key) = @reader.uid(key)
123
-
124
- # Move an entry from old_key to new_key within the same zone. Preserves
125
- # uid (minting one first if absent), validates both keys against the
126
- # manifest, refuses to clobber, and writes one mv audit row.
127
- def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
128
- Mover.new(store: self, reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
129
- .call(old_key, new_key, as: as, dry_run: dry_run, correlation_id: correlation_id)
130
- end
131
-
132
78
  def audit_log
133
79
  @audit_log ||= Store::AuditLog.new(@root)
134
80
  end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.10.5"
3
- PROTOCOL = "textus/2"
2
+ VERSION = "0.14.0"
3
+ PROTOCOL = "textus/3"
4
4
  end
data/lib/textus.rb CHANGED
@@ -7,6 +7,7 @@ loader.inflector.inflect(
7
7
  "cli" => "CLI",
8
8
  "json" => "Json",
9
9
  "yaml" => "Yaml",
10
+ "hook_dsl_scanner" => "HookDSLScanner",
10
11
  )
11
12
  loader.ignore(File.expand_path("textus/errors.rb", __dir__))
12
13
  loader.setup