textus 0.9.2 → 0.10.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/CHANGELOG.md +105 -0
- data/lib/textus/application/context.rb +0 -24
- data/lib/textus/application/refresh/orchestrator.rb +3 -2
- data/lib/textus/application/refresh/worker.rb +1 -1
- data/lib/textus/application/writes/accept.rb +3 -2
- data/lib/textus/application/writes/build.rb +101 -9
- data/lib/textus/application/writes/delete.rb +1 -2
- data/lib/textus/application/writes/put.rb +1 -2
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/builder/renderer/markdown.rb +1 -1
- data/lib/textus/builder/renderer/text.rb +1 -1
- data/lib/textus/builder/renderer/yaml.rb +1 -1
- data/lib/textus/builder/renderer.rb +1 -1
- data/lib/textus/cli/verb/accept.rb +1 -2
- data/lib/textus/cli/verb/audit.rb +1 -2
- data/lib/textus/cli/verb/blame.rb +1 -2
- data/lib/textus/cli/verb/delete.rb +1 -2
- data/lib/textus/cli/verb/freshness.rb +1 -2
- data/lib/textus/cli/verb/get.rb +1 -2
- data/lib/textus/cli/verb/hook_run.rb +1 -1
- data/lib/textus/cli/verb/mv.rb +1 -2
- data/lib/textus/cli/verb/policy_explain.rb +1 -2
- data/lib/textus/cli/verb/put.rb +5 -4
- data/lib/textus/cli/verb/refresh.rb +1 -2
- data/lib/textus/cli/verb/refresh_stale.rb +1 -2
- data/lib/textus/cli/verb/reject.rb +1 -2
- data/lib/textus/cli/verb.rb +14 -0
- data/lib/textus/composition.rb +1 -0
- data/lib/textus/doctor.rb +3 -1
- data/lib/textus/intro.rb +0 -5
- data/lib/textus/manifest/entry.rb +21 -4
- data/lib/textus/manifest.rb +0 -11
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/refresh.rb +1 -2
- data/lib/textus/store/staleness.rb +1 -1
- data/lib/textus/store/writer.rb +1 -1
- data/lib/textus/store.rb +1 -1
- data/lib/textus/version.rb +1 -1
- metadata +1 -4
- data/lib/textus/builder.rb +0 -99
- data/lib/textus/publisher.rb +0 -6
- data/lib/textus/store/view.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 198dc9a4561b79bf4da22a2f890caa5da2764b8d82b1665b506d6c4c8c0e3fe3
|
|
4
|
+
data.tar.gz: dc5333c3605b7b05174f4b290fcb63260aad7ea089da900f0b3325cf3823d83c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9e08a7a3fc46732dcd9458114765a54e3f8d2aca08b3ead6b70ecfc2782c0e9fec5eec38fc933cef9582c5943db52973c8bb06e544c02afec468dc50bbc8797
|
|
7
|
+
data.tar.gz: 9cb891ae4ce1d7583af7546e16e1f1a23d8d88af105430d31c2eb3fd4e25af6df2a60c5677c810a3d11f01582d7500a18d681b8996e9f2f5da1948e32ec2a39f
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,111 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
8
8
|
(currently `textus/2`, embedded in every envelope as `protocol`). The protocol
|
|
9
9
|
is additive within a major; a new major would change the wire string.
|
|
10
10
|
|
|
11
|
+
## 0.10.0 — Shim removal, signal-based zone detection, Builder extraction (2026-05-22)
|
|
12
|
+
|
|
13
|
+
### Breaking — Ruby API
|
|
14
|
+
|
|
15
|
+
- `Textus::Publisher` constant removed. Use `Textus::Infra::Publisher`.
|
|
16
|
+
- `Textus::Store::View` class removed. Use `Textus::Application::Context`
|
|
17
|
+
(constructed via `Composition.context(store, role:)`).
|
|
18
|
+
- `Textus::Builder` class removed as a public entry point. Build logic lives
|
|
19
|
+
in `Textus::Application::Writes::Build`. External callers should use
|
|
20
|
+
`Textus::Composition.writes_build(ctx).call` instead of
|
|
21
|
+
`Textus::Builder.new(store).build`. The `Textus::Builder` namespace is
|
|
22
|
+
retained internally only for nested helpers (`Builder::Pipeline`,
|
|
23
|
+
`Builder::Renderer::*`).
|
|
24
|
+
- `Application::Context` no longer exposes `put` / `delete` / `get` / `list`
|
|
25
|
+
/ `where` shim methods. Hook callers that receive a Context via the
|
|
26
|
+
`store:` hook keyword must call `ctx.store.put(...)` etc., and explicitly
|
|
27
|
+
pass `as: ctx.role` for write operations.
|
|
28
|
+
- Intake handler return values must use `_meta:` for frontmatter. The
|
|
29
|
+
previous `frontmatter:` legacy key is no longer accepted.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `textus reject` and `textus refresh-stale` now work correctly for stores
|
|
34
|
+
that use the post-0.9.2 default zone names (`review`, `output`).
|
|
35
|
+
Zone-kind detection is now signal-based (driven by `writable_by:`
|
|
36
|
+
membership), not name-based. Stores using the pre-0.9.2 names (`pending`,
|
|
37
|
+
`derived`) continue to work.
|
|
38
|
+
- Event payloads' `store:` keyword now carries a Context whose
|
|
39
|
+
`correlation_id` matches the event payload's top-level `correlation_id`
|
|
40
|
+
key. Previously the `store:` Context received a fresh, unrelated
|
|
41
|
+
`correlation_id`.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- `Textus::Manifest::Entry#in_generator_zone?` and `#in_proposal_zone?`
|
|
46
|
+
predicates. Internal `derived?` retained as an alias of
|
|
47
|
+
`in_generator_zone?`.
|
|
48
|
+
- `:built` and `:published` events now carry `correlation_id` in the
|
|
49
|
+
payload, matching the existing pattern on `:put` / `:deleted` /
|
|
50
|
+
`:accepted`.
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
|
|
54
|
+
- Legacy zone-purpose annotations for `canon` / `intake` / `pending` /
|
|
55
|
+
`derived` removed from `Textus::Intro::ZONE_PURPOSES`. Custom-named zones
|
|
56
|
+
continue to get no purpose annotation (existing behavior). Stores still
|
|
57
|
+
using the pre-rename default names will simply not get purpose
|
|
58
|
+
annotations on those zones in `textus intro` output.
|
|
59
|
+
- Dead code: `Textus::Manifest#validate_keys!` removed (had no callers).
|
|
60
|
+
|
|
61
|
+
### Internal
|
|
62
|
+
|
|
63
|
+
- Builder logic fully extracted into `Application::Writes::Build`.
|
|
64
|
+
- CLI verbs now share `context_for(store)` / `resolved_role(store)`
|
|
65
|
+
helpers on `CLI::Verb`.
|
|
66
|
+
- Internal helpers in `Manifest`, `Doctor`, and `Manifest::Entry` are
|
|
67
|
+
properly marked private.
|
|
68
|
+
|
|
69
|
+
### Unchanged
|
|
70
|
+
|
|
71
|
+
- Wire protocol stays `textus/2`. Envelope shape unchanged.
|
|
72
|
+
- CLI verbs, their flags, and their JSON output shape — unchanged.
|
|
73
|
+
- Manifest YAML schema — unchanged.
|
|
74
|
+
- Event names — unchanged (payload gains `correlation_id` on `:built` /
|
|
75
|
+
`:published`, but no existing key is removed or renamed).
|
|
76
|
+
- Hook DSL — unchanged in shape. The `store:` keyword still passes an
|
|
77
|
+
object that responds to `.get`, `.list`, `.where`. The Context's
|
|
78
|
+
role-aware `with_role` is the recommended construction site for hook
|
|
79
|
+
contexts now.
|
|
80
|
+
|
|
81
|
+
### Migration recipe
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Hook handlers — before 0.10.0
|
|
85
|
+
Textus.hook(:intake, :my_hook) do |store:, config:, args:|
|
|
86
|
+
store.put("inbox.foo", meta: { ... }, body: "...") # used Context shim
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Hook handlers — 0.10.0+
|
|
90
|
+
Textus.hook(:intake, :my_hook) do |store:, config:, args:|
|
|
91
|
+
ctx = store # rename for clarity if desired
|
|
92
|
+
ctx.store.put("inbox.foo", meta: { ... }, body: "...", as: ctx.role)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Intake handler returns — before 0.10.0
|
|
96
|
+
{ frontmatter: { ... }, body: "..." } # legacy key
|
|
97
|
+
|
|
98
|
+
# Intake handler returns — 0.10.0+
|
|
99
|
+
{ _meta: { ... }, body: "..." } # _meta is the canonical key
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If you imported the removed constants directly:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Before
|
|
106
|
+
Textus::Publisher # removed
|
|
107
|
+
Textus::Store::View # removed
|
|
108
|
+
Textus::Builder.new(store).build(key, ...) # removed
|
|
109
|
+
|
|
110
|
+
# After
|
|
111
|
+
Textus::Infra::Publisher
|
|
112
|
+
Textus::Application::Context # via Composition.context(store, role:)
|
|
113
|
+
Textus::Composition.writes_build(ctx).call(key, ...)
|
|
114
|
+
```
|
|
115
|
+
|
|
11
116
|
## 0.9.2 — Policies, audit verbs, zone rename (2026-05-22)
|
|
12
117
|
|
|
13
118
|
### Breaking — manifest YAML
|
|
@@ -39,30 +39,6 @@ module Textus
|
|
|
39
39
|
dry_run: @dry_run,
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
|
-
|
|
43
|
-
# Backward-compat for intake handlers receiving a Context (was Store::View)
|
|
44
|
-
# that call store.put/get/delete on it. Slated for removal in 0.10.0.
|
|
45
|
-
def put(key, **opts)
|
|
46
|
-
opts[:as] ||= role
|
|
47
|
-
store.put(key, **opts)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def delete(key, **opts)
|
|
51
|
-
opts[:as] ||= role
|
|
52
|
-
store.delete(key, **opts)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def get(key, **)
|
|
56
|
-
store.get(key, **)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def list(*, **)
|
|
60
|
-
store.list(*, **)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def where(*, **)
|
|
64
|
-
store.where(*, **)
|
|
65
|
-
end
|
|
66
42
|
end
|
|
67
43
|
end
|
|
68
44
|
end
|
|
@@ -2,11 +2,12 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Refresh
|
|
4
4
|
class Orchestrator
|
|
5
|
-
def initialize(worker:, bus:, store_root:, store: nil, detached_spawner: nil)
|
|
5
|
+
def initialize(worker:, bus:, store_root:, store: nil, role: "human", detached_spawner: nil)
|
|
6
6
|
@worker = worker
|
|
7
7
|
@bus = bus
|
|
8
8
|
@store_root = store_root
|
|
9
9
|
@store = store
|
|
10
|
+
@role = role
|
|
10
11
|
@detached_spawner = detached_spawner || default_spawner
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -46,7 +47,7 @@ module Textus
|
|
|
46
47
|
|
|
47
48
|
if thread.alive?
|
|
48
49
|
thread.kill
|
|
49
|
-
store_view = @store ? Textus::
|
|
50
|
+
store_view = @store ? Textus::Application::Context.new(store: @store, role: @role) : nil
|
|
50
51
|
payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
|
|
51
52
|
payload[:store] = store_view if store_view
|
|
52
53
|
@bus.publish(:refresh_detached, **payload)
|
|
@@ -17,6 +17,8 @@ module Textus
|
|
|
17
17
|
|
|
18
18
|
case action
|
|
19
19
|
when "put"
|
|
20
|
+
# Nested proposal "frontmatter" — the meta to write to the accepted
|
|
21
|
+
# target. Not related to the removed intake-handler legacy bridge.
|
|
20
22
|
target_meta = env["_meta"]["frontmatter"] || {}
|
|
21
23
|
target_body = env["body"]
|
|
22
24
|
Composition.writes_put(@ctx).call(target, meta: target_meta, body: target_body)
|
|
@@ -28,9 +30,8 @@ module Textus
|
|
|
28
30
|
|
|
29
31
|
Composition.writes_delete(@ctx).call(pending_key)
|
|
30
32
|
|
|
31
|
-
store_view = Store::View.new(@ctx.store)
|
|
32
33
|
@bus.publish(:accepted,
|
|
33
|
-
store:
|
|
34
|
+
store: @ctx.with_role(@ctx.role),
|
|
34
35
|
key: pending_key,
|
|
35
36
|
target_key: target,
|
|
36
37
|
correlation_id: @ctx.correlation_id)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
1
3
|
module Textus
|
|
2
4
|
module Application
|
|
3
5
|
module Writes
|
|
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.
|
|
4
10
|
class Build
|
|
5
11
|
def initialize(ctx:, bus:)
|
|
6
12
|
@ctx = ctx
|
|
@@ -8,15 +14,101 @@ module Textus
|
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def call(prefix: nil)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
built = []
|
|
18
|
+
manifest.entries.each do |mentry|
|
|
19
|
+
next unless mentry.in_generator_zone?
|
|
20
|
+
next unless mentry.projection || mentry.template
|
|
21
|
+
next if prefix && !mentry.key.start_with?(prefix)
|
|
22
|
+
|
|
23
|
+
built << materialize(mentry)
|
|
24
|
+
end
|
|
25
|
+
published_leaves = publish_leaves(prefix: prefix)
|
|
26
|
+
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def store = @ctx.store
|
|
32
|
+
def manifest = store.manifest
|
|
33
|
+
def root = store.root
|
|
34
|
+
|
|
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
|
+
def materialize(mentry)
|
|
71
|
+
target_path = Builder::Pipeline.run(
|
|
72
|
+
store: store,
|
|
73
|
+
mentry: mentry,
|
|
74
|
+
template_loader: ->(name) { read_template(name) },
|
|
75
|
+
)
|
|
76
|
+
publish_and_fire(mentry, target_path)
|
|
77
|
+
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read_template(name)
|
|
81
|
+
tpl_path = File.join(root, "templates", name)
|
|
82
|
+
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
83
|
+
|
|
84
|
+
File.read(tpl_path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def publish_and_fire(mentry, target_path)
|
|
88
|
+
envelope = store.get(mentry.key)
|
|
89
|
+
repo_root = File.dirname(root)
|
|
90
|
+
|
|
91
|
+
mentry.publish_to.each do |rel|
|
|
92
|
+
target_abs = File.join(repo_root, rel)
|
|
93
|
+
Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: root)
|
|
94
|
+
publish_event(:published,
|
|
95
|
+
key: mentry.key,
|
|
96
|
+
envelope: envelope,
|
|
97
|
+
source: target_path,
|
|
98
|
+
target: target_abs)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
publish_event(:built,
|
|
102
|
+
key: mentry.key,
|
|
103
|
+
envelope: envelope,
|
|
104
|
+
sources: Array(mentry.projection&.fetch("select", nil)).compact)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
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
|
+
@bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
|
|
20
112
|
end
|
|
21
113
|
end
|
|
22
114
|
end
|
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
|
-
|
|
10
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
ctx = context_for(store)
|
|
11
10
|
emit(Textus::Composition.writes_accept(ctx).call(key))
|
|
12
11
|
end
|
|
13
12
|
end
|
|
@@ -11,8 +11,7 @@ module Textus
|
|
|
11
11
|
option :limit, "--limit=N"
|
|
12
12
|
|
|
13
13
|
def call(store)
|
|
14
|
-
|
|
15
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
14
|
+
ctx = context_for(store)
|
|
16
15
|
since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ctx.now)
|
|
17
16
|
rows = Textus::Composition.audit(ctx).call(
|
|
18
17
|
key: key_filter,
|
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
9
|
-
|
|
10
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
ctx = context_for(store)
|
|
11
10
|
rows = Textus::Composition.blame(ctx).call(key: key, limit: limit&.to_i)
|
|
12
11
|
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
13
12
|
end
|
|
@@ -7,8 +7,7 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
def call(store)
|
|
9
9
|
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
|
-
|
|
11
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
10
|
+
ctx = context_for(store)
|
|
12
11
|
emit(Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag))
|
|
13
12
|
end
|
|
14
13
|
end
|
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
option :zone, "--zone=Z"
|
|
7
7
|
|
|
8
8
|
def call(store)
|
|
9
|
-
|
|
10
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
ctx = context_for(store)
|
|
11
10
|
rows = Textus::Composition.freshness(ctx).call(prefix: prefix, zone: zone)
|
|
12
11
|
emit({ "verb" => "freshness", "rows" => rows })
|
|
13
12
|
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
9
|
-
|
|
10
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
ctx = context_for(store)
|
|
11
10
|
result = Textus::Composition.reads_get(ctx).call(key)
|
|
12
11
|
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
13
12
|
|
|
@@ -24,7 +24,7 @@ module Textus
|
|
|
24
24
|
|
|
25
25
|
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
26
26
|
callable = store.registry.rpc_callable(:intake, name)
|
|
27
|
-
view =
|
|
27
|
+
view = Application::Context.new(store: store, role: role)
|
|
28
28
|
|
|
29
29
|
begin
|
|
30
30
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -8,8 +8,7 @@ module Textus
|
|
|
8
8
|
def call(store)
|
|
9
9
|
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
10
10
|
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
11
|
-
|
|
12
|
-
emit(store.mv(old_key, new_key, as: role, dry_run: dry_run || false))
|
|
11
|
+
emit(store.mv(old_key, new_key, as: resolved_role(store), dry_run: dry_run || false))
|
|
13
12
|
end
|
|
14
13
|
end
|
|
15
14
|
end
|
|
@@ -4,8 +4,7 @@ module Textus
|
|
|
4
4
|
class PolicyExplain < Verb
|
|
5
5
|
def call(store)
|
|
6
6
|
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
7
|
-
|
|
8
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
7
|
+
ctx = context_for(store)
|
|
9
8
|
result = Textus::Composition.policy_explain(ctx).call(key: key)
|
|
10
9
|
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
11
10
|
end
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Textus
|
|
|
10
10
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
11
11
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
12
12
|
|
|
13
|
-
role =
|
|
13
|
+
role = resolved_role(store)
|
|
14
14
|
|
|
15
15
|
raw = @stdin.read
|
|
16
16
|
payload =
|
|
@@ -19,7 +19,8 @@ module Textus
|
|
|
19
19
|
result =
|
|
20
20
|
begin
|
|
21
21
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
22
|
-
callable.call(config: { "bytes" => raw },
|
|
22
|
+
callable.call(config: { "bytes" => raw },
|
|
23
|
+
store: Textus::Application::Context.new(store: store, role: role), args: {})
|
|
23
24
|
end
|
|
24
25
|
rescue Timeout::Error
|
|
25
26
|
raise UsageError.new(
|
|
@@ -32,14 +33,14 @@ module Textus
|
|
|
32
33
|
"name" => basename,
|
|
33
34
|
"last_refreshed_at" => Time.now.utc.iso8601,
|
|
34
35
|
"fetched_with" => fetch_name,
|
|
35
|
-
}.merge(result[:_meta] || result["_meta"] ||
|
|
36
|
+
}.merge(result[:_meta] || result["_meta"] || {}),
|
|
36
37
|
"body" => result[:body] || result["body"] || "",
|
|
37
38
|
}
|
|
38
39
|
else
|
|
39
40
|
JSON.parse(raw)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
meta = payload["_meta"] ||
|
|
43
|
+
meta = payload["_meta"] || {}
|
|
43
44
|
body = payload["body"] || ""
|
|
44
45
|
if_etag = payload["if_etag"]
|
|
45
46
|
ctx = Textus::Composition.context(store, role: role)
|
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
9
|
-
|
|
10
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
ctx = context_for(store)
|
|
11
10
|
emit(Textus::Composition.refresh_worker(ctx).run(key))
|
|
12
11
|
end
|
|
13
12
|
end
|
|
@@ -7,8 +7,7 @@ module Textus
|
|
|
7
7
|
option :as_flag, "--as=ROLE"
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
|
-
|
|
11
|
-
ctx = Textus::Composition.context(store, role: role)
|
|
10
|
+
ctx = context_for(store)
|
|
12
11
|
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
13
12
|
emit(result)
|
|
14
13
|
exit(1) unless result["ok"]
|
|
@@ -6,8 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("reject requires a key")
|
|
9
|
-
|
|
10
|
-
emit(store.reject(key, as: role))
|
|
9
|
+
emit(store.reject(key, as: resolved_role(store)))
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
end
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -57,6 +57,20 @@ module Textus
|
|
|
57
57
|
@stdout.puts(JSON.generate(payload))
|
|
58
58
|
exit_code
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
# Resolves the active role for this invocation. Honors the verb's
|
|
62
|
+
# `--as` flag if declared, then TEXTUS_ROLE, then the project default.
|
|
63
|
+
def resolved_role(store)
|
|
64
|
+
flag = respond_to?(:as_flag) ? as_flag : nil
|
|
65
|
+
Role.resolve(flag: flag, env: ENV, root: store.root)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns an Application::Context bound to the resolved role.
|
|
69
|
+
# Convenience for verbs whose only pre-call boilerplate is
|
|
70
|
+
# resolving the role and wrapping it in a context.
|
|
71
|
+
def context_for(store)
|
|
72
|
+
Textus::Composition.context(store, role: resolved_role(store))
|
|
73
|
+
end
|
|
60
74
|
end
|
|
61
75
|
end
|
|
62
76
|
end
|
data/lib/textus/composition.rb
CHANGED
data/lib/textus/doctor.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Textus
|
|
|
52
52
|
|
|
53
53
|
def run_registered_checks(store)
|
|
54
54
|
out = []
|
|
55
|
-
view =
|
|
55
|
+
view = Application::Context.new(store: store, role: "human")
|
|
56
56
|
store.registry.rpc_names(:check).each do |name|
|
|
57
57
|
callable = store.registry.rpc_callable(:check, name)
|
|
58
58
|
begin
|
|
@@ -86,5 +86,7 @@ module Textus
|
|
|
86
86
|
"fix" => fix,
|
|
87
87
|
}
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
private_class_method :run_registered_checks, :fail_issue
|
|
89
91
|
end
|
|
90
92
|
end
|
data/lib/textus/intro.rb
CHANGED
|
@@ -16,11 +16,6 @@ module Textus
|
|
|
16
16
|
"inbox" => "declared external inputs; script-refreshed via actions",
|
|
17
17
|
"review" => "AI proposals awaiting human accept",
|
|
18
18
|
"output" => "build-computed outputs; never hand-edited",
|
|
19
|
-
# legacy 0.9.1 zone names — kept so intro still annotates pre-rename stores
|
|
20
|
-
"canon" => "slow-changing identity; human-only writes",
|
|
21
|
-
"intake" => "declared external inputs; script-refreshed via actions",
|
|
22
|
-
"pending" => "AI proposals awaiting human accept",
|
|
23
|
-
"derived" => "build-computed outputs; never hand-edited",
|
|
24
19
|
}.freeze
|
|
25
20
|
|
|
26
21
|
WRITE_FLOWS = {
|
|
@@ -56,15 +56,32 @@ module Textus
|
|
|
56
56
|
@publish_each.gsub(PUBLISH_EACH_VAR_RE) { vars.fetch(::Regexp.last_match(1)) }
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Signal-based zone-kind predicates: derive the "kind" of a zone from its
|
|
60
|
+
# writable_by signals rather than its literal name. This keeps detection
|
|
61
|
+
# working when users rename the default zones (canon/intake/pending/derived
|
|
62
|
+
# → identity/inbox/review/output, etc.).
|
|
63
|
+
def in_generator_zone?
|
|
64
|
+
zone_writers.include?("build")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def in_proposal_zone?
|
|
68
|
+
zone_writers.include?("ai")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Legacy alias for in_generator_zone?. Retained because internal validation
|
|
72
|
+
# callers (and external tools) read more naturally as `derived?`.
|
|
59
73
|
def derived?
|
|
60
|
-
|
|
61
|
-
writers.include?("build")
|
|
62
|
-
rescue UsageError => e
|
|
63
|
-
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
74
|
+
in_generator_zone?
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
private
|
|
67
78
|
|
|
79
|
+
def zone_writers
|
|
80
|
+
@manifest.zone_writers(@zone)
|
|
81
|
+
rescue UsageError => e
|
|
82
|
+
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
def validate_inject_intro!
|
|
69
86
|
return unless @inject_intro
|
|
70
87
|
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -136,17 +136,6 @@ module Textus
|
|
|
136
136
|
end
|
|
137
137
|
# rubocop:enable Metrics/AbcSize
|
|
138
138
|
|
|
139
|
-
# Validates all declared entry keys; raises UsageError listing all offenders.
|
|
140
|
-
def validate_keys!
|
|
141
|
-
offenders = []
|
|
142
|
-
@entries.each do |entry|
|
|
143
|
-
validate_key!(entry.key)
|
|
144
|
-
rescue UsageError => e
|
|
145
|
-
offenders << e.message
|
|
146
|
-
end
|
|
147
|
-
raise UsageError.new("invalid manifest keys: #{offenders.join("; ")}") unless offenders.empty?
|
|
148
|
-
end
|
|
149
|
-
|
|
150
139
|
def validate_key!(key)
|
|
151
140
|
raise UsageError.new("empty key") if key.nil? || key.empty?
|
|
152
141
|
|
data/lib/textus/projection.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Textus
|
|
|
40
40
|
def apply_reducer(rows)
|
|
41
41
|
name = @spec["reduce"] or return rows
|
|
42
42
|
callable = @store.registry.rpc_callable(:reduce, name)
|
|
43
|
-
view =
|
|
43
|
+
view = Application::Context.new(store: @store, role: "human")
|
|
44
44
|
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
45
45
|
callable.call(store: view, rows: rows, config: @spec["reduce_config"] || {})
|
|
46
46
|
end
|
data/lib/textus/refresh.rb
CHANGED
|
@@ -15,8 +15,7 @@ module Textus
|
|
|
15
15
|
def self.normalize_action_result(res, format:)
|
|
16
16
|
res = res.transform_keys(&:to_s) if res.is_a?(Hash)
|
|
17
17
|
res ||= {}
|
|
18
|
-
|
|
19
|
-
meta_val = res["_meta"] || res["frontmatter"]
|
|
18
|
+
meta_val = res["_meta"]
|
|
20
19
|
body = res["body"]
|
|
21
20
|
content = res["content"]
|
|
22
21
|
|
data/lib/textus/store/writer.rb
CHANGED
|
@@ -152,7 +152,7 @@ module Textus
|
|
|
152
152
|
raise ProposalError.new("only human role can reject proposals; got '#{as}'") unless as == "human"
|
|
153
153
|
|
|
154
154
|
mentry, = @store.manifest.resolve(pending_key)
|
|
155
|
-
raise ProposalError.new("reject: '#{pending_key}' is not a
|
|
155
|
+
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})") unless mentry.in_proposal_zone?
|
|
156
156
|
|
|
157
157
|
env = @store.get(pending_key)
|
|
158
158
|
proposal = env.dig("_meta", "proposal") or
|
data/lib/textus/store.rb
CHANGED
data/lib/textus/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -123,7 +123,6 @@ files:
|
|
|
123
123
|
- lib/textus/application/writes/delete.rb
|
|
124
124
|
- lib/textus/application/writes/publish.rb
|
|
125
125
|
- lib/textus/application/writes/put.rb
|
|
126
|
-
- lib/textus/builder.rb
|
|
127
126
|
- lib/textus/builder/pipeline.rb
|
|
128
127
|
- lib/textus/builder/renderer.rb
|
|
129
128
|
- lib/textus/builder/renderer/json.rb
|
|
@@ -226,7 +225,6 @@ files:
|
|
|
226
225
|
- lib/textus/mustache.rb
|
|
227
226
|
- lib/textus/projection.rb
|
|
228
227
|
- lib/textus/proposal.rb
|
|
229
|
-
- lib/textus/publisher.rb
|
|
230
228
|
- lib/textus/refresh.rb
|
|
231
229
|
- lib/textus/role.rb
|
|
232
230
|
- lib/textus/schema.rb
|
|
@@ -237,7 +235,6 @@ files:
|
|
|
237
235
|
- lib/textus/store/reader.rb
|
|
238
236
|
- lib/textus/store/staleness.rb
|
|
239
237
|
- lib/textus/store/validator.rb
|
|
240
|
-
- lib/textus/store/view.rb
|
|
241
238
|
- lib/textus/store/writer.rb
|
|
242
239
|
- lib/textus/version.rb
|
|
243
240
|
homepage: https://github.com/patrick204nqh/textus
|
data/lib/textus/builder.rb
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
# As of 0.9.1, Textus::Application::Writes::Build is the preferred public
|
|
4
|
-
# entry point. This class remains as the implementation home of materialization
|
|
5
|
-
# and projection logic; full extraction is deferred to 0.10.0.
|
|
6
|
-
module Textus
|
|
7
|
-
class Builder
|
|
8
|
-
def initialize(store)
|
|
9
|
-
@store = store
|
|
10
|
-
@manifest = store.manifest
|
|
11
|
-
@root = store.root
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def build(prefix: nil)
|
|
15
|
-
built = []
|
|
16
|
-
@manifest.entries.each do |mentry|
|
|
17
|
-
next unless derived_zone?(mentry)
|
|
18
|
-
next unless mentry.projection || mentry.template
|
|
19
|
-
next if prefix && !mentry.key.start_with?(prefix)
|
|
20
|
-
|
|
21
|
-
result = materialize(mentry)
|
|
22
|
-
built << result
|
|
23
|
-
end
|
|
24
|
-
published_leaves = publish_leaves(prefix: prefix)
|
|
25
|
-
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def publish_leaves(prefix: nil)
|
|
31
|
-
repo_root = File.dirname(@root)
|
|
32
|
-
out = []
|
|
33
|
-
@manifest.entries.each do |mentry|
|
|
34
|
-
next unless mentry.nested && mentry.publish_each
|
|
35
|
-
next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
|
|
36
|
-
|
|
37
|
-
@manifest.enumerate(prefix: mentry.key).each do |row|
|
|
38
|
-
next unless row[:manifest_entry].equal?(mentry)
|
|
39
|
-
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
40
|
-
|
|
41
|
-
out << publish_leaf(mentry, row, repo_root)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
out
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def publish_leaf(mentry, row, repo_root)
|
|
48
|
-
target_rel = mentry.publish_target_for(row[:key])
|
|
49
|
-
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
50
|
-
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
51
|
-
raise PublishError.new(
|
|
52
|
-
"entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
|
|
57
|
-
@store.fire_event(:published, key: row[:key], envelope: @store.get(row[:key]),
|
|
58
|
-
source: row[:path], target: target_abs)
|
|
59
|
-
{ "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def derived_zone?(mentry)
|
|
63
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
64
|
-
writers.include?("build")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def materialize(mentry)
|
|
68
|
-
target_path = Pipeline.run(
|
|
69
|
-
store: @store,
|
|
70
|
-
mentry: mentry,
|
|
71
|
-
template_loader: ->(name) { read_template(name) },
|
|
72
|
-
)
|
|
73
|
-
publish_and_fire(mentry, target_path)
|
|
74
|
-
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def read_template(name)
|
|
78
|
-
tpl_path = File.join(@root, "templates", name)
|
|
79
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
80
|
-
|
|
81
|
-
File.read(tpl_path)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def publish_and_fire(mentry, target_path)
|
|
85
|
-
envelope = @store.get(mentry.key)
|
|
86
|
-
repo_root = File.dirname(@root)
|
|
87
|
-
|
|
88
|
-
mentry.publish_to.each do |rel|
|
|
89
|
-
target_abs = File.join(repo_root, rel)
|
|
90
|
-
Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: @root)
|
|
91
|
-
@store.fire_event(:published, key: mentry.key, envelope: envelope,
|
|
92
|
-
source: target_path, target: target_abs)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
@store.fire_event(:built, key: mentry.key, envelope: envelope,
|
|
96
|
-
sources: Array(mentry.projection&.fetch("select", nil)).compact)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
data/lib/textus/publisher.rb
DELETED
data/lib/textus/store/view.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Store
|
|
3
|
-
# Deprecated as of 0.9.1: use Textus::Application::Context instead.
|
|
4
|
-
# Removal scheduled for 0.10.0.
|
|
5
|
-
class View
|
|
6
|
-
def self.new(store, writable: false, as: nil)
|
|
7
|
-
unless @warned_once
|
|
8
|
-
warn "[textus] Store::View is deprecated; use Application::Context (will be removed in 0.10.0)"
|
|
9
|
-
@warned_once = true
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
raise UsageError.new("writable Store::View requires an as: role") if writable && (as.nil? || as.to_s.empty?)
|
|
13
|
-
|
|
14
|
-
Textus::Application::Context.new(store: store, role: as || "human")
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|