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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +60 -40
- data/CHANGELOG.md +318 -3
- data/README.md +34 -27
- data/SPEC.md +226 -145
- data/docs/conventions.md +8 -8
- data/lib/textus/application/context.rb +4 -0
- data/lib/textus/application/reads/blame.rb +1 -1
- data/lib/textus/application/reads/deps.rb +15 -0
- data/lib/textus/application/reads/freshness.rb +4 -4
- data/lib/textus/application/reads/get.rb +9 -12
- data/lib/textus/application/reads/list.rb +15 -0
- data/lib/textus/application/reads/policy_explain.rb +2 -2
- data/lib/textus/application/reads/published.rb +15 -0
- data/lib/textus/application/reads/rdeps.rb +15 -0
- data/lib/textus/application/reads/schema_envelope.rb +15 -0
- data/lib/textus/application/reads/stale.rb +15 -0
- data/lib/textus/application/reads/uid.rb +15 -0
- data/lib/textus/application/reads/validate_all.rb +15 -0
- data/lib/textus/application/reads/where.rb +15 -0
- data/lib/textus/application/refresh/all.rb +2 -2
- data/lib/textus/application/refresh/orchestrator.rb +1 -1
- data/lib/textus/application/refresh/worker.rb +8 -8
- data/lib/textus/application/writes/accept.rb +26 -8
- data/lib/textus/application/writes/build.rb +12 -49
- data/lib/textus/application/writes/delete.rb +1 -1
- data/lib/textus/application/writes/mv.rb +144 -0
- data/lib/textus/application/writes/publish.rb +42 -10
- data/lib/textus/application/writes/put.rb +1 -1
- data/lib/textus/application/writes/reject.rb +37 -0
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/builder/renderer/yaml.rb +1 -1
- data/lib/textus/cli/group/key.rb +1 -1
- data/lib/textus/cli/group/refresh.rb +21 -0
- data/lib/textus/cli/group/rule.rb +11 -0
- data/lib/textus/cli/verb/accept.rb +1 -2
- data/lib/textus/cli/verb/audit.rb +3 -3
- data/lib/textus/cli/verb/blame.rb +1 -2
- data/lib/textus/cli/verb/build.rb +6 -2
- data/lib/textus/cli/verb/delete.rb +1 -2
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -2
- data/lib/textus/cli/verb/get.rb +2 -3
- data/lib/textus/cli/verb/hook_run.rb +3 -2
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mv.rb +1 -1
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/put.rb +3 -3
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -2
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/{policy_explain.rb → rule_explain.rb} +2 -3
- data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb.rb +9 -3
- data/lib/textus/cli.rb +6 -6
- data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +39 -16
- data/lib/textus/doctor/check/intake_registration.rb +4 -4
- data/lib/textus/doctor/check/protocol_version.rb +47 -0
- data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor.rb +6 -5
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/permission.rb +4 -4
- data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
- data/lib/textus/domain/policy/promotion.rb +45 -0
- data/lib/textus/entry/base.rb +28 -0
- data/lib/textus/entry/json.rb +59 -0
- data/lib/textus/entry/markdown.rb +46 -0
- data/lib/textus/entry/text.rb +35 -0
- data/lib/textus/entry/yaml.rb +59 -0
- data/lib/textus/entry.rb +16 -0
- data/lib/textus/envelope.rb +44 -14
- data/lib/textus/errors.rb +24 -5
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/dispatcher.rb +1 -1
- data/lib/textus/hooks/dsl.rb +3 -10
- data/lib/textus/hooks/loader.rb +1 -2
- data/lib/textus/hooks/registry.rb +22 -21
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/init.rb +25 -34
- data/lib/textus/intro.rb +65 -9
- data/lib/textus/manifest/entry/parser.rb +84 -0
- data/lib/textus/manifest/entry/validators/events.rb +21 -0
- data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
- data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
- data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
- data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
- data/lib/textus/manifest/entry/validators.rb +20 -0
- data/lib/textus/manifest/entry.rb +38 -189
- data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
- data/lib/textus/manifest/schema.rb +49 -0
- data/lib/textus/manifest.rb +50 -24
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/operations/reads.rb +39 -0
- data/lib/textus/operations/refresh.rb +27 -0
- data/lib/textus/operations/writes.rb +21 -0
- data/lib/textus/operations.rb +44 -0
- data/lib/textus/projection.rb +9 -8
- data/lib/textus/refresh.rb +4 -5
- data/lib/textus/schema/tools.rb +8 -7
- data/lib/textus/store/reader.rb +1 -1
- data/lib/textus/store/staleness/intake_check.rb +1 -1
- data/lib/textus/store/validator.rb +3 -3
- data/lib/textus/store/writer.rb +5 -74
- data/lib/textus/store.rb +1 -55
- data/lib/textus/version.rb +2 -2
- data/lib/textus.rb +1 -0
- metadata +35 -10
- data/lib/textus/cli/group/policy.rb +0 -11
- data/lib/textus/composition.rb +0 -72
- data/lib/textus/proposal.rb +0 -10
- 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
|
-
|
|
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 `:
|
|
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
|
|
89
|
-
textus refresh-stale --zone=
|
|
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 `
|
|
92
|
+
Freshness budgets live in the top-level `rules:` block, matched by glob:
|
|
93
93
|
|
|
94
94
|
```yaml
|
|
95
|
-
|
|
96
|
-
- match:
|
|
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=
|
|
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::
|
|
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
|
|
|
@@ -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 `
|
|
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.
|
|
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("
|
|
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
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
@@ -9,7 +9,7 @@ module Textus
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call(key:)
|
|
12
|
-
policies = @ctx.store.manifest.
|
|
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
|
-
|
|
32
|
+
promotion: winners.promote && { requires: winners.promote.requires },
|
|
33
33
|
},
|
|
34
34
|
}
|
|
35
35
|
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::
|
|
8
|
+
worker = Textus::Application::Refresh::Worker.new(ctx: ctx, bus: ctx.store.bus)
|
|
9
9
|
|
|
10
|
-
stale_rows = ctx.
|
|
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(:
|
|
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(:
|
|
31
|
-
@bus.publish(:
|
|
32
|
-
|
|
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.
|
|
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
|
-
|
|
60
|
+
suppress_events: true
|
|
61
61
|
)
|
|
62
62
|
change = detect_change(before_etag, envelope)
|
|
63
63
|
unless change == :unchanged
|
|
64
|
-
@bus.publish(:
|
|
65
|
-
|
|
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
|
|
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["
|
|
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["
|
|
23
|
-
target_body = env
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(pending_key)
|
|
32
34
|
|
|
33
|
-
@bus.publish(:
|
|
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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
|
|
25
|
+
materialize(mentry)
|
|
24
26
|
end
|
|
25
|
-
|
|
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(:
|
|
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(:
|
|
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
|