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
data/docs/conventions.md CHANGED
@@ -20,7 +20,7 @@ Recommended top-level layout — the spec allows alternatives, but this is what
20
20
  zones/
21
21
  identity/ # identity, voice, slow-changing facts — humans only
22
22
  working/ # agent-writable working memory
23
- inbox/ # script-fed external inputs
23
+ intake/ # runner-fed external inputs
24
24
  review/ # AI proposals awaiting accept
25
25
  output/ # generated by build runners — never edit by hand
26
26
  ```
@@ -82,25 +82,25 @@ Full contract for both shapes is in [`../SPEC.md` §5.2 and §5.2.1](../SPEC.md)
82
82
 
83
83
  ## Intake and freshness
84
84
 
85
- External inputs land via `:intake` hooks, not shell commands. Each inbox entry names a registered handler; refresh is on demand:
85
+ External inputs land via `:resolve_intake` hooks, not shell commands. Each intake entry names a registered handler; refresh is on demand:
86
86
 
87
87
  ```sh
88
- textus refresh inbox.notion.roadmap --as=script
89
- textus refresh-stale --zone=inbox --as=script # everything past its TTL
88
+ textus refresh intake.notion.roadmap --as=runner
89
+ textus refresh-stale --zone=intake --as=runner # everything past its TTL
90
90
  ```
91
91
 
92
- Freshness budgets live in the top-level `policies:` block, matched by glob:
92
+ Freshness budgets live in the top-level `rules:` block, matched by glob:
93
93
 
94
94
  ```yaml
95
- policies:
96
- - match: inbox.notion.**
95
+ rules:
96
+ - match: intake.notion.**
97
97
  refresh: { ttl: 6h, on_stale: warn } # warn | sync | timed_sync
98
98
  ```
99
99
 
100
100
  A typical scheduled-refresh integration shells the `refresh-stale` sweep itself:
101
101
 
102
102
  ```sh
