textus 0.52.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- 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/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- 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 +11 -11
- 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 +1 -1
- 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.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 +17 -9
- 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 +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- 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 +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- 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 +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- 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 +7 -5
- 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/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- 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/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 -3
- 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 +132 -118
- 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/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- 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/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- 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 -73
- 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/jobs.rb +0 -31
- 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/enqueue.rb +0 -50
- 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
|
@@ -2,14 +2,14 @@ module Textus
|
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
4
|
class Base < Entry
|
|
5
|
-
attr_reader :raw, :key, :path, :
|
|
5
|
+
attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
|
|
6
6
|
|
|
7
7
|
# rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
|
|
8
|
-
def initialize(raw:, key:, path:,
|
|
8
|
+
def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
|
|
9
9
|
@raw = raw
|
|
10
10
|
@key = key
|
|
11
11
|
@path = path
|
|
12
|
-
@
|
|
12
|
+
@lane = lane
|
|
13
13
|
@schema = schema
|
|
14
14
|
@owner = owner
|
|
15
15
|
@format = format
|
|
@@ -17,13 +17,14 @@ module Textus
|
|
|
17
17
|
end
|
|
18
18
|
# rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
policy.
|
|
20
|
+
def lane_writers(policy)
|
|
21
|
+
verb = policy.verb_for_lane(@lane)
|
|
22
|
+
policy.roles_with_capability(verb)
|
|
22
23
|
rescue UsageError => e
|
|
23
24
|
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
+
def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
|
|
27
28
|
|
|
28
29
|
def nested? = false
|
|
29
30
|
def derived? = false
|
|
@@ -36,6 +37,9 @@ module Textus
|
|
|
36
37
|
def external? = false
|
|
37
38
|
def projection? = false
|
|
38
39
|
|
|
40
|
+
alias zone lane
|
|
41
|
+
alias in_proposal_zone? in_proposal_lane?
|
|
42
|
+
|
|
39
43
|
# Whether git should track this entry's file. Default true; an entry
|
|
40
44
|
# marked `tracked: false` in the manifest stays protocol-readable but is
|
|
41
45
|
# listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
|
|
@@ -60,19 +64,19 @@ module Textus
|
|
|
60
64
|
# Minimal context object passed into entry `publish_via` hooks.
|
|
61
65
|
# Everything beyond the three primitives is derived. Data.define
|
|
62
66
|
# instances are frozen, so we recompute per-call rather than
|
|
63
|
-
# memoizing — RoleScope/
|
|
67
|
+
# memoizing — RoleScope/Step::Context construction is cheap.
|
|
64
68
|
PublishContext = ::Data.define(:container, :call, :reader) do
|
|
65
69
|
def manifest = container.manifest
|
|
66
70
|
def root = container.root
|
|
67
71
|
def repo_root = File.dirname(container.root)
|
|
68
|
-
def
|
|
72
|
+
def steps = container.steps
|
|
69
73
|
|
|
70
74
|
def hook_context
|
|
71
|
-
Textus::
|
|
75
|
+
Textus::Step::Context.new(scope: scope_for_hooks)
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def emit(event, **payload)
|
|
75
|
-
|
|
79
|
+
steps.publish(event, ctx: hook_context, **payload)
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
# Read a named template from the store's templates/ directory.
|
|
@@ -91,7 +95,7 @@ module Textus
|
|
|
91
95
|
private
|
|
92
96
|
|
|
93
97
|
def scope_for_hooks
|
|
94
|
-
Textus::RoleScope.new(
|
|
98
|
+
Textus::Surfaces::RoleScope.new(
|
|
95
99
|
container: container, role: call.role, dry_run: call.dry_run,
|
|
96
100
|
)
|
|
97
101
|
end
|
|
@@ -5,7 +5,7 @@ module Textus
|
|
|
5
5
|
def self.call(raw)
|
|
6
6
|
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
7
7
|
path = raw["path"] or raise UsageError.new("manifest entry '#{key}' missing path")
|
|
8
|
-
|
|
8
|
+
lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
|
|
9
9
|
|
|
10
10
|
raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
|
|
11
11
|
kind = raw_kind.to_sym
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
|
|
20
20
|
common = {
|
|
21
21
|
raw: raw,
|
|
22
|
-
key: key, path: path,
|
|
22
|
+
key: key, path: path, lane: lane,
|
|
23
23
|
schema: raw["schema"], owner: raw["owner"],
|
|
24
24
|
format: format,
|
|
25
25
|
publish_targets: publish_targets(raw)
|
|
@@ -31,12 +31,12 @@ module Textus
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# ADR 0093: an entry's production block is the unified `source:`. Returns a
|
|
34
|
-
#
|
|
34
|
+
# Manifest::Policy::Source; kind (intake/derived) is read from source.from.
|
|
35
35
|
def self.parse_source(raw, key)
|
|
36
36
|
block = raw["source"] or
|
|
37
|
-
raise BadManifest.new("entry '#{key}' requires a source: { from:
|
|
37
|
+
raise BadManifest.new("entry '#{key}' requires a source: { from: derive|fetch|external, ... }")
|
|
38
38
|
|
|
39
|
-
Textus::
|
|
39
|
+
Textus::Manifest::Policy::Source.new(block)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# ADR 0094: `publish:` is a LIST of target objects — to-targets
|
|
@@ -52,7 +52,7 @@ module Textus
|
|
|
52
52
|
"[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
|
|
53
53
|
)
|
|
54
54
|
end
|
|
55
|
-
block.map { |t| Textus::
|
|
55
|
+
block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def self.resolve_format(raw, path)
|
|
@@ -17,6 +17,8 @@ module Textus
|
|
|
17
17
|
def derived? = @source.kind == :derived
|
|
18
18
|
def external? = @source.external?
|
|
19
19
|
def projection? = @source.projection?
|
|
20
|
+
def fetch? = @source.fetch?
|
|
21
|
+
def derive? = @source.derive?
|
|
20
22
|
def nested? = !!@raw["nested"]
|
|
21
23
|
def handler = @source.handler
|
|
22
24
|
def config = @source.config
|
|
@@ -33,11 +35,10 @@ module Textus
|
|
|
33
35
|
def publish_via(pctx, prefix: nil)
|
|
34
36
|
built = false
|
|
35
37
|
if projection?
|
|
36
|
-
Textus::
|
|
38
|
+
Textus::Pipeline::Acquire::Projection.new(container: pctx.container, call: pctx.call).run(self)
|
|
37
39
|
built = true
|
|
38
40
|
pctx.emit(:entry_produced, key: @key, envelope: pctx.reader.call(@key), sources: Array(@source.select).compact)
|
|
39
41
|
end
|
|
40
|
-
|
|
41
42
|
emitted = publish_mode.publish(pctx, prefix: prefix)
|
|
42
43
|
return emitted if emitted
|
|
43
44
|
return nil unless built
|
|
@@ -16,11 +16,12 @@ module Textus
|
|
|
16
16
|
|
|
17
17
|
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize
|
|
18
18
|
targets = entry.publish_targets.select(&:to_target?)
|
|
19
|
+
|
|
19
20
|
return nil if targets.empty?
|
|
20
21
|
|
|
21
22
|
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
22
23
|
envelope = pctx.reader.call(entry.key)
|
|
23
|
-
renderer = Textus::
|
|
24
|
+
renderer = Textus::Pipeline::Render.new(template_loader: ->(n) { pctx.read_template(n) })
|
|
24
25
|
content = nil # parsed lazily; the data's `content` (always _meta-free)
|
|
25
26
|
|
|
26
27
|
targets.each do |t|
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
module Validators
|
|
5
5
|
module Events
|
|
6
6
|
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
|
-
pubsub_events = Textus::
|
|
7
|
+
pubsub_events = Textus::Step::Catalog::PUBSUB.keys
|
|
8
8
|
events = entry.events
|
|
9
9
|
events.each_key do |evt|
|
|
10
10
|
next if pubsub_events.include?(evt.to_sym)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
|
+
class HandlerPermit
|
|
5
5
|
attr_reader :handlers
|
|
6
6
|
|
|
7
7
|
def initialize(handlers:)
|
|
8
8
|
@handlers = Array(handlers).map(&:to_s).freeze
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def permits?(handler)
|
|
12
12
|
@handlers.include?(handler.to_s)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
|
+
class React
|
|
5
|
+
ALLOWED_KEYS = %w[on when do scope budget idempotency observe priority].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :raw
|
|
8
|
+
|
|
9
|
+
def initialize(raw:)
|
|
10
|
+
raise Textus::BadManifest.new("react: must be a map") unless raw.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
raw = raw.each_with_object({}) do |(key, value), out|
|
|
13
|
+
normalized = key == true ? "on" : key.to_s
|
|
14
|
+
out[normalized] = value
|
|
15
|
+
end
|
|
16
|
+
raise Textus::BadManifest.new("react.ttl is invalid; ttl belongs only to source.ttl or retention.ttl") if raw.key?("ttl")
|
|
17
|
+
|
|
18
|
+
unknown = raw.keys - ALLOWED_KEYS
|
|
19
|
+
raise Textus::BadManifest.new("react: unknown key(s): #{unknown.join(", ")}") unless unknown.empty?
|
|
20
|
+
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
@raw
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
@@ -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:
|
|
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
10
|
# Materialization is async-only (job-queue model): a write enqueues a
|
|
11
11
|
# `materialize` job, converged by a worker. There is no per-entry write
|
|
12
12
|
# trigger knob.
|
|
13
13
|
class Source
|
|
14
|
-
FROMS = %w[
|
|
14
|
+
FROMS = %w[fetch derive external].freeze
|
|
15
15
|
|
|
16
16
|
attr_reader :from, :handler, :config, :command, :sources
|
|
17
17
|
|
|
@@ -25,16 +25,21 @@ module Textus
|
|
|
25
25
|
@projection = {}
|
|
26
26
|
|
|
27
27
|
case @from
|
|
28
|
-
when "
|
|
29
|
-
when "
|
|
30
|
-
when "
|
|
28
|
+
when "fetch" then init_fetch(raw)
|
|
29
|
+
when "derive" then init_derive(raw)
|
|
30
|
+
when "external" then init_external(raw)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def kind
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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)
|
|
38
43
|
|
|
39
44
|
# Flattened projection accessors (ADR 0094) — read directly off the source
|
|
40
45
|
# block; nil when absent or not a projection source.
|
|
@@ -49,22 +54,22 @@ module Textus
|
|
|
49
54
|
|
|
50
55
|
private
|
|
51
56
|
|
|
52
|
-
def
|
|
57
|
+
def init_derive(raw)
|
|
53
58
|
%w[select pluck sort_by transform].each { |f| @projection[f] = raw[f] if raw.key?(f) }
|
|
54
59
|
return unless @projection["select"].nil? && @projection["transform"].nil?
|
|
55
60
|
|
|
56
|
-
raise Textus::BadManifest.new("source (from:
|
|
61
|
+
raise Textus::BadManifest.new("source (from: derive) requires `select:` and/or `transform:`")
|
|
57
62
|
end
|
|
58
63
|
|
|
59
|
-
def
|
|
64
|
+
def init_fetch(raw)
|
|
60
65
|
@handler = raw["handler"] or
|
|
61
|
-
raise Textus::BadManifest.new("source (from:
|
|
66
|
+
raise Textus::BadManifest.new("source (from: fetch) requires a `handler:` field")
|
|
62
67
|
@config = raw["config"] || {}
|
|
63
68
|
end
|
|
64
69
|
|
|
65
|
-
def
|
|
70
|
+
def init_external(raw)
|
|
66
71
|
@command = raw["command"] or
|
|
67
|
-
raise Textus::BadManifest.new("source (from:
|
|
72
|
+
raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
|
|
68
73
|
@sources = raw["sources"] || []
|
|
69
74
|
end
|
|
70
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
|
|
@@ -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
|