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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
4
|
# Garbage collection (ADR 0093). A glob-matched rule slot: when an entry
|
|
5
5
|
# ages past `ttl`, retire it. Destructive only — runs on the full
|
|
6
|
-
# `
|
|
6
|
+
# `converge` pass, never on a write (ADR 0079's invariant). Orthogonal to
|
|
7
7
|
# production (`source:`): an intake entry can re-pull hourly AND archive
|
|
8
8
|
# after 90 days. `warn`/`refresh` are gone (refresh is implied by an
|
|
9
9
|
# intake source; warn never fired after ADR 0089's pure-read get).
|
|
@@ -21,7 +21,7 @@ module Textus
|
|
|
21
21
|
raise Textus::BadManifest.new("retention action must be one of #{ACTIONS.join("|")}, got #{raw["action"].inspect}")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def ttl_seconds = Textus::
|
|
24
|
+
def ttl_seconds = Textus::Core::Duration.seconds(@ttl)
|
|
25
25
|
def destructive? = true
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
4
|
# An entry's data-acquisition declaration (ADR 0094). `source:` says HOW the
|
|
5
5
|
# entry's data is acquired; rendering is a publish concern, so there are no
|
|
6
6
|
# template/render fields here. `from` is the acquire + staleness axis:
|
|
7
|
-
# from:
|
|
8
|
-
# from:
|
|
9
|
-
# from:
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# from: derive -> derived (internal projection; observable -> rdeps staleness)
|
|
8
|
+
# from: fetch -> intake (external fetch; unobservable -> ttl staleness)
|
|
9
|
+
# from: external -> external (out-of-band runner; staleness only, textus never runs it)
|
|
10
|
+
# Materialization is async-only (job-queue model): a write enqueues a
|
|
11
|
+
# `materialize` job, converged by a worker. There is no per-entry write
|
|
12
|
+
# trigger knob.
|
|
12
13
|
class Source
|
|
13
|
-
FROMS
|
|
14
|
-
STRATEGIES = %w[async sync].freeze
|
|
14
|
+
FROMS = %w[fetch derive external].freeze
|
|
15
15
|
|
|
16
16
|
attr_reader :from, :handler, :config, :command, :sources
|
|
17
17
|
|
|
@@ -21,26 +21,25 @@ module Textus
|
|
|
21
21
|
raise Textus::BadManifest.new("source.from must be one of #{FROMS.join("|")}, got #{raw["from"].inspect}")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
@on_write = (raw["on_write"] || "async").to_s
|
|
25
|
-
unless STRATEGIES.include?(@on_write)
|
|
26
|
-
raise Textus::BadManifest.new("source.on_write must be one of #{STRATEGIES.join("/")}, got #{@on_write.inspect}")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
24
|
@ttl = raw["ttl"]
|
|
30
25
|
@projection = {}
|
|
31
26
|
|
|
32
27
|
case @from
|
|
33
|
-
when "
|
|
34
|
-
when "
|
|
35
|
-
when "
|
|
28
|
+
when "fetch" then init_fetch(raw)
|
|
29
|
+
when "derive" then init_derive(raw)
|
|
30
|
+
when "external" then init_external(raw)
|
|
36
31
|
end
|
|
37
32
|
end
|
|
38
33
|
|
|
39
|
-
def kind
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def
|
|
34
|
+
def kind
|
|
35
|
+
{ "fetch" => :intake, "derive" => :derived, "external" => :external }.fetch(@from)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fetch? = @from == "fetch"
|
|
39
|
+
def derive? = @from == "derive"
|
|
40
|
+
def external? = @from == "external"
|
|
41
|
+
def projection? = derive?
|
|
42
|
+
def ttl_seconds = @ttl.nil? ? nil : Textus::Core::Duration.seconds(@ttl)
|
|
44
43
|
|
|
45
44
|
# Flattened projection accessors (ADR 0094) — read directly off the source
|
|
46
45
|
# block; nil when absent or not a projection source.
|
|
@@ -55,22 +54,22 @@ module Textus
|
|
|
55
54
|
|
|
56
55
|
private
|
|
57
56
|
|
|
58
|
-
def
|
|
57
|
+
def init_derive(raw)
|
|
59
58
|
%w[select pluck sort_by transform].each { |f| @projection[f] = raw[f] if raw.key?(f) }
|
|
60
59
|
return unless @projection["select"].nil? && @projection["transform"].nil?
|
|
61
60
|
|
|
62
|
-
raise Textus::BadManifest.new("source (from:
|
|
61
|
+
raise Textus::BadManifest.new("source (from: derive) requires `select:` and/or `transform:`")
|
|
63
62
|
end
|
|
64
63
|
|
|
65
|
-
def
|
|
64
|
+
def init_fetch(raw)
|
|
66
65
|
@handler = raw["handler"] or
|
|
67
|
-
raise Textus::BadManifest.new("source (from:
|
|
66
|
+
raise Textus::BadManifest.new("source (from: fetch) requires a `handler:` field")
|
|
68
67
|
@config = raw["config"] || {}
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
def
|
|
70
|
+
def init_external(raw)
|
|
72
71
|
@command = raw["command"] or
|
|
73
|
-
raise Textus::BadManifest.new("source (from:
|
|
72
|
+
raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
|
|
74
73
|
@sources = raw["sources"] || []
|
|
75
74
|
end
|
|
76
75
|
end
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
class Manifest
|
|
3
|
-
# Authority over
|
|
3
|
+
# Authority over lanes and roles derived from a Manifest::Data snapshot.
|
|
4
4
|
# Encapsulates the lookups previously living on Manifest itself
|
|
5
|
-
# (
|
|
6
|
-
# capabilities
|
|
7
|
-
# (Schema::KIND_REQUIRES_VERB) and a role may write a
|
|
8
|
-
# include that verb (
|
|
5
|
+
# (lane_writers, permission_for). Write authority is derived from
|
|
6
|
+
# capabilities x lane-kind (ADR 0030): each lane-kind requires one verb
|
|
7
|
+
# (Schema::KIND_REQUIRES_VERB) and a role may write a lane iff its caps
|
|
8
|
+
# include that verb (verb_for_lane, roles_with_capability). Derived /
|
|
9
9
|
# proposal-queue status is authoritative via the declared-kind family
|
|
10
|
-
# (declared_kind, derived_entry?,
|
|
10
|
+
# (declared_kind, derived_entry?, queue_lane?, queue_lane).
|
|
11
11
|
class Policy
|
|
12
12
|
def initialize(data)
|
|
13
13
|
@data = data
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# The capability a
|
|
17
|
-
#
|
|
16
|
+
# The capability a lane's kind requires to be written, or nil if the
|
|
17
|
+
# lane declares no kind. declared_kind returns a Symbol; the table is
|
|
18
18
|
# keyed by String.
|
|
19
|
-
def
|
|
20
|
-
kind = declared_kind(
|
|
19
|
+
def verb_for_lane(lane_name)
|
|
20
|
+
kind = declared_kind(lane_name)
|
|
21
21
|
kind && Schema::KIND_REQUIRES_VERB[kind.to_s]
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -41,39 +41,24 @@ module Textus
|
|
|
41
41
|
roles_with_capability(verb).first
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# The
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
raise UsageError.new("undeclared zone '#{zone_name}'") unless @data.declared_zone_kinds.key?(zone_name)
|
|
48
|
-
|
|
49
|
-
roles_with_capability(verb_for_zone(zone_name))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def permission_for(zone_name)
|
|
53
|
-
Textus::Domain::Permission.new(
|
|
54
|
-
zone: zone_name,
|
|
55
|
-
writers: zone_writers(zone_name),
|
|
56
|
-
)
|
|
44
|
+
# The kind declared on a lane in the manifest, or nil if undeclared.
|
|
45
|
+
def declared_kind(lane_name)
|
|
46
|
+
@data.declared_lane_kinds[lane_name]
|
|
57
47
|
end
|
|
58
48
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
# Lane names declaring `kind` (a Symbol), in manifest order. Lets callers
|
|
50
|
+
# (boot) name a kind's live lane instance(s) instead of hardcoding names.
|
|
51
|
+
def lanes_of_kind(kind)
|
|
52
|
+
@data.declared_lane_kinds.select { |_name, k| k == kind }.keys
|
|
62
53
|
end
|
|
63
54
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@data.declared_zone_kinds.select { |_name, k| k == kind }.keys
|
|
55
|
+
# The single lane declaring `kind: queue`, or nil. Schema guarantees <=1.
|
|
56
|
+
def queue_lane
|
|
57
|
+
@data.declared_lane_kinds.key(:queue)
|
|
68
58
|
end
|
|
69
59
|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
@data.declared_zone_kinds.key(:queue)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# ADR 0091: derived-ness is a property of the ENTRY, not its zone (one
|
|
76
|
-
# machine zone holds both intake and derived entries). Resolve the entry
|
|
60
|
+
# ADR 0091: derived-ness is a property of the ENTRY, not its lane (one
|
|
61
|
+
# machine lane holds both intake and derived entries). Resolve the entry
|
|
77
62
|
# and ask it directly. Returns false if entries are not yet built
|
|
78
63
|
# (validator phase during Data#initialize) — validators must not rely on
|
|
79
64
|
# cross-entry state during construction.
|
|
@@ -84,24 +69,27 @@ module Textus
|
|
|
84
69
|
entry.derived?
|
|
85
70
|
end
|
|
86
71
|
|
|
87
|
-
# The single
|
|
88
|
-
def
|
|
89
|
-
@data.
|
|
72
|
+
# The single lane declaring kind: machine, or nil.
|
|
73
|
+
def machine_lane
|
|
74
|
+
@data.declared_lane_kinds.key(:machine)
|
|
90
75
|
end
|
|
91
76
|
|
|
92
|
-
# A
|
|
93
|
-
def
|
|
94
|
-
declared_kind(
|
|
77
|
+
# A lane is a proposal queue iff it declares kind: queue.
|
|
78
|
+
def queue_lane?(lane_name)
|
|
79
|
+
declared_kind(lane_name) == :queue
|
|
95
80
|
end
|
|
96
81
|
|
|
97
|
-
# The
|
|
82
|
+
# The lane a proposer role writes proposals into: the single lane that
|
|
98
83
|
# declares kind: queue, when the role can write it. Returns nil if there
|
|
99
|
-
# is no queue
|
|
100
|
-
def
|
|
84
|
+
# is no queue lane or the role cannot write it.
|
|
85
|
+
def propose_lane_for(role)
|
|
101
86
|
return nil if role.nil?
|
|
102
87
|
|
|
103
|
-
q =
|
|
104
|
-
return nil unless q
|
|
88
|
+
q = queue_lane
|
|
89
|
+
return nil unless q
|
|
90
|
+
|
|
91
|
+
q_verb = verb_for_lane(q)
|
|
92
|
+
return nil unless roles_with_capability(q_verb).include?(role)
|
|
105
93
|
|
|
106
94
|
q
|
|
107
95
|
end
|
|
@@ -54,7 +54,8 @@ module Textus
|
|
|
54
54
|
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless nested_entry?(entry)
|
|
55
55
|
|
|
56
56
|
primary_ext = Textus::Entry.for_format(entry.format).extensions.first
|
|
57
|
-
|
|
57
|
+
base = Textus::Key::Path.normalize_relative_path(entry.path)
|
|
58
|
+
path = File.join(@data.root, base, *remaining) + primary_ext
|
|
58
59
|
Resolution.new(entry: entry, path: path, remaining: remaining)
|
|
59
60
|
end
|
|
60
61
|
end
|
|
@@ -73,7 +74,7 @@ module Textus
|
|
|
73
74
|
# addressable store keys in the public `list` surface.
|
|
74
75
|
return [] if entry.publish_mode.keyless? && !include_keyless
|
|
75
76
|
|
|
76
|
-
base = File.join(@data.root,
|
|
77
|
+
base = File.join(@data.root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
77
78
|
return [] unless File.directory?(base)
|
|
78
79
|
|
|
79
80
|
Dir.glob(File.join(base, nested_glob(entry.format)))
|
|
@@ -22,7 +22,7 @@ module Textus
|
|
|
22
22
|
def for(key)
|
|
23
23
|
slots = PICK_FIELDS.to_h { |f| [f, []] }
|
|
24
24
|
@blocks.each do |b|
|
|
25
|
-
next unless Textus::
|
|
25
|
+
next unless Textus::Manifest::Policy::Matcher.matches?(b.match, key)
|
|
26
26
|
|
|
27
27
|
slots.each_key { |slot| slots[slot] << b if b.public_send(slot) }
|
|
28
28
|
end
|
|
@@ -30,7 +30,7 @@ module Textus
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def explain(key)
|
|
33
|
-
@blocks.select { |b| Textus::
|
|
33
|
+
@blocks.select { |b| Textus::Manifest::Policy::Matcher.matches?(b.match, key) }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
@@ -39,7 +39,7 @@ module Textus
|
|
|
39
39
|
return nil if blocks.empty?
|
|
40
40
|
|
|
41
41
|
globs = blocks.map(&:match)
|
|
42
|
-
winning = Textus::
|
|
42
|
+
winning = Textus::Manifest::Policy::Matcher.pick_most_specific(globs, key: key)
|
|
43
43
|
blocks.find { |b| b.match == winning }&.public_send(slot)
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -57,7 +57,7 @@ module Textus
|
|
|
57
57
|
|
|
58
58
|
# One dispatch over the registry, replacing the four bespoke parse_*
|
|
59
59
|
# methods. :deferred carries the raw Hash after a shape check (its
|
|
60
|
-
# contents validate later — guard predicates at
|
|
60
|
+
# contents validate later — guard predicates at Dispatch::Auth check time,
|
|
61
61
|
# ADR 0031); :immediate instantiates the policy class now. :tagged passes
|
|
62
62
|
# the raw Hash straight to a policy class that is a tagged union and
|
|
63
63
|
# dispatches on its discriminator field (e.g. upkeep's on:). A mapping
|
|
@@ -4,11 +4,11 @@ module Textus
|
|
|
4
4
|
# The manifest's key whitelists and the rule-field registry — the schema's
|
|
5
5
|
# data tables (ADR 0109; the vocabulary lives in Schema::Vocabulary).
|
|
6
6
|
module Keys
|
|
7
|
-
ROOT_KEYS = %w[version roles
|
|
7
|
+
ROOT_KEYS = %w[version roles lanes entries rules audit].freeze
|
|
8
8
|
ROLE_KEYS = %w[name can].freeze
|
|
9
|
-
|
|
9
|
+
LANE_KEYS = %w[name kind owner desc].freeze
|
|
10
10
|
ENTRY_KEYS = %w[
|
|
11
|
-
key path
|
|
11
|
+
key path lane kind schema owner nested format
|
|
12
12
|
source publish
|
|
13
13
|
events ignore tracked
|
|
14
14
|
].freeze
|
|
@@ -23,7 +23,7 @@ module Textus
|
|
|
23
23
|
# `inject_boot`/`provenance` fields are kept here so the schema walk can
|
|
24
24
|
# still emit the migration hint rather than a bare "unknown key".
|
|
25
25
|
SOURCE_KEYS = %w[
|
|
26
|
-
from handler config template project command sources ttl
|
|
26
|
+
from handler config template project command sources ttl inject_boot provenance
|
|
27
27
|
select pluck sort_by transform
|
|
28
28
|
].freeze
|
|
29
29
|
# ADR 0093: rule-level GC slot. drop/archive only (refresh gone).
|
|
@@ -40,12 +40,11 @@ module Textus
|
|
|
40
40
|
# (in_rule_list / in_rule_explain)
|
|
41
41
|
#
|
|
42
42
|
# Per field:
|
|
43
|
-
# yaml_key manifest key
|
|
44
|
-
#
|
|
45
|
-
# policy_class the Domain::Policy backing the field (nil = raw value)
|
|
43
|
+
# yaml_key manifest key used in a rule block
|
|
44
|
+
# policy_class the Manifest::Policy backing the field (nil = raw value)
|
|
46
45
|
# validation :immediate (instantiate the policy at parse, surfacing
|
|
47
46
|
# shape errors eagerly), :deferred (shape-check + carry
|
|
48
|
-
# the raw Hash; guard predicates validate at
|
|
47
|
+
# the raw Hash; guard predicates validate at Dispatch::Auth
|
|
49
48
|
# build time, ADR 0031), or :tagged (pass the raw Hash to a
|
|
50
49
|
# tagged-union policy that dispatches on its discriminator
|
|
51
50
|
# field, e.g. upkeep's on:)
|
|
@@ -61,9 +60,9 @@ module Textus
|
|
|
61
60
|
# Key order here fixes the order of RULE_KEYS (after match), the slots,
|
|
62
61
|
# the RuleSet members, and the doctor SLOTS.
|
|
63
62
|
FIELD_REGISTRY = {
|
|
64
|
-
|
|
65
|
-
yaml_key: "
|
|
66
|
-
policy_class: Textus::
|
|
63
|
+
handler_permit: {
|
|
64
|
+
yaml_key: "handler_permit",
|
|
65
|
+
policy_class: Textus::Manifest::Policy::HandlerPermit,
|
|
67
66
|
validation: :immediate, sub_keys: nil, arg_key: :handlers,
|
|
68
67
|
in_pick: true, in_ambiguity: true,
|
|
69
68
|
in_rule_list: true, in_rule_explain: %i[detail]
|
|
@@ -77,11 +76,18 @@ module Textus
|
|
|
77
76
|
},
|
|
78
77
|
retention: {
|
|
79
78
|
yaml_key: "retention",
|
|
80
|
-
policy_class: Textus::
|
|
79
|
+
policy_class: Textus::Manifest::Policy::Retention,
|
|
81
80
|
validation: :tagged, sub_keys: RETENTION_KEYS, arg_key: nil,
|
|
82
81
|
in_pick: true, in_ambiguity: true,
|
|
83
82
|
in_rule_list: true, in_rule_explain: %i[lean detail]
|
|
84
83
|
},
|
|
84
|
+
react: {
|
|
85
|
+
yaml_key: "react",
|
|
86
|
+
policy_class: Textus::Manifest::Policy::React,
|
|
87
|
+
validation: :immediate, sub_keys: nil, arg_key: :raw,
|
|
88
|
+
in_pick: true, in_ambiguity: true,
|
|
89
|
+
in_rule_list: true, in_rule_explain: %i[lean detail]
|
|
90
|
+
},
|
|
85
91
|
}.freeze
|
|
86
92
|
|
|
87
93
|
RULE_KEYS = (["match"] + FIELD_REGISTRY.values.map { |m| m[:yaml_key] }).freeze
|
|
@@ -14,34 +14,36 @@ module Textus
|
|
|
14
14
|
raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
|
|
15
15
|
|
|
16
16
|
walk(raw, ROOT_KEYS, "$")
|
|
17
|
+
raise BadManifest.new("manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
18
|
+
|
|
17
19
|
validate_roles!(raw["roles"])
|
|
18
|
-
|
|
20
|
+
validate_lanes!(raw["lanes"])
|
|
19
21
|
validate_entries!(raw["entries"])
|
|
20
|
-
validate_owners!(raw["
|
|
22
|
+
validate_owners!(raw["lanes"], raw["entries"])
|
|
21
23
|
validate_rules!(raw["rules"])
|
|
22
24
|
walk(raw["audit"], AUDIT_KEYS, "$.audit") if raw["audit"].is_a?(Hash)
|
|
23
25
|
validate_single_queue!(raw)
|
|
24
26
|
validate_single_machine!(raw)
|
|
25
|
-
|
|
27
|
+
validate_lane_kind_consistency!(raw)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def
|
|
29
|
-
Array(
|
|
30
|
-
walk(z,
|
|
30
|
+
def validate_lanes!(lanes)
|
|
31
|
+
Array(lanes).each_with_index do |z, i|
|
|
32
|
+
walk(z, LANE_KEYS, "$.lanes[#{i}]")
|
|
31
33
|
if z["kind"].nil?
|
|
32
|
-
raise BadManifest.new("
|
|
34
|
+
raise BadManifest.new("lane '#{z["name"]}' at '$.lanes[#{i}]' must declare a kind (one of: #{LANE_KINDS.join(", ")})")
|
|
33
35
|
end
|
|
34
|
-
next if
|
|
36
|
+
next if LANE_KINDS.include?(z["kind"])
|
|
35
37
|
|
|
36
38
|
if %w[quarantine derived].include?(z["kind"])
|
|
37
39
|
raise BadManifest.new(
|
|
38
|
-
"
|
|
40
|
+
"lane kind '#{z["kind"]}' at '$.lanes[#{i}]' was folded into 'machine' (ADR 0091) — " \
|
|
39
41
|
"use `kind: machine`",
|
|
40
42
|
)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
raise BadManifest.new(
|
|
44
|
-
"unknown
|
|
46
|
+
"unknown lane kind '#{z["kind"]}' at '$.lanes[#{i}]' (known: #{LANE_KINDS.join(", ")})",
|
|
45
47
|
)
|
|
46
48
|
end
|
|
47
49
|
end
|
|
@@ -168,7 +170,7 @@ module Textus
|
|
|
168
170
|
hints = {
|
|
169
171
|
"lifecycle" => "age GC moved to the `retention:` rule ({ ttl, action: drop|archive }); " \
|
|
170
172
|
"intake cadence to the entry's `source: { ttl }`",
|
|
171
|
-
"materialize" => "
|
|
173
|
+
"materialize" => "removed — materialization is automatic (a write enqueues a job; run `drain`)",
|
|
172
174
|
}
|
|
173
175
|
hints.each do |old, hint|
|
|
174
176
|
next unless rule.key?(old)
|
|
@@ -194,10 +196,10 @@ module Textus
|
|
|
194
196
|
Array(r["can"]).each do |verb|
|
|
195
197
|
next if CAPABILITIES.include?(verb)
|
|
196
198
|
|
|
197
|
-
# The quarantine capability folded into
|
|
199
|
+
# The quarantine capability folded into the converge capability (ADR 0090); a
|
|
198
200
|
# manifest still naming the old quarantine capability (`ingest`, or
|
|
199
201
|
# legacy `fetch`) gets a pointed hint rather than a bare error.
|
|
200
|
-
hint = %w[ingest fetch].include?(verb) ? " — the quarantine capability folded into '
|
|
202
|
+
hint = %w[ingest fetch].include?(verb) ? " — the quarantine capability folded into 'converge' (ADR 0090)" : ""
|
|
201
203
|
raise BadManifest.new(
|
|
202
204
|
"unknown capability '#{verb}' for role '#{name}' at '#{path}' " \
|
|
203
205
|
"(known: #{CAPABILITIES.join(", ")})#{hint}",
|
|
@@ -217,9 +219,9 @@ module Textus
|
|
|
217
219
|
# (ADR 0045 D1) so attribution can't bypass the closed-name guarantee.
|
|
218
220
|
# Applies to both zone owners and entry owners; owner is optional, so a
|
|
219
221
|
# nil owner is not an error.
|
|
220
|
-
def validate_owners!(
|
|
221
|
-
Array(
|
|
222
|
-
check_owner!(z["owner"], "$.
|
|
222
|
+
def validate_owners!(lanes, entries)
|
|
223
|
+
Array(lanes).each_with_index do |z, i|
|
|
224
|
+
check_owner!(z["owner"], "$.lanes[#{i}]")
|
|
223
225
|
end
|
|
224
226
|
Array(entries).each_with_index do |e, i|
|
|
225
227
|
check_owner!(e["owner"], "$.entries[#{i}]")
|
|
@@ -265,20 +267,20 @@ module Textus
|
|
|
265
267
|
end
|
|
266
268
|
|
|
267
269
|
def validate_single_queue!(raw)
|
|
268
|
-
queues = Array(raw["
|
|
270
|
+
queues = Array(raw["lanes"]).select { |z| z["kind"] == "queue" }.map { |z| z["name"] }
|
|
269
271
|
return if queues.size <= 1
|
|
270
272
|
|
|
271
273
|
raise BadManifest.new(
|
|
272
|
-
"at most one
|
|
274
|
+
"at most one lane may declare kind: queue (found: #{queues.join(", ")})",
|
|
273
275
|
)
|
|
274
276
|
end
|
|
275
277
|
|
|
276
278
|
def validate_single_machine!(raw)
|
|
277
|
-
machines = Array(raw["
|
|
279
|
+
machines = Array(raw["lanes"]).select { |z| z["kind"] == "machine" }.map { |z| z["name"] }
|
|
278
280
|
return if machines.size <= 1
|
|
279
281
|
|
|
280
282
|
raise BadManifest.new(
|
|
281
|
-
"at most one
|
|
283
|
+
"at most one lane may declare kind: machine (found: #{machines.join(", ")})",
|
|
282
284
|
)
|
|
283
285
|
end
|
|
284
286
|
|
|
@@ -300,20 +302,20 @@ module Textus
|
|
|
300
302
|
end
|
|
301
303
|
end
|
|
302
304
|
|
|
303
|
-
# Write authority is derived from capabilities (ADR 0030): a
|
|
305
|
+
# Write authority is derived from capabilities (ADR 0030): a lane of a
|
|
304
306
|
# given kind can only be written by a role that holds the kind's required
|
|
305
|
-
# verb. Reject a manifest declaring a
|
|
307
|
+
# verb. Reject a manifest declaring a lane whose required verb is held by
|
|
306
308
|
# no role. Capabilities.resolve returns the defaults when `roles:` is nil,
|
|
307
309
|
# so the capability union is all four verbs and every kind is satisfied.
|
|
308
|
-
def
|
|
310
|
+
def validate_lane_kind_consistency!(raw)
|
|
309
311
|
held = Capabilities.resolve(raw["roles"]).values.flatten.uniq
|
|
310
312
|
|
|
311
|
-
Array(raw["
|
|
313
|
+
Array(raw["lanes"]).each_with_index do |z, i|
|
|
312
314
|
verb = KIND_REQUIRES_VERB[z["kind"]]
|
|
313
315
|
next if verb.nil? || held.include?(verb)
|
|
314
316
|
|
|
315
317
|
raise BadManifest.new(
|
|
316
|
-
"
|
|
318
|
+
"lane '#{z["name"]}' (#{z["kind"]}) at '$.lanes[#{i}]' " \
|
|
317
319
|
"needs a role with capability '#{verb}'; none declared",
|
|
318
320
|
)
|
|
319
321
|
end
|
|
@@ -5,17 +5,17 @@ module Textus
|
|
|
5
5
|
# 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
|
|
6
6
|
# in ADR 0091). Each kind pairs with the capability that authorizes
|
|
7
7
|
# originating bytes in it. ONE source of truth; the derived constants below
|
|
8
|
-
# cannot drift. A BIJECTION again (0090 had two kinds →
|
|
8
|
+
# cannot drift. A BIJECTION again (0090 had two kinds → the converge capability; 0091
|
|
9
9
|
# collapses them, so kind ↔ capability is 1:1).
|
|
10
10
|
module Vocabulary
|
|
11
11
|
LANES = {
|
|
12
12
|
"canon" => "author",
|
|
13
13
|
"workspace" => "keep",
|
|
14
|
-
"machine" => "
|
|
14
|
+
"machine" => "converge",
|
|
15
15
|
"queue" => "propose",
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
LANE_KINDS = LANES.keys.freeze
|
|
19
19
|
CAPABILITIES = LANES.values.uniq.freeze
|
|
20
20
|
KIND_REQUIRES_VERB = LANES
|
|
21
21
|
end
|
|
@@ -7,13 +7,13 @@ module Textus
|
|
|
7
7
|
module Schema
|
|
8
8
|
# Re-export the vocabulary.
|
|
9
9
|
LANES = Vocabulary::LANES
|
|
10
|
-
|
|
10
|
+
LANE_KINDS = Vocabulary::LANE_KINDS
|
|
11
11
|
CAPABILITIES = Vocabulary::CAPABILITIES
|
|
12
12
|
KIND_REQUIRES_VERB = Vocabulary::KIND_REQUIRES_VERB
|
|
13
13
|
# Re-export the keys + registry.
|
|
14
14
|
ROOT_KEYS = Keys::ROOT_KEYS
|
|
15
15
|
ROLE_KEYS = Keys::ROLE_KEYS
|
|
16
|
-
|
|
16
|
+
LANE_KEYS = Keys::LANE_KEYS
|
|
17
17
|
ENTRY_KEYS = Keys::ENTRY_KEYS
|
|
18
18
|
PUBLISH_KEYS = Keys::PUBLISH_KEYS
|
|
19
19
|
SOURCE_KEYS = Keys::SOURCE_KEYS
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
#
|
|
7
7
|
# * data — frozen value: raw, root, zones, entries, audit_config, role_caps
|
|
8
8
|
# * resolver — resolves keys → entry + path
|
|
9
|
-
# * policy — zone/role authority (
|
|
10
|
-
#
|
|
9
|
+
# * policy — zone/role authority (lane_writers, declared_kind, derived_entry?,
|
|
10
|
+
# queue_lane?, permission_for, …)
|
|
11
11
|
# * rules — match-block rule engine (lifecycle, handler allowlist, materialize, …)
|
|
12
12
|
#
|
|
13
13
|
# Use `manifest.data.entries`, `manifest.policy.declared_kind(z)`, etc.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
require "timeout"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Pipeline
|
|
5
5
|
module Acquire
|
|
6
6
|
# Invokes a :resolve_handler hook handler by name under a timeout — the single
|
|
7
7
|
# home for "call the intake handler under a deadline" (ADR 0048 D1). Shared by
|
|
8
8
|
# Produce::Acquire::Intake (the internal ingest mechanism — no public verb since ADR 0079)
|
|
9
|
-
# as driven by the
|
|
9
|
+
# as driven by the converge sweep (drain/serve) and `textus hook run` (ADR 0089 made
|
|
10
10
|
# ingest system-pushed; there is no read or put trigger).
|
|
11
11
|
# Always passes a Container as `caps:` so the hook contract (ADR 0027) is
|
|
12
12
|
# uniform across every entry point. Maps Timeout::Error to a UsageError;
|
|
@@ -18,7 +18,7 @@ module Textus
|
|
|
18
18
|
|
|
19
19
|
def invoke(caps:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
|
|
20
20
|
Timeout.timeout(timeout) do
|
|
21
|
-
caps.
|
|
21
|
+
caps.steps.invoke(:fetch, handler, caps: caps, config: config, args: args)
|
|
22
22
|
end
|
|
23
23
|
rescue Timeout::Error
|
|
24
24
|
raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")
|