103
- textus refresh-stale --zone=inbox --as=script # in cron / CI
103
+ textus refresh-stale --zone=intake --as=runner # in cron / CI
104
104
  ```
105
105
 
106
106
  See [`./zones.md` §6](zones.md) for the full intake contract and [`./events.md`](events.md) for writing custom handlers.
@@ -5,6 +5,10 @@ module Textus
5
5
  class Context
6
6
  attr_reader :store, :role, :correlation_id
7
7
 
8
+ def self.system(store)
9
+ new(store: store, role: "human")
10
+ end
11
+
8
12
  def initialize(store:, role:, correlation_id: nil, clock: Time, dry_run: false)
9
13
  @store = store
10
14
  @role = role.to_s
@@ -13,7 +13,7 @@ module Textus
13
13
  end
14
14
 
15
15
  def call(key:, limit: nil)
16
- audit_rows = Textus::Composition.audit(@ctx).call(key: key, limit: limit)
16
+ audit_rows = Textus::Application::Reads::Audit.new(ctx: @ctx).call(key: key, limit: limit)
17
17
  path = resolve_path(key)
18
18
  return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
19
19
 
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Deps
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.deps(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -4,7 +4,7 @@ module Textus
4
4
  module Application
5
5
  module Reads
6
6
  # Per-entry freshness report. Walks every entry declared in the manifest,
7
- # consults `policies_for(key)` for a refresh policy, and reports the
7
+ # consults `rules_for(key)` for a refresh rule, and reports the
8
8
  # current status. Status is one of :fresh, :stale, :never_refreshed, or
9
9
  # :no_policy.
10
10
  class Freshness
@@ -27,15 +27,15 @@ module Textus
27
27
  private
28
28
 
29
29
  def row_for(mentry)
30
- set = @ctx.store.manifest.policies_for(mentry.key)
30
+ set = @ctx.store.manifest.rules_for(mentry.key)
31
31
  refresh = set.refresh
32
32
  envelope = safe_get(mentry.key)
33
- last = envelope&.dig("_meta", "last_refreshed_at")
33
+ last = envelope&.meta&.dig("last_refreshed_at")
34
34
 
35
35
  return base_row(mentry, last).merge(status: :no_policy) if refresh.nil?
36
36
 
37
37
  fp = refresh.to_freshness_policy
38
- verdict = @evaluator.call(fp, envelope || {}, now: @ctx.now)
38
+ verdict = @evaluator.call(fp, envelope, now: @ctx.now)
39
39
  status = if verdict.fresh? then :fresh
40
40
  elsif last.nil? then :never_refreshed
41
41
  else :stale
@@ -12,7 +12,7 @@ module Textus
12
12
  envelope = @ctx.store.reader.read_raw_envelope(key)
13
13
  return nil if envelope.nil?
14
14
 
15
- policy_set = @ctx.store.manifest.policies_for(key)
15
+ policy_set = @ctx.store.manifest.rules_for(key)
16
16
  refresh_policy = policy_set.refresh
17
17
  return annotate_fresh(envelope) if refresh_policy.nil?
18
18
 
@@ -40,21 +40,18 @@ module Textus
40
40
  private
41
41
 
42
42
  def annotate(envelope, verdict, refreshing:, refresh_error: nil)
43
- envelope = envelope.dup
44
- envelope["stale"] = verdict.stale?
45
- envelope["stale_reason"] = verdict.reason
46
- envelope["refreshing"] = refreshing
47
- envelope["refresh_error"] = refresh_error if refresh_error
48
- envelope
43
+ fresh = {
44
+ "stale" => verdict.stale?,
45
+ "stale_reason" => verdict.reason,
46
+ "refreshing" => refreshing,
47
+ }
48
+ fresh["refresh_error"] = refresh_error if refresh_error
49
+ envelope.with(freshness: fresh)
49
50
  end
50
51
 
51
52
  # No refresh policy applies to this key — treat as fresh, skip evaluation/orchestration.
52
53
  def annotate_fresh(envelope)
53
- envelope = envelope.dup
54
- envelope["stale"] = false
55
- envelope["stale_reason"] = nil
56
- envelope["refreshing"] = false
57
- envelope
54
+ envelope.with(freshness: { "stale" => false, "stale_reason" => nil, "refreshing" => false })
58
55
  end
59
56
  end
60
57
  end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class List
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(prefix: nil, zone: nil)
10
+ @ctx.store.reader.list(prefix: prefix, zone: zone)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -9,7 +9,7 @@ module Textus
9
9
  end
10
10
 
11
11
  def call(key:)
12
- policies = @ctx.store.manifest.policies
12
+ policies = @ctx.store.manifest.rules
13
13
  matching = policies.explain(key)
14
14
  winners = policies.for(key)
15
15
 
@@ -29,7 +29,7 @@ module Textus
29
29
  on_stale: winners.refresh.on_stale,
30
30
  },
31
31
  handler_allowlist: winners.handler_allowlist&.handlers,
32
- promote_requires: winners.promote&.requires,
32
+ promotion: winners.promote && { requires: winners.promote.requires },
33
33
  },
34
34
  }
35
35
  end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Published
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call
10
+ @ctx.store.reader.published
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Rdeps
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.rdeps(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class SchemaEnvelope
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.schema_envelope(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Stale
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(prefix: nil, zone: nil)
10
+ @ctx.store.reader.stale(prefix: prefix, zone: zone)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Uid
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.uid(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class ValidateAll
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call
10
+ @ctx.store.reader.validate_all
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Where
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.where(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,9 +5,9 @@ module Textus
5
5
  module_function
6
6
 
7
7
  def call(ctx, prefix: nil, zone: nil)
8
- worker = Textus::Composition.refresh_worker(ctx)
8
+ worker = Textus::Application::Refresh::Worker.new(ctx: ctx, bus: ctx.store.bus)
9
9
 
10
- stale_rows = ctx.store.stale(prefix: prefix, zone: zone)
10
+ stale_rows = Textus::Application::Reads::Stale.new(ctx: ctx).call(prefix: prefix, zone: zone)
11
11
  refreshed = []
12
12
  failed = []
13
13
  skipped = []
@@ -50,7 +50,7 @@ module Textus
50
50
  store_view = @store ? Textus::Application::Context.new(store: @store, role: @role) : nil
51
51
  payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
52
52
  payload[:store] = store_view if store_view
53
- @bus.publish(:refresh_detached, **payload)
53
+ @bus.publish(:refresh_backgrounded, **payload)
54
54
  @detached_spawner.call(store_root: @store_root, key: key)
55
55
  Textus::Domain::Outcome::Detached.new
56
56
  elsif result.is_a?(Textus::Error)
@@ -27,9 +27,9 @@ module Textus
27
27
  end
28
28
 
29
29
  def fetch_with_bus(key, mentry)
30
- callable = @ctx.store.registry.rpc_callable(:intake, mentry.intake_handler)
31
- @bus.publish(:refresh_began, store: read_view, key: key, mode: :sync,
32
- correlation_id: @ctx.correlation_id)
30
+ callable = @ctx.store.registry.rpc_callable(:resolve_intake, mentry.intake_handler)
31
+ @bus.publish(:refresh_started, store: read_view, key: key, mode: :sync,
32
+ correlation_id: @ctx.correlation_id)
33
33
  call_intake(key, mentry, callable)
34
34
  end
35
35
 
@@ -54,22 +54,22 @@ module Textus
54
54
 
55
55
  def persist_and_notify(key, mentry, result, before_etag)
56
56
  normalized = Textus::Refresh.normalize_action_result(result, format: mentry.format)
57
- envelope = @ctx.store.put(
57
+ envelope = Textus::Application::Writes::Put.new(ctx: @ctx, bus: @bus).call(
58
58
  key,
59
59
  meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
60
- as: @ctx.role, suppress_events: true
60
+ suppress_events: true
61
61
  )
62
62
  change = detect_change(before_etag, envelope)
63
63
  unless change == :unchanged
64
- @bus.publish(:refreshed, store: read_view, key: key, envelope: envelope, change: change,
65
- correlation_id: @ctx.correlation_id)
64
+ @bus.publish(:entry_refreshed, store: read_view, key: key, envelope: envelope, change: change,
65
+ correlation_id: @ctx.correlation_id)
66
66
  end
67
67
  envelope
68
68
  end
69
69
 
70
70
  def detect_change(before_etag, envelope)
71
71
  if before_etag.nil? then :created
72
- elsif envelope["etag"] == before_etag then :unchanged
72
+ elsif envelope.etag == before_etag then :unchanged
73
73
  else :updated
74
74
  end
75
75
  end
@@ -10,27 +10,29 @@ module Textus
10
10
  def call(pending_key)
11
11
  raise ProposalError.new("only human role can accept proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
12
12
 
13
- env = @ctx.store.get(pending_key)
14
- proposal = env["_meta"]["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
13
+ env = @ctx.store.reader.get(pending_key)
14
+ proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
15
15
  target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
16
16
  action = proposal["action"] || "put"
17
17
 
18
+ evaluate_promotion!(env, target)
19
+
18
20
  case action
19
21
  when "put"
20
22
  # Nested proposal "frontmatter" — the meta to write to the accepted
21
23
  # target. Not related to the removed intake-handler legacy bridge.
22
- target_meta = env["_meta"]["frontmatter"] || {}
23
- target_body = env["body"]
24
- Composition.writes_put(@ctx).call(target, meta: target_meta, body: target_body)
24
+ target_meta = env.meta["frontmatter"] || {}
25
+ target_body = env.body
26
+ Textus::Application::Writes::Put.new(ctx: @ctx, bus: @bus).call(target, meta: target_meta, body: target_body)
25
27
  when "delete"
26
- Composition.writes_delete(@ctx).call(target)
28
+ Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(target)
27
29
  else
28
30
  raise ProposalError.new("unknown action: #{action}")
29
31
  end
30
32
 
31
- Composition.writes_delete(@ctx).call(pending_key)
33
+ Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(pending_key)
32
34
 
33
- @bus.publish(:accepted,
35
+ @bus.publish(:proposal_accepted,
34
36
  store: @ctx.with_role(@ctx.role),
35
37
  key: pending_key,
36
38
  target_key: target,
@@ -38,6 +40,22 @@ module Textus
38
40
 
39
41
  { "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
40
42
  end
43
+
44
+ private
45
+
46
+ def evaluate_promotion!(env, target_key)
47
+ rules = @ctx.store.manifest.rules_for(target_key)
48
+ promote = rules.promote
49
+ return if promote.nil? || promote.requires.empty?
50
+
51
+ policy = Textus::Domain::Policy::Promotion.from_names(promote.requires)
52
+ result = policy.evaluate(entry: env, store: @ctx.store)
53
+ return if result.ok?
54
+
55
+ raise ProposalError.new(
56
+ "promotion gate failed: #{result.reasons.join("; ")}",
57
+ )
58
+ end
41
59
  end
42
60
  end
43
61
  end
@@ -4,9 +4,12 @@ module Textus
4
4
  module Application
5
5
  module Writes
6
6
  # Materializes generator-zone entries (template + projection) onto disk
7
- # and copies the result to any configured `publish_to` / `publish_each`
8
- # targets. Fires `:built` and `:published` events on the bus, tagged with
9
- # the request's correlation_id for traceability.
7
+ # and copies the result to any configured `publish_to:` targets. Fires
8
+ # `:build_completed` and `:file_published` events.
9
+ #
10
+ # For `publish_each:` (per-leaf publishing of nested entries), see
11
+ # `Application::Writes::Publish`. The CLI verb `textus build` calls
12
+ # both classes and merges the results.
10
13
  class Build
11
14
  def initialize(ctx:, bus:)
12
15
  @ctx = ctx
@@ -14,16 +17,14 @@ module Textus
14
17
  end
15
18
 
16
19
  def call(prefix: nil)
17
- built = []
18
- manifest.entries.each do |mentry|
20
+ built = manifest.entries.filter_map do |mentry|
19
21
  next unless mentry.in_generator_zone?
20
22
  next unless mentry.projection || mentry.template
21
23
  next if prefix && !mentry.key.start_with?(prefix)
22
24
 
23
- built << materialize(mentry)
25
+ materialize(mentry)
24
26
  end
25
- published_leaves = publish_leaves(prefix: prefix)
26
- { "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
27
+ { "protocol" => Textus::PROTOCOL, "built" => built }
27
28
  end
28
29
 
29
30
  private
@@ -32,41 +33,6 @@ module Textus
32
33
  def manifest = store.manifest
33
34
  def root = store.root
34
35
 
35
- def publish_leaves(prefix: nil)
36
- repo_root = File.dirname(root)
37
- out = []
38
- manifest.entries.each do |mentry|
39
- next unless mentry.nested && mentry.publish_each
40
- next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
41
-
42
- manifest.enumerate(prefix: mentry.key).each do |row|
43
- next unless row[:manifest_entry].equal?(mentry)
44
- next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
45
-
46
- out << publish_leaf(mentry, row, repo_root)
47
- end
48
- end
49
- out
50
- end
51
-
52
- def publish_leaf(mentry, row, repo_root)
53
- target_rel = mentry.publish_target_for(row[:key])
54
- target_abs = File.expand_path(File.join(repo_root, target_rel))
55
- unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
56
- raise PublishError.new(
57
- "entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
58
- )
59
- end
60
-
61
- Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: root)
62
- publish_event(:published,
63
- key: row[:key],
64
- envelope: store.get(row[:key]),
65
- source: row[:path],
66
- target: target_abs)
67
- { "key" => row[:key], "source" => row[:path], "target" => target_abs }
68
- end
69
-
70
36
  def materialize(mentry)
71
37
  target_path = Builder::Pipeline.run(
72
38
  store: store,
@@ -85,29 +51,26 @@ module Textus
85
51
  end
86
52
 
87
53
  def publish_and_fire(mentry, target_path)
88
- envelope = store.get(mentry.key)
54
+ envelope = store.reader.get(mentry.key)
89
55
  repo_root = File.dirname(root)
90
56
 
91
57
  mentry.publish_to.each do |rel|
92
58
  target_abs = File.join(repo_root, rel)
93
59
  Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: root)
94
- publish_event(:published,
60
+ publish_event(:file_published,
95
61
  key: mentry.key,
96
62
  envelope: envelope,
97
63
  source: target_path,
98
64
  target: target_abs)
99
65
  end
100
66
 
101
- publish_event(:built,
67
+ publish_event(:build_completed,
102
68
  key: mentry.key,
103
69
  envelope: envelope,
104
70
  sources: Array(mentry.projection&.fetch("select", nil)).compact)
105
71
  end
106
72
 
107
73
  def publish_event(event, **payload)
108
- # `with_role` returns a Context that preserves the original
109
- # correlation_id, so hooks reading `store.correlation_id` see the
110
- # same value as the event's top-level correlation_id key.
111
74
  @bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
112
75
  end
113
76
  end
@@ -21,7 +21,7 @@ module Textus
21
21
  )
22
22
 
23
23
  unless suppress_events
24
- @bus.publish(:deleted,
24
+ @bus.publish(:entry_deleted,
25
25
  store: @ctx.with_role(@ctx.role),
26
26
  key: key,
27
27
  correlation_id: @ctx.correlation_id)