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
|
@@ -6,15 +6,15 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered =
|
|
9
|
+
registered = steps.names(:fetch).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
13
13
|
"code" => "intake.handler_missing",
|
|
14
14
|
"level" => "error",
|
|
15
15
|
"subject" => name.to_s,
|
|
16
|
-
"message" => "manifest references intake handler '#{name}' but no
|
|
17
|
-
"fix" => "create .textus/
|
|
16
|
+
"message" => "manifest references intake handler '#{name}' but no fetch step for '#{name}' is registered",
|
|
17
|
+
"fix" => "create .textus/steps/fetch/#{name}.rb with `class #{name.to_s.split(/[-_]/).map(&:capitalize).join}Fetch < Textus::Step::Fetch`",
|
|
18
18
|
}
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -23,8 +23,8 @@ module Textus
|
|
|
23
23
|
"code" => "intake.handler_orphan",
|
|
24
24
|
"level" => "warning",
|
|
25
25
|
"subject" => name.to_s,
|
|
26
|
-
"message" => "
|
|
27
|
-
"fix" => "remove the unused
|
|
26
|
+
"message" => "fetch step '#{name}' is registered but no manifest entry references it",
|
|
27
|
+
"fix" => "remove the unused step, or add an entry with `intake.handler: #{name}`",
|
|
28
28
|
}
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -8,10 +8,10 @@ module Textus
|
|
|
8
8
|
# corruption (the accept gate already refuses them).
|
|
9
9
|
class ProposalTargets < Check
|
|
10
10
|
def call
|
|
11
|
-
queue = manifest.policy.
|
|
11
|
+
queue = manifest.policy.queue_lane
|
|
12
12
|
return [] unless queue
|
|
13
13
|
|
|
14
|
-
dispatch(:list,
|
|
14
|
+
dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
@@ -20,7 +20,7 @@ module Textus
|
|
|
20
20
|
target = dispatch(:get, key).meta&.dig("proposal", "target_key")
|
|
21
21
|
return nil if target.nil? # not a proposal entry — skip
|
|
22
22
|
|
|
23
|
-
zone = manifest.resolver.resolve(target).entry.
|
|
23
|
+
zone = manifest.resolver.resolve(target).entry.lane
|
|
24
24
|
return nil if manifest.policy.declared_kind(zone.to_s) == :canon
|
|
25
25
|
|
|
26
26
|
{
|
|
@@ -2,7 +2,7 @@ module Textus
|
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
4
|
# Flags entries whose key is matched by two or more rule blocks of the
|
|
5
|
-
# SAME specificity in the same slot (lifecycle /
|
|
5
|
+
# SAME specificity in the same slot (lifecycle / handler_permit /
|
|
6
6
|
# guard / materialize). Ties are non-deterministic in the parser's pick step, so
|
|
7
7
|
# they're a configuration smell — surface them.
|
|
8
8
|
class RuleAmbiguity < Check
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
carriers = matches.select { |b| b.public_send(slot) }
|
|
27
27
|
return [] if carriers.length < 2
|
|
28
28
|
|
|
29
|
-
by_specificity = carriers.group_by { |b| Textus::
|
|
29
|
+
by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
|
|
30
30
|
tied = by_specificity.values.select { |group| group.length > 1 }
|
|
31
31
|
tied.map { |group| issue_for(mentry, slot, group) }
|
|
32
32
|
end
|
|
@@ -3,8 +3,14 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
result = Textus::Doctor::Validator.new(
|
|
7
|
+
reader: ->(key, ctnr, c) { Textus::Action::Get.new(key: key).call(container: ctnr, call: c) },
|
|
8
|
+
manifest: @container.manifest,
|
|
9
|
+
audit_log: @container.audit_log,
|
|
10
|
+
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
11
|
+
).call(container: @container, call: Textus::Call.build(role: Textus::Role::DEFAULT))
|
|
12
|
+
|
|
13
|
+
result["violations"].map do |v|
|
|
8
14
|
fix = v["expected"] &&
|
|
9
15
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
10
16
|
{
|
|
@@ -31,7 +31,7 @@ module Textus
|
|
|
31
31
|
"level" => "warning",
|
|
32
32
|
"subject" => sentinel_path,
|
|
33
33
|
"message" => "sentinel is not valid JSON",
|
|
34
|
-
"fix" => "delete #{sentinel_path} and re-run 'textus
|
|
34
|
+
"fix" => "delete #{sentinel_path} and re-run 'textus drain' to regenerate",
|
|
35
35
|
}
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -51,7 +51,7 @@ module Textus
|
|
|
51
51
|
"level" => "warning",
|
|
52
52
|
"subject" => sentinel.target,
|
|
53
53
|
"message" => "published file at #{sentinel.target} was modified out-of-band",
|
|
54
|
-
"fix" => "re-run 'textus
|
|
54
|
+
"fix" => "re-run 'textus drain' to overwrite, or copy the manual edit back into the store source",
|
|
55
55
|
}
|
|
56
56
|
end
|
|
57
57
|
end
|
data/lib/textus/doctor/check.rb
CHANGED
|
@@ -14,8 +14,9 @@ module Textus
|
|
|
14
14
|
.downcase
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def initialize(container)
|
|
17
|
+
def initialize(container, role: Textus::Role::DEFAULT)
|
|
18
18
|
@container = container
|
|
19
|
+
@role = role
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def call
|
|
@@ -26,16 +27,18 @@ module Textus
|
|
|
26
27
|
|
|
27
28
|
def root = @container.root
|
|
28
29
|
def manifest = @container.manifest
|
|
29
|
-
def
|
|
30
|
+
def steps = @container.steps
|
|
30
31
|
|
|
31
|
-
# Dispatch a verb through
|
|
32
|
+
# Dispatch a verb through Gate.
|
|
32
33
|
def dispatch(verb, *args, **kwargs)
|
|
33
|
-
Textus::
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
34
|
+
klass = Textus::Action::VERBS[verb]
|
|
35
|
+
spec = klass.contract if klass.respond_to?(:contract?) && klass.contract?
|
|
36
|
+
inputs = spec ? Textus::Contract::Binder.inputs_from_ordered(spec, args, kwargs) : kwargs
|
|
37
|
+
cmd_class = Textus::Gate::VERB_COMMAND.fetch(verb)
|
|
38
|
+
merged = inputs.merge(role: @role)
|
|
39
|
+
filled = cmd_class.members.to_h { |m| [m, merged.key?(m) ? merged[m] : nil] }
|
|
40
|
+
cmd = cmd_class.new(**filled)
|
|
41
|
+
@container.gate.dispatch(cmd)
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Textus
|
|
2
|
-
module
|
|
4
|
+
module Doctor
|
|
3
5
|
class Validator
|
|
4
6
|
def initialize(reader:, manifest:, audit_log:, schema_for:)
|
|
5
|
-
@reader
|
|
6
|
-
@manifest
|
|
7
|
+
@reader = reader
|
|
8
|
+
@manifest = manifest
|
|
7
9
|
@audit_log = audit_log
|
|
8
10
|
@schema_for = schema_for
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def call
|
|
13
|
+
def call(container:, call:)
|
|
14
|
+
@container = container
|
|
15
|
+
@call = call
|
|
12
16
|
violations = []
|
|
13
17
|
check_content_violations(violations)
|
|
14
18
|
check_role_authority_violations(violations)
|
|
15
|
-
{ "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
19
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
private
|
|
19
23
|
|
|
20
24
|
def check_content_violations(violations)
|
|
21
25
|
@manifest.resolver.enumerate.each do |row|
|
|
22
|
-
key
|
|
26
|
+
key = row[:key]
|
|
23
27
|
mentry = row[:manifest_entry]
|
|
24
|
-
env
|
|
28
|
+
env = fetch_envelope(key, violations) or next
|
|
25
29
|
schema = mentry.schema && @schema_for.call(mentry.schema)
|
|
26
30
|
next unless schema
|
|
27
31
|
|
|
@@ -42,7 +46,7 @@ module Textus
|
|
|
42
46
|
next unless schema
|
|
43
47
|
|
|
44
48
|
env = begin
|
|
45
|
-
@reader.
|
|
49
|
+
@reader.call(row[:key], @container, @call)
|
|
46
50
|
rescue StandardError
|
|
47
51
|
next
|
|
48
52
|
end
|
|
@@ -54,19 +58,24 @@ module Textus
|
|
|
54
58
|
last_writer = @audit_log.last_writer_for(key)
|
|
55
59
|
return if last_writer.nil?
|
|
56
60
|
|
|
57
|
-
last_writer_is_authority =
|
|
58
|
-
|
|
61
|
+
last_writer_is_authority =
|
|
62
|
+
@manifest.policy.roles_with_capability("author").include?(last_writer)
|
|
59
63
|
env.meta.each_key do |field|
|
|
60
64
|
owner = schema.maintained_by(field)
|
|
61
65
|
next if owner.nil? || last_writer == owner || last_writer_is_authority
|
|
62
66
|
|
|
63
|
-
violations << {
|
|
64
|
-
|
|
67
|
+
violations << {
|
|
68
|
+
"key" => key,
|
|
69
|
+
"code" => "role_authority",
|
|
70
|
+
"field" => field,
|
|
71
|
+
"expected" => owner,
|
|
72
|
+
"last_writer" => last_writer,
|
|
73
|
+
}
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def fetch_envelope(key, violations)
|
|
69
|
-
@reader.
|
|
78
|
+
@reader.call(key, @container, @call)
|
|
70
79
|
rescue Textus::Error => e
|
|
71
80
|
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
72
81
|
nil
|
data/lib/textus/doctor.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Textus
|
|
|
22
22
|
Check::UnownedSchemaFields,
|
|
23
23
|
Check::SchemaViolations,
|
|
24
24
|
Check::RuleAmbiguity,
|
|
25
|
-
Check::
|
|
25
|
+
Check::HandlerPermit,
|
|
26
26
|
Check::OrphanedPublishTargets,
|
|
27
27
|
Check::PublishTreeIndexOverlap,
|
|
28
28
|
Check::ProposalTargets,
|
|
@@ -33,7 +33,7 @@ module Textus
|
|
|
33
33
|
|
|
34
34
|
module_function
|
|
35
35
|
|
|
36
|
-
def build(container:, checks: nil)
|
|
36
|
+
def build(container:, checks: nil, role: Textus::Role::DEFAULT)
|
|
37
37
|
selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
|
|
38
38
|
unknown = selected_keys - ALL_CHECKS
|
|
39
39
|
unless unknown.empty?
|
|
@@ -43,7 +43,7 @@ module Textus
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
|
|
46
|
-
issues = selected.flat_map { |c| c.new(container).call }
|
|
46
|
+
issues = selected.flat_map { |c| c.new(container, role:).call }
|
|
47
47
|
issues.concat(run_registered_checks(container))
|
|
48
48
|
|
|
49
49
|
summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
|
|
@@ -56,12 +56,12 @@ module Textus
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def run_registered_checks(container)
|
|
59
|
-
container.
|
|
59
|
+
container.steps.names(:validate).flat_map { |name| invoke_registered_check(container, name) }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def invoke_registered_check(container, name)
|
|
63
63
|
result = Timeout.timeout(DOCTOR_CHECK_TIMEOUT_SECONDS) do
|
|
64
|
-
container.
|
|
64
|
+
container.steps.invoke(:validate, name, caps: container)
|
|
65
65
|
end
|
|
66
66
|
return result.map { |h| h.transform_keys(&:to_s) } if result.is_a?(Array)
|
|
67
67
|
|
|
@@ -75,7 +75,7 @@ module Textus
|
|
|
75
75
|
rescue StandardError => e
|
|
76
76
|
[fail_issue(name, code: "doctor_check.failed",
|
|
77
77
|
message: "#{e.class}: #{e.message}",
|
|
78
|
-
fix: "fix the :validate
|
|
78
|
+
fix: "fix the :validate step in .textus/steps/validate/")]
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def fail_issue(name, code:, message:, fix:)
|
|
@@ -32,42 +32,15 @@ module Textus
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def put(key, mentry:, payload:, if_etag: nil)
|
|
35
|
-
path =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
existing_uid = @reader.existing_uid(key)
|
|
40
|
-
meta, content = ensure_uid(mentry.format, meta, payload.content, existing_uid)
|
|
41
|
-
|
|
42
|
-
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
43
|
-
mentry: mentry, path: path,
|
|
44
|
-
meta: meta, body: payload.body, content: content
|
|
45
|
-
)
|
|
46
|
-
|
|
35
|
+
path = resolve_path(key)
|
|
36
|
+
meta, content = prepare_uid(mentry, payload, key)
|
|
37
|
+
bytes, eff_meta, eff_body, eff_content = serialize_entry(mentry, path, meta, payload, content)
|
|
47
38
|
enforce_name_match!(path, eff_meta, mentry.format)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{ "_meta" => eff_meta, "content" => eff_content },
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
|
|
58
|
-
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
59
|
-
|
|
60
|
-
@file_store.write(path, bytes)
|
|
61
|
-
etag_after = Etag.for_bytes(bytes)
|
|
62
|
-
envelope = Textus::Envelope.build(
|
|
63
|
-
key: key, mentry: mentry, path: path,
|
|
64
|
-
meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
|
|
65
|
-
)
|
|
66
|
-
@audit_log.append(
|
|
67
|
-
role: @call.role, verb: "put", key: key,
|
|
68
|
-
etag_before: etag_before, etag_after: etag_after,
|
|
69
|
-
extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
70
|
-
)
|
|
39
|
+
validate_schema(mentry, eff_meta, eff_content)
|
|
40
|
+
etag_before = check_etag!(path, key, if_etag)
|
|
41
|
+
write_bytes(path, bytes)
|
|
42
|
+
envelope = build_envelope(key, mentry, path, eff_meta, eff_body, eff_content)
|
|
43
|
+
audit_put(key, etag_before, envelope.etag)
|
|
71
44
|
envelope
|
|
72
45
|
end
|
|
73
46
|
|
|
@@ -155,7 +128,7 @@ module Textus
|
|
|
155
128
|
# The zone directory under which `path` lives (`<root>/zones/<zone>`),
|
|
156
129
|
# or nil if `path` is not under the store's zones tree.
|
|
157
130
|
def zone_floor(path)
|
|
158
|
-
zones_root = File.join(@manifest.data.root, "
|
|
131
|
+
zones_root = File.join(@manifest.data.root, "data")
|
|
159
132
|
prefix = "#{zones_root}/"
|
|
160
133
|
return nil unless path.start_with?(prefix)
|
|
161
134
|
|
|
@@ -176,6 +149,62 @@ module Textus
|
|
|
176
149
|
meta: meta, body: body, content: content, path: path,
|
|
177
150
|
)
|
|
178
151
|
end
|
|
152
|
+
|
|
153
|
+
def resolve_path(key)
|
|
154
|
+
@manifest.resolver.resolve(key).path
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def prepare_uid(mentry, payload, key)
|
|
158
|
+
meta = payload.meta || {}
|
|
159
|
+
existing_uid = @reader.existing_uid(key)
|
|
160
|
+
ensure_uid(mentry.format, meta, payload.content, existing_uid)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def serialize_entry(mentry, path, meta, payload, content)
|
|
164
|
+
serialize_for_put(
|
|
165
|
+
mentry: mentry, path: path,
|
|
166
|
+
meta: meta, body: payload.body, content: content
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def validate_schema(mentry, eff_meta, eff_content)
|
|
171
|
+
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
172
|
+
return unless schema
|
|
173
|
+
|
|
174
|
+
Entry.for_format(mentry.format).validate_against(
|
|
175
|
+
schema,
|
|
176
|
+
{ "_meta" => eff_meta, "content" => eff_content },
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def check_etag!(path, key, if_etag)
|
|
181
|
+
etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
|
|
182
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
183
|
+
|
|
184
|
+
etag_before
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def write_bytes(path, bytes)
|
|
188
|
+
@file_store.write(path, bytes)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def build_envelope(key, mentry, path, eff_meta, eff_body, eff_content)
|
|
192
|
+
Textus::Envelope.build(
|
|
193
|
+
key: key, mentry: mentry, path: path,
|
|
194
|
+
meta: eff_meta, body: eff_body,
|
|
195
|
+
etag: Etag.for_bytes(@file_store.read(path)),
|
|
196
|
+
content: eff_content
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def audit_put(key, etag_before, etag_after)
|
|
201
|
+
extras = @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
202
|
+
@audit_log.append(
|
|
203
|
+
role: @call.role, verb: "put", key: key,
|
|
204
|
+
etag_before: etag_before, etag_after: etag_after,
|
|
205
|
+
extras: extras
|
|
206
|
+
)
|
|
207
|
+
end
|
|
179
208
|
end
|
|
180
209
|
end
|
|
181
210
|
end
|
data/lib/textus/envelope.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
Envelope = Data.define(
|
|
5
|
-
:protocol, :key, :
|
|
5
|
+
:protocol, :key, :lane, :owner, :path, :format,
|
|
6
6
|
:uid, :etag, :schema_ref, :meta, :body, :content, :freshness
|
|
7
7
|
) do
|
|
8
8
|
# rubocop:disable Metrics/ParameterLists
|
|
@@ -11,7 +11,7 @@ module Textus
|
|
|
11
11
|
new(
|
|
12
12
|
protocol: Textus::PROTOCOL,
|
|
13
13
|
key: key,
|
|
14
|
-
|
|
14
|
+
lane: mentry.lane,
|
|
15
15
|
owner: mentry.owner,
|
|
16
16
|
path: path,
|
|
17
17
|
format: mentry.format,
|
|
@@ -34,7 +34,7 @@ module Textus
|
|
|
34
34
|
h = {
|
|
35
35
|
"protocol" => protocol,
|
|
36
36
|
"key" => key,
|
|
37
|
-
"
|
|
37
|
+
"lane" => lane,
|
|
38
38
|
"owner" => owner,
|
|
39
39
|
"path" => path,
|
|
40
40
|
"format" => format,
|
|
@@ -49,6 +49,8 @@ module Textus
|
|
|
49
49
|
h
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
alias_method :zone, :lane
|
|
53
|
+
|
|
52
54
|
def stale?
|
|
53
55
|
return false if freshness.nil?
|
|
54
56
|
|
data/lib/textus/errors.rb
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
module Textus
|
|
2
|
+
ErrorInfo = Data.define(:details, :exit_code, :hint) do
|
|
3
|
+
def self.for(details: {}, exit_code: 1, hint: nil)
|
|
4
|
+
new(details: details, exit_code: exit_code, hint: hint)
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
2
8
|
class Error < StandardError
|
|
3
|
-
attr_reader :code
|
|
9
|
+
attr_reader :code
|
|
4
10
|
|
|
5
|
-
def initialize(code, message, details: {}, exit_code: 1, hint: nil)
|
|
11
|
+
def initialize(code, message, info: nil, details: {}, exit_code: 1, hint: nil)
|
|
6
12
|
super(message)
|
|
7
13
|
@code = code
|
|
8
|
-
@
|
|
9
|
-
@exit_code = exit_code
|
|
10
|
-
@hint = hint
|
|
14
|
+
@info = info || ErrorInfo.for(details:, exit_code:, hint:)
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
def details = @info.details
|
|
18
|
+
def exit_code = @info.exit_code
|
|
19
|
+
def hint = @info.hint
|
|
20
|
+
|
|
13
21
|
def to_envelope
|
|
14
22
|
env = {
|
|
15
23
|
"protocol" => Textus::PROTOCOL,
|
|
16
24
|
"ok" => false,
|
|
17
25
|
"code" => @code,
|
|
18
26
|
"message" => message,
|
|
19
|
-
"details" =>
|
|
27
|
+
"details" => details,
|
|
20
28
|
}
|
|
21
|
-
env["hint"] =
|
|
29
|
+
env["hint"] = hint if hint
|
|
22
30
|
env
|
|
23
31
|
end
|
|
24
32
|
end
|
|
@@ -97,7 +105,7 @@ module Textus
|
|
|
97
105
|
else
|
|
98
106
|
"no declared role"
|
|
99
107
|
end
|
|
100
|
-
details = { "key" => k, "
|
|
108
|
+
details = { "key" => k, "lane" => z }
|
|
101
109
|
details["verb"] = verb if verb
|
|
102
110
|
details["holders"] = holders if holders
|
|
103
111
|
super(
|
|
@@ -166,9 +174,9 @@ module Textus
|
|
|
166
174
|
def initialize(m, format: nil)
|
|
167
175
|
hint =
|
|
168
176
|
if format
|
|
169
|
-
"the template rendered invalid #{format}; try rendering with mock data and parsing the output before re-running
|
|
177
|
+
"the template rendered invalid #{format}; try rendering with mock data and parsing the output before re-running drain"
|
|
170
178
|
else
|
|
171
|
-
"the template rendered invalid content; try rendering with mock data and parsing the output before re-running
|
|
179
|
+
"the template rendered invalid content; try rendering with mock data and parsing the output before re-running drain"
|
|
172
180
|
end
|
|
173
181
|
super("bad_render", m, hint: hint)
|
|
174
182
|
end
|
|
@@ -177,7 +185,7 @@ module Textus
|
|
|
177
185
|
class PublishError < Error
|
|
178
186
|
def initialize(m, target: nil)
|
|
179
187
|
hint =
|
|
180
|
-
("file at #{target} wasn't published by textus; back it up and delete it, or move it under .textus/
|
|
188
|
+
("file at #{target} wasn't published by textus; back it up and delete it, or move it under .textus/data/" if target)
|
|
181
189
|
super("publish_error", m, details: target ? { "target" => target } : {}, hint: hint)
|
|
182
190
|
end
|
|
183
191
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Events
|
|
3
|
+
ENTRY_WRITTEN = "entry.written"
|
|
4
|
+
ENTRY_DELETED = "entry.deleted"
|
|
5
|
+
ENTRY_RENAMED = "entry.renamed"
|
|
6
|
+
ENTRY_FETCHED = "entry.fetched"
|
|
7
|
+
ENTRY_DERIVED = "entry.derived"
|
|
8
|
+
ENTRY_VALIDATED = "entry.validated"
|
|
9
|
+
ENTRY_PUBLISHED = "entry.published"
|
|
10
|
+
PIPELINE_FAILED = "pipeline.failed"
|
|
11
|
+
|
|
12
|
+
STEP_FETCH_COMPLETE = "step.fetch.complete"
|
|
13
|
+
STEP_TRANSFORM_COMPLETE = "step.transform.complete"
|
|
14
|
+
STEP_VALIDATE_PASSED = "step.validate.passed"
|
|
15
|
+
STEP_VALIDATE_FAILED = "step.validate.failed"
|
|
16
|
+
|
|
17
|
+
STORE_LOADED = "store.loaded"
|
|
18
|
+
SESSION_OPENED = "session.opened"
|
|
19
|
+
SESSION_CLOSED = "session.closed"
|
|
20
|
+
end
|
|
21
|
+
end
|