textus 0.51.0 → 0.53.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 +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +13 -13
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +19 -11
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +17 -63
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/role.rb +1 -1
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -4
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +133 -107
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -69
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Background
|
|
3
|
+
class Worker
|
|
4
|
+
Summary = Struct.new(:completed, :failed, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
def self.for(container:, queue:)
|
|
7
|
+
new(queue: queue, container: container,
|
|
8
|
+
lease_ttl: container.manifest.data.worker_config[:lease_ttl])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(queue:, container:, lease_ttl: 60)
|
|
12
|
+
@queue = queue
|
|
13
|
+
@container = container
|
|
14
|
+
@lease_ttl = lease_ttl
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def drain(worker_id: "drain-#{Process.pid}")
|
|
18
|
+
completed = 0
|
|
19
|
+
failed = 0
|
|
20
|
+
loop do
|
|
21
|
+
leased = @queue.lease(worker_id: worker_id, lease_ttl: @lease_ttl)
|
|
22
|
+
break unless leased
|
|
23
|
+
|
|
24
|
+
case run_one(leased)
|
|
25
|
+
when :completed then completed += 1
|
|
26
|
+
when :dead_lettered then failed += 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
Summary.new(completed: completed, failed: failed)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def drain_pool(pool: 4)
|
|
33
|
+
summaries = []
|
|
34
|
+
mutex = Mutex.new
|
|
35
|
+
threads = Array.new(pool) do |i|
|
|
36
|
+
Thread.new do
|
|
37
|
+
s = drain(worker_id: "pool-#{Process.pid}-#{i}")
|
|
38
|
+
mutex.synchronize { summaries << s }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
threads.each(&:join)
|
|
42
|
+
Summary.new(completed: summaries.sum(&:completed), failed: summaries.sum(&:failed))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def run_one(leased)
|
|
48
|
+
job = leased.job
|
|
49
|
+
klass = Textus::Background::Job.fetch(job.type)
|
|
50
|
+
action = if klass.instance_method(:initialize).parameters.any?
|
|
51
|
+
klass.new(**job.args.transform_keys(&:to_sym))
|
|
52
|
+
else
|
|
53
|
+
klass.new
|
|
54
|
+
end
|
|
55
|
+
call = Textus::Call.build(
|
|
56
|
+
role: job.enqueued_by || Textus::Role::AUTOMATION,
|
|
57
|
+
correlation_id: SecureRandom.uuid,
|
|
58
|
+
)
|
|
59
|
+
action.call(container: @container, call: call)
|
|
60
|
+
@queue.ack(leased)
|
|
61
|
+
:completed
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
@queue.fail(leased, error: e.message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/textus/boot.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
# Read-only "what's in this store and how do I use it" envelope.
|
|
3
3
|
# A single call gives an agent the working model of a textus-managed
|
|
4
|
-
# project:
|
|
4
|
+
# project: lanes and their write authority, entries and their flags,
|
|
5
5
|
# registered hooks, write flows, and the CLI verb catalog.
|
|
6
6
|
#
|
|
7
7
|
# Boot is side-effect-free.
|
|
@@ -10,25 +10,25 @@ module Textus
|
|
|
10
10
|
|
|
11
11
|
# Per-capability write-flow templates. Each lambda receives the user-facing
|
|
12
12
|
# role name and the manifest, and returns guidance for that verb with the
|
|
13
|
-
# live
|
|
13
|
+
# live lane named by kind (ADR 0034). A role holding multiple verbs gets one
|
|
14
14
|
# joined string; roles whose verbs have no template are omitted.
|
|
15
15
|
WRITE_FLOW_TEMPLATES = {
|
|
16
16
|
author: lambda do |name, manifest|
|
|
17
|
-
"edit files in #{
|
|
17
|
+
"edit files in #{lane_label(manifest, :canon, "your canon lane")}, " \
|
|
18
18
|
"then 'textus put KEY --as=#{name}'"
|
|
19
19
|
end,
|
|
20
20
|
keep: lambda do |name, manifest|
|
|
21
|
-
"keep durable notes in #{
|
|
21
|
+
"keep durable notes in #{lane_label(manifest, :workspace, "your workspace")}: " \
|
|
22
22
|
"'textus put KEY --as=#{name}' (no accept needed)"
|
|
23
23
|
end,
|
|
24
24
|
propose: lambda do |name, manifest|
|
|
25
25
|
authority = manifest.policy.roles_with_capability("author").first || "the author-holder"
|
|
26
|
-
"propose changes by writing #{manifest.policy.
|
|
26
|
+
"propose changes by writing #{manifest.policy.queue_lane}.* entries with --as=#{name} " \
|
|
27
27
|
"and a 'proposal:' frontmatter block; the #{authority} role runs 'textus accept' to apply"
|
|
28
28
|
end,
|
|
29
|
-
|
|
30
|
-
machine =
|
|
31
|
-
"'textus
|
|
29
|
+
converge: lambda do |_name, manifest|
|
|
30
|
+
machine = lane_label(manifest, :machine, "machine")
|
|
31
|
+
"'textus drain' materializes derived #{machine} entries from their sources and " \
|
|
32
32
|
"refreshes stale intake #{machine} entries from their declared source; " \
|
|
33
33
|
"derived files are never hand-edited (reactive on canon writes, or a full pass on demand)"
|
|
34
34
|
end,
|
|
@@ -44,17 +44,17 @@ module Textus
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Human-readable name(s) for the live
|
|
47
|
+
# Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
|
|
48
48
|
# when the manifest declares none. Lets write-flow guidance name the live
|
|
49
|
-
#
|
|
50
|
-
def self.
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
# lane by kind instead of a hardcoded instance name (ADR 0034).
|
|
50
|
+
def self.lane_label(manifest, kind, fallback)
|
|
51
|
+
lanes = manifest.policy.lanes_of_kind(kind)
|
|
52
|
+
lanes.empty? ? fallback : lanes.join(", ")
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Static, store-independent parts of the agent-facing protocol. The
|
|
56
56
|
# `recipes` and `role_resolution` blocks are derived per-manifest in
|
|
57
|
-
# agent_protocol(...) because
|
|
57
|
+
# agent_protocol(...) because lane and role names are user-configurable.
|
|
58
58
|
AGENT_PROTOCOL_TEMPLATE = {
|
|
59
59
|
"envelope_shape" => {
|
|
60
60
|
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
@@ -84,23 +84,23 @@ module Textus
|
|
|
84
84
|
{ "name" => "put" },
|
|
85
85
|
{ "name" => "propose" },
|
|
86
86
|
{ "name" => "accept" },
|
|
87
|
+
{ "name" => "enqueue" },
|
|
87
88
|
{ "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
|
|
88
|
-
{ "name" => "
|
|
89
|
+
{ "name" => "drain" },
|
|
89
90
|
{ "name" => "audit" },
|
|
90
91
|
{ "name" => "blame" },
|
|
91
92
|
{ "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
|
|
92
93
|
{ "name" => "doctor" },
|
|
93
|
-
{ "name" => "
|
|
94
|
+
{ "name" => "jobs" },
|
|
94
95
|
{ "name" => "pulse" },
|
|
95
|
-
{ "name" => "capabilities" },
|
|
96
96
|
].freeze
|
|
97
97
|
|
|
98
98
|
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
99
99
|
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
100
100
|
def self.contract_summaries
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
Textus::Action::VERBS.values
|
|
102
|
+
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
103
|
+
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
@@ -117,11 +117,15 @@ module Textus
|
|
|
117
117
|
def self.agent_quickstart(manifest, audit_log)
|
|
118
118
|
agent_role = manifest.policy.proposer_role
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc|
|
|
121
|
+
next unless agent_role
|
|
122
|
+
|
|
123
|
+
verb = manifest.policy.verb_for_lane(lane_name)
|
|
124
|
+
writers = manifest.policy.roles_with_capability(verb)
|
|
125
|
+
acc << lane_name if writers.include?(agent_role)
|
|
122
126
|
end
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
125
129
|
|
|
126
130
|
{
|
|
127
131
|
# Both verb lists derive from the MCP catalog (ADR 0056, ADR 0057): the
|
|
@@ -131,11 +135,11 @@ module Textus
|
|
|
131
135
|
# internal scan, ADR 0085) nor omit one it can
|
|
132
136
|
# (schema_show/rules); write_verbs drops the old `put KEY --as=… --stdin` CLI
|
|
133
137
|
# framing (role is connection-resolved over MCP; there is no stdin).
|
|
134
|
-
#
|
|
135
|
-
"read_verbs" => Textus::MCP::Catalog.read_verbs,
|
|
136
|
-
"write_verbs" => agent_role ? Textus::MCP::Catalog.write_verbs : [],
|
|
137
|
-
"
|
|
138
|
-
"
|
|
138
|
+
# writable_lanes / propose_lane below carry the agent's write authority.
|
|
139
|
+
"read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs,
|
|
140
|
+
"write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [],
|
|
141
|
+
"writable_lanes" => writable_lanes,
|
|
142
|
+
"propose_lane" => propose_lane,
|
|
139
143
|
"latest_seq" => audit_log.latest_seq,
|
|
140
144
|
}
|
|
141
145
|
end
|
|
@@ -145,13 +149,13 @@ module Textus
|
|
|
145
149
|
# `textus get KEY`, MCP as the `get` tool) or is a plain materialize step. This
|
|
146
150
|
# keeps shell lines out of the surface an MCP agent reads.
|
|
147
151
|
def self.recipes(manifest)
|
|
148
|
-
queue = manifest.policy.
|
|
149
|
-
feeds =
|
|
152
|
+
queue = manifest.policy.queue_lane
|
|
153
|
+
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
150
154
|
{
|
|
151
155
|
"read" => {
|
|
152
156
|
"purpose" => "find and read an entry",
|
|
153
157
|
"steps" => [
|
|
154
|
-
"list (
|
|
158
|
+
"list (lane:, prefix:) — discover keys without reading bodies",
|
|
155
159
|
"get KEY — returns the entry envelope",
|
|
156
160
|
],
|
|
157
161
|
},
|
|
@@ -166,17 +170,17 @@ module Textus
|
|
|
166
170
|
"propose" => {
|
|
167
171
|
"purpose" => "agent suggests a change for human review",
|
|
168
172
|
"agent_steps" => [
|
|
169
|
-
"propose KEY — writes the change into the #{queue}
|
|
173
|
+
"propose KEY — writes the change into the #{queue} lane for review",
|
|
170
174
|
],
|
|
171
175
|
"human_steps" => [
|
|
172
|
-
"accept #{queue}.KEY — promotes the proposal into its target
|
|
176
|
+
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
173
177
|
],
|
|
174
178
|
},
|
|
175
|
-
"
|
|
179
|
+
"drain" => {
|
|
176
180
|
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
177
181
|
"steps" => [
|
|
178
182
|
"pulse — its `stale` list names entries past their ttl",
|
|
179
|
-
"
|
|
183
|
+
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
180
184
|
],
|
|
181
185
|
},
|
|
182
186
|
}
|
|
@@ -202,7 +206,7 @@ module Textus
|
|
|
202
206
|
return {
|
|
203
207
|
"protocol" => PROTOCOL_ID,
|
|
204
208
|
"store_root" => container.root,
|
|
205
|
-
"
|
|
209
|
+
"lanes" => lanes_for(manifest),
|
|
206
210
|
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
207
211
|
"contract_etag" => etag,
|
|
208
212
|
}
|
|
@@ -211,7 +215,7 @@ module Textus
|
|
|
211
215
|
{
|
|
212
216
|
"protocol" => PROTOCOL_ID,
|
|
213
217
|
"store_root" => container.root,
|
|
214
|
-
"
|
|
218
|
+
"lanes" => lanes_for(manifest),
|
|
215
219
|
"entries" => entries_for(manifest),
|
|
216
220
|
"hooks" => hooks_for_container(container),
|
|
217
221
|
"write_flows" => write_flows_for(manifest),
|
|
@@ -219,16 +223,17 @@ module Textus
|
|
|
219
223
|
"agent_protocol" => agent_protocol(manifest),
|
|
220
224
|
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
221
225
|
"contract_etag" => etag,
|
|
222
|
-
"docs" => { "spec" => "SPEC.md", "example" => "
|
|
226
|
+
"docs" => { "spec" => "SPEC.md", "example" => ".textus/" },
|
|
223
227
|
}
|
|
224
228
|
end
|
|
225
229
|
|
|
226
|
-
def self.
|
|
227
|
-
manifest.data.
|
|
228
|
-
|
|
230
|
+
def self.lanes_for(manifest)
|
|
231
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
232
|
+
verb = manifest.policy.verb_for_lane(name)
|
|
233
|
+
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
229
234
|
kind = manifest.policy.declared_kind(name)
|
|
230
235
|
row["kind"] = kind.to_s if kind
|
|
231
|
-
purpose = manifest.data.
|
|
236
|
+
purpose = manifest.data.lane_descs[name]
|
|
232
237
|
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
233
238
|
row
|
|
234
239
|
end
|
|
@@ -239,7 +244,7 @@ module Textus
|
|
|
239
244
|
derived = e.derived?
|
|
240
245
|
{
|
|
241
246
|
"key" => e.key,
|
|
242
|
-
"
|
|
247
|
+
"lane" => e.lane,
|
|
243
248
|
"schema" => e.schema,
|
|
244
249
|
"nested" => e.is_a?(Textus::Manifest::Entry::Nested),
|
|
245
250
|
"owner" => e.owner,
|
|
@@ -252,16 +257,21 @@ module Textus
|
|
|
252
257
|
end
|
|
253
258
|
|
|
254
259
|
def self.hooks_for_container(container)
|
|
255
|
-
hooks_for_container_internal(
|
|
260
|
+
hooks_for_container_internal(steps: container.steps)
|
|
256
261
|
end
|
|
257
262
|
|
|
258
|
-
def self.hooks_for_container_internal(
|
|
263
|
+
def self.hooks_for_container_internal(steps:)
|
|
259
264
|
sections = {}
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
rpc_kind_map = {
|
|
266
|
+
resolve_handler: :fetch,
|
|
267
|
+
transform_rows: :transform,
|
|
268
|
+
validate: :validate,
|
|
269
|
+
}
|
|
270
|
+
Step::Catalog::RPC.each_key do |event|
|
|
271
|
+
sections[event.to_s] = steps.names(rpc_kind_map.fetch(event)).map(&:to_s).sort
|
|
262
272
|
end
|
|
263
|
-
|
|
264
|
-
sections[event.to_s] =
|
|
273
|
+
Step::Catalog::PUBSUB.each_key do |event|
|
|
274
|
+
sections[event.to_s] = steps.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
265
275
|
end
|
|
266
276
|
sections
|
|
267
277
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Command
|
|
3
|
+
Get = Data.define(:key, :role)
|
|
4
|
+
Put = Data.define(:key, :meta, :body, :content, :if_etag, :role)
|
|
5
|
+
Propose = Data.define(:key, :meta, :body, :content, :role, :pending_key) do
|
|
6
|
+
def initialize(key:, role:, meta: nil, body: nil, content: nil, pending_key: nil)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
KeyDelete = Data.define(:key, :if_etag, :role)
|
|
11
|
+
KeyMv = Data.define(:old_key, :new_key, :dry_run, :role)
|
|
12
|
+
Accept = Data.define(:pending_key, :role)
|
|
13
|
+
Reject = Data.define(:pending_key, :role)
|
|
14
|
+
Enqueue = Data.define(:type, :args, :role)
|
|
15
|
+
List = Data.define(:prefix, :lane, :role)
|
|
16
|
+
Where = Data.define(:key, :role)
|
|
17
|
+
Uid = Data.define(:key, :role)
|
|
18
|
+
Blame = Data.define(:key, :limit, :role)
|
|
19
|
+
Audit = Data.define(:key, :lane, :verb, :since, :seq_since, :correlation_id, :limit, :role)
|
|
20
|
+
Deps = Data.define(:key, :role)
|
|
21
|
+
Rdeps = Data.define(:key, :role)
|
|
22
|
+
Pulse = Data.define(:since, :role)
|
|
23
|
+
RuleExplain = Data.define(:key, :detail, :role)
|
|
24
|
+
RuleList = Data.define(:role)
|
|
25
|
+
RuleLint = Data.define(:candidate_yaml, :role)
|
|
26
|
+
Published = Data.define(:role)
|
|
27
|
+
SchemaShow = Data.define(:key, :role)
|
|
28
|
+
Doctor = Data.define(:checks, :role)
|
|
29
|
+
Boot = Data.define(:lean, :role)
|
|
30
|
+
Jobs = Data.define(:state, :action, :job_id, :role)
|
|
31
|
+
DataMv = Data.define(:from, :to, :dry_run, :role)
|
|
32
|
+
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run, :role)
|
|
33
|
+
KeyDeletePrefix = Data.define(:prefix, :dry_run, :role)
|
|
34
|
+
Drain = Data.define(:prefix, :lane, :role)
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/textus/container.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
require "time"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Freshness
|
|
6
6
|
# The single currency evaluator (ADR 0099). Answers "is the stored data
|
|
7
7
|
# stale relative to its source?" for every produce-method:
|
|
8
|
-
# - intake (source.from:
|
|
8
|
+
# - intake (source.from: fetch) -> AGE signal: now - basis > source.ttl,
|
|
9
9
|
# basis = _meta.last_fetched_at (else file mtime). No ttl -> :no_policy
|
|
10
10
|
# (skipped — a cadence-less handler is not auto-re-pulled).
|
|
11
|
-
# -
|
|
11
|
+
# - external -> DRIFT signal: a source changed since generated.at
|
|
12
12
|
# (surfaced by the doctor generator_drift check; derived entries annotate
|
|
13
|
-
# fresh at read time because
|
|
14
|
-
# Replaces
|
|
13
|
+
# fresh at read time because converge runs them reactively).
|
|
14
|
+
# Replaces Core::IntakeStaleness and Core::Staleness::GeneratorCheck and
|
|
15
15
|
# the inline copies in Read::Get / Read::Freshness.
|
|
16
16
|
class Evaluator
|
|
17
17
|
def initialize(manifest:, file_stat:, clock:)
|
|
@@ -32,11 +32,11 @@ module Textus
|
|
|
32
32
|
Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Keys of intake entries past their source.ttl — the
|
|
36
|
-
# scope (replaces
|
|
35
|
+
# Keys of intake entries past their source.ttl — the converge produce
|
|
36
|
+
# scope (replaces Core::IntakeStaleness#call). A ttl-less intake entry
|
|
37
37
|
# is :no_policy and skipped; a never-recorded one (with a ttl) is stale.
|
|
38
|
-
def stale_intake_keys(prefix: nil,
|
|
39
|
-
@manifest.data.entries.select { |m| due?(m, prefix: prefix,
|
|
38
|
+
def stale_intake_keys(prefix: nil, lane: nil)
|
|
39
|
+
@manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Age basis as a Time (or nil): _meta.last_fetched_at when present, else
|
|
@@ -62,9 +62,9 @@ module Textus
|
|
|
62
62
|
|
|
63
63
|
def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
|
|
64
64
|
|
|
65
|
-
def due?(mentry, prefix:,
|
|
65
|
+
def due?(mentry, prefix:, lane:)
|
|
66
66
|
return false unless mentry.intake?
|
|
67
|
-
return false if
|
|
67
|
+
return false if lane && mentry.lane != lane
|
|
68
68
|
return false if prefix && !mentry.key.start_with?(prefix)
|
|
69
69
|
|
|
70
70
|
ttl = mentry.source.ttl_seconds
|
|
@@ -92,7 +92,7 @@ module Textus
|
|
|
92
92
|
|
|
93
93
|
# --- generator drift (lifted from Staleness::GeneratorCheck) ---
|
|
94
94
|
|
|
95
|
-
def drift_applicable?(mentry) = mentry.
|
|
95
|
+
def drift_applicable?(mentry) = mentry.external?
|
|
96
96
|
|
|
97
97
|
def drift_reason(mentry, path)
|
|
98
98
|
return "derived entry has never been generated" unless @file_stat.exists?(path)
|
|
@@ -158,7 +158,7 @@ module Textus
|
|
|
158
158
|
def file?(fpath) = !@file_stat.directory?(fpath) && @file_stat.exists?(fpath)
|
|
159
159
|
|
|
160
160
|
def drift_row(mentry, path, reason)
|
|
161
|
-
{ "key" => mentry.key, "path" => path, "generator" => mentry.
|
|
161
|
+
{ "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Freshness
|
|
6
6
|
# Value object describing the freshness annotation attached to an Envelope
|
|
7
|
-
# after a currency evaluation (ADR 0099 — was
|
|
7
|
+
# after a currency evaluation (ADR 0099 — was Core::Freshness).
|
|
8
8
|
#
|
|
9
9
|
# Note on wire format: `#to_h_for_wire` is intentionally narrower than the
|
|
10
10
|
# full field set. It emits the legacy keys ("stale", "stale_reason",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Currency — "is the stored data stale relative to its source?" (ADR 0099).
|
|
6
6
|
# The home of the single Freshness evaluator and its Verdict value object.
|
|
7
|
-
# Distinct from
|
|
7
|
+
# Distinct from Core::Retention (GC dueness, Q2).
|
|
8
8
|
module Freshness
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
require "time"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
module Retention
|
|
6
6
|
# Retention sweep reporter (ADR 0093/0099). Which entries are past their
|
|
7
7
|
# `retention:` ttl and the destructive action that applies. Age basis: file
|
|
8
|
-
# mtime. Only drop/archive. Renamed off the
|
|
9
|
-
#
|
|
8
|
+
# mtime. Only drop/archive. Renamed off the Core::Retention vs
|
|
9
|
+
# Manifest::Policy::Retention collision (ADR 0099).
|
|
10
10
|
class Sweep
|
|
11
11
|
def self.expired?(ttl_seconds:, mtime:, now:)
|
|
12
12
|
return false if ttl_seconds.nil? || mtime.nil?
|
|
@@ -20,16 +20,16 @@ module Textus
|
|
|
20
20
|
@clock = clock
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def call(prefix: nil,
|
|
23
|
+
def call(prefix: nil, lane: nil)
|
|
24
24
|
@manifest.data.entries
|
|
25
|
-
.select { |m| matches?(m, prefix: prefix,
|
|
25
|
+
.select { |m| matches?(m, prefix: prefix, lane: lane) }
|
|
26
26
|
.flat_map { |m| rows_for(m) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
def matches?(mentry, prefix:,
|
|
32
|
-
return false if
|
|
31
|
+
def matches?(mentry, prefix:, lane:)
|
|
32
|
+
return false if lane && mentry.lane != lane
|
|
33
33
|
return false if prefix && !Textus::Key::Matching.matches_prefix?(
|
|
34
34
|
mentry.key, prefix, nested: mentry.is_a?(Textus::Manifest::Entry::Nested)
|
|
35
35
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Retention — "is the entry old enough to retire?" (Q2, ADR 0093/0099).
|
|
6
6
|
# GC dueness, orthogonal to Freshness (content currency). The reporter is
|
|
7
|
-
#
|
|
7
|
+
# Core::Retention::Sweep; the manifest rule policy is Manifest::Policy::Retention.
|
|
8
8
|
module Retention
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Core
|
|
5
5
|
# Pure value object representing a published-file sentinel. Holds the
|
|
6
6
|
# recorded target path, source path, sha256 checksum, and publish mode.
|
|
7
7
|
# Has no filesystem I/O — path layout and persistence live in
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
# verb reported.
|
|
9
9
|
class GeneratorDrift < Check
|
|
10
10
|
def call
|
|
11
|
-
gen = Textus::
|
|
11
|
+
gen = Textus::Core::Freshness::Evaluator.new(
|
|
12
12
|
manifest: manifest,
|
|
13
13
|
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
14
14
|
clock: Textus::Ports::Clock.new,
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
"level" => "warning",
|
|
20
20
|
"subject" => row["key"],
|
|
21
21
|
"message" => row["reason"],
|
|
22
|
-
"fix" => "rematerialize the entry: `textus
|
|
22
|
+
"fix" => "rematerialize the entry: `textus drain`",
|
|
23
23
|
}
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
# For every entry with a fetch handler, look up its handler_permit policy
|
|
5
|
+
# (if any) and verify the declared handler is permitted. Emits a failure
|
|
6
|
+
# when the handler is rejected by policy.
|
|
7
|
+
class HandlerPermit < Check
|
|
8
|
+
def call
|
|
9
|
+
out = []
|
|
10
|
+
manifest.data.entries.each do |mentry|
|
|
11
|
+
next unless mentry.intake?
|
|
12
|
+
|
|
13
|
+
handler = mentry.handler
|
|
14
|
+
|
|
15
|
+
permit = manifest.rules.for(mentry.key).handler_permit
|
|
16
|
+
next if permit.nil?
|
|
17
|
+
next if permit.permits?(handler)
|
|
18
|
+
|
|
19
|
+
out << {
|
|
20
|
+
"code" => "policy.handler_not_permitted",
|
|
21
|
+
"level" => "error",
|
|
22
|
+
"subject" => mentry.key,
|
|
23
|
+
"message" => "entry '#{mentry.key}' declares source.handler='#{handler}' but " \
|
|
24
|
+
"handler_permit allows only: #{permit.handlers.join(", ")}",
|
|
25
|
+
"fix" => "change handler to one of [#{permit.handlers.join(", ")}] or " \
|
|
26
|
+
"extend handler_permit in .textus/manifest.yaml",
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -4,26 +4,19 @@ module Textus
|
|
|
4
4
|
class Hooks < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
dir = File.join(root, "
|
|
7
|
+
dir = File.join(root, "steps")
|
|
8
8
|
return out unless File.directory?(dir)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"code" => "hook.load_failed",
|
|
21
|
-
"level" => "error",
|
|
22
|
-
"subject" => File.basename(f),
|
|
23
|
-
"message" => "#{e.class}: #{e.message}",
|
|
24
|
-
"fix" => "open #{f} and fix the syntax/load error",
|
|
25
|
-
}
|
|
26
|
-
end
|
|
10
|
+
Textus::Step::Loader.new(registry: Textus::Step::RegistryStore.new).load_dir(dir)
|
|
11
|
+
out
|
|
12
|
+
rescue Textus::UsageError => e
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "step.load_failed",
|
|
15
|
+
"level" => "error",
|
|
16
|
+
"subject" => "steps",
|
|
17
|
+
"message" => e.message,
|
|
18
|
+
"fix" => "open the named step file under .textus/steps/ and fix the error",
|
|
19
|
+
}
|
|
27
20
|
out
|
|
28
21
|
end
|
|
29
22
|
end
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
next unless entry.nested?
|
|
9
9
|
next if entry.publish_mode.keyless? # publish_tree files are opaque payload, never keys (ADR 0047)
|
|
10
10
|
|
|
11
|
-
base = File.join(root,
|
|
11
|
+
base = File.join(root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
12
12
|
next unless File.directory?(base)
|
|
13
13
|
|
|
14
14
|
check_all_paths(entry, base, out)
|