textus 0.52.0 → 0.54.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 +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -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 +25 -41
- 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/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- 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 +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- 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/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- 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 +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -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 +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- 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/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- 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/handler_allowlist.rb +0 -17
- 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/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- 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/retention/apply.rb +0 -52
- 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/manifest/entry/validators/events.rb +0 -22
- 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/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- 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
data/lib/textus/cli/verb.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "optparse"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class CLI
|
|
6
|
-
# Subclasses must implement #call(store) and return an integer exit code.
|
|
7
|
-
# Use #emit(obj) for normal JSON output (returns 0).
|
|
8
|
-
# Subclasses that don't need a Textus store (e.g. Init) override
|
|
9
|
-
# `.needs_store?` to return false; dispatch will pass nil instead.
|
|
10
|
-
class Verb
|
|
11
|
-
class << self
|
|
12
|
-
def option(name, optspec)
|
|
13
|
-
options << [name, optspec]
|
|
14
|
-
attr_accessor(name)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def options
|
|
18
|
-
@options ||= []
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def needs_store?
|
|
22
|
-
true
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Declarative CLI name. Reader returns the registered name (or nil
|
|
26
|
-
# for verbs that aren't directly invokable, like the abstract
|
|
27
|
-
# Verb/Group base classes). Writer registers it.
|
|
28
|
-
def command_name(name = nil)
|
|
29
|
-
if name.nil?
|
|
30
|
-
@command_name
|
|
31
|
-
else
|
|
32
|
-
@command_name = name.to_s
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Declares that this verb is a subcommand of `group_klass`. When
|
|
37
|
-
# set, the verb is NOT a top-level CLI verb — it's listed under
|
|
38
|
-
# the group's subcommands instead.
|
|
39
|
-
def parent_group(group_klass = nil)
|
|
40
|
-
if group_klass.nil?
|
|
41
|
-
@parent_group
|
|
42
|
-
else
|
|
43
|
-
@parent_group = group_klass
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def inherited(subclass)
|
|
48
|
-
super
|
|
49
|
-
subclass.instance_variable_set(:@options, [])
|
|
50
|
-
subclass.instance_variable_set(:@command_name, nil)
|
|
51
|
-
subclass.instance_variable_set(:@parent_group, nil)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Recursive subclass enumeration. Ruby 3.1 ships Class#subclasses
|
|
55
|
-
# but not Class#descendants, so we expand it ourselves.
|
|
56
|
-
def descendants
|
|
57
|
-
subclasses.flat_map { |k| [k] + k.descendants }
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def initialize(stdin:, stdout:, stderr:, cwd: nil)
|
|
62
|
-
@stdin = stdin
|
|
63
|
-
@stdout = stdout
|
|
64
|
-
@stderr = stderr
|
|
65
|
-
@cwd = cwd
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def parse(argv)
|
|
69
|
-
fmt = "json"
|
|
70
|
-
OptionParser.new do |o|
|
|
71
|
-
self.class.options.each do |name, optspec|
|
|
72
|
-
o.on(optspec) { |v| public_send(:"#{name}=", v) }
|
|
73
|
-
end
|
|
74
|
-
o.on("--output=FMT") { |v| fmt = v }
|
|
75
|
-
o.on("--format=FMT") { |_v| raise FlagRenamed.new("--format", "--output") }
|
|
76
|
-
end.permute!(argv)
|
|
77
|
-
raise UsageError.new("only --output=json is supported in v1") unless fmt == "json"
|
|
78
|
-
|
|
79
|
-
@positional = argv.dup
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
attr_reader :positional
|
|
83
|
-
|
|
84
|
-
# Hashes get "protocol" => PROTOCOL prepended unless they already
|
|
85
|
-
# carry one (Store envelopes do). Caller's value wins on collision.
|
|
86
|
-
def emit(obj, exit_code: 0)
|
|
87
|
-
payload = obj.is_a?(Hash) ? { "protocol" => PROTOCOL }.merge(obj) : obj
|
|
88
|
-
@stdout.puts(JSON.generate(payload))
|
|
89
|
-
exit_code
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Resolves the active role for this invocation. Honors the verb's
|
|
93
|
-
# `--as` flag if declared, then TEXTUS_ROLE, then the project default.
|
|
94
|
-
# Pass `default:` to override the fallback (e.g. MCPServe uses AGENT).
|
|
95
|
-
def resolved_role(store, default: Role::DEFAULT)
|
|
96
|
-
flag = respond_to?(:as_flag) ? as_flag : nil
|
|
97
|
-
Role.resolve(flag: flag, env: ENV, root: store.root, default: default)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Returns a Call value bound to the resolved role. Convenience for
|
|
101
|
-
# verbs whose only pre-call boilerplate is resolving the role and
|
|
102
|
-
# wrapping it in a Call.
|
|
103
|
-
def context_for(store)
|
|
104
|
-
Textus::Call.build(role: resolved_role(store))
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Returns a RoleScope bound to the resolved role.
|
|
108
|
-
def session_for(store)
|
|
109
|
-
store.as(resolved_role(store))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# The input stream — the source for a `cli_stdin` envelope (ADR 0068).
|
|
113
|
-
attr_reader :stdin
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
data/lib/textus/cli.rb
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "optparse"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class CLI
|
|
6
|
-
# Auto-derived verb table. Every CLI::Verb (or Group) subclass that
|
|
7
|
-
# declares `command_name "X"` and has no `parent_group` is a top-level
|
|
8
|
-
# verb. Sorted alphabetically for stable help output. Adding a new
|
|
9
|
-
# verb requires only a new file declaring its `command_name`.
|
|
10
|
-
#
|
|
11
|
-
# `k.name` gates out anonymous (Class.new) subclasses: real verbs are always
|
|
12
|
-
# named constants (generated Gen* or hand-authored classes), so this is a
|
|
13
|
-
# no-op in production but keeps throwaway test fixtures from leaking into the
|
|
14
|
-
# registry (and tripping the reconciliation guards order-dependently).
|
|
15
|
-
def self.verbs
|
|
16
|
-
Runner.install!
|
|
17
|
-
Verb.descendants
|
|
18
|
-
.select { |k| k.name && k.command_name && k.parent_group.nil? }
|
|
19
|
-
.sort_by(&:command_name)
|
|
20
|
-
.to_h { |k| [k.command_name, k] }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
24
|
-
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(stdin:, stdout:, stderr:, cwd:)
|
|
28
|
-
@stdin = stdin
|
|
29
|
-
@stdout = stdout
|
|
30
|
-
@stderr = stderr
|
|
31
|
-
@cwd = cwd
|
|
32
|
-
@root_arg = nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def run(argv)
|
|
36
|
-
# `--root` is a global, position-agnostic option: pull it out of argv
|
|
37
|
-
# wherever it appears so it works uniformly before OR after any verb or
|
|
38
|
-
# group (e.g. both `textus --root=X hook list` and
|
|
39
|
-
# `textus hook list --root=X`). Without this, `order!` below only sees
|
|
40
|
-
# options before the first verb token, so a trailing `--root` reached the
|
|
41
|
-
# verb's own parser and raised InvalidOption (#161 F5). TEXTUS_ROOT already
|
|
42
|
-
# works everywhere via Store.discover, so this brings the flag to parity.
|
|
43
|
-
@root_arg = extract_root!(argv)
|
|
44
|
-
|
|
45
|
-
# Define --version/--help ourselves so OptionParser doesn't intercept them
|
|
46
|
-
# with its built-in handlers (which print "version unknown" and a bare usage
|
|
47
|
-
# line, then exit before we ever reach the verb dispatch below).
|
|
48
|
-
show_version = false
|
|
49
|
-
show_help = false
|
|
50
|
-
OptionParser.new do |o|
|
|
51
|
-
o.on("--version", "-v") { show_version = true }
|
|
52
|
-
o.on("--help", "-h") { show_help = true }
|
|
53
|
-
end.order!(argv)
|
|
54
|
-
|
|
55
|
-
return @stdout.puts(VERSION) || 0 if show_version
|
|
56
|
-
return print_help || 0 if show_help
|
|
57
|
-
|
|
58
|
-
verb = argv.shift
|
|
59
|
-
raise UsageError.new("missing verb") if verb.nil?
|
|
60
|
-
|
|
61
|
-
klass = self.class.verbs[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
62
|
-
coerce_exit_code(dispatch(klass, argv))
|
|
63
|
-
rescue Textus::Error => e
|
|
64
|
-
emit_error(e)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
# Remove the first `--root=PATH` or `--root PATH` token from argv (anywhere)
|
|
70
|
-
# and return its value, or nil if absent. Mutates argv in place.
|
|
71
|
-
def extract_root!(argv)
|
|
72
|
-
i = argv.index { |a| a == "--root" || a.start_with?("--root=") }
|
|
73
|
-
return nil unless i
|
|
74
|
-
|
|
75
|
-
tok = argv[i]
|
|
76
|
-
if tok.start_with?("--root=")
|
|
77
|
-
argv.delete_at(i)
|
|
78
|
-
tok.delete_prefix("--root=")
|
|
79
|
-
else
|
|
80
|
-
val = argv[i + 1]
|
|
81
|
-
raise UsageError.new("--root requires a PATH") if val.nil? || val.start_with?("-")
|
|
82
|
-
|
|
83
|
-
argv.delete_at(i + 1)
|
|
84
|
-
argv.delete_at(i)
|
|
85
|
-
val
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def coerce_exit_code(value)
|
|
90
|
-
case value
|
|
91
|
-
when Integer then value
|
|
92
|
-
when true, nil then 0
|
|
93
|
-
when false then 1
|
|
94
|
-
else
|
|
95
|
-
@stderr.puts("warning: verb returned non-Integer #{value.class}; treating as 0")
|
|
96
|
-
0
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def store
|
|
101
|
-
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def dispatch(klass, argv)
|
|
105
|
-
v = klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
106
|
-
v.parse(argv)
|
|
107
|
-
v.call(klass.needs_store? ? store : nil)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def emit_error(err)
|
|
111
|
-
@stdout.puts(JSON.generate(err.to_envelope))
|
|
112
|
-
@stderr.puts("#{err.code}: #{err.message}")
|
|
113
|
-
@stderr.puts(" → #{err.hint}") if err.hint
|
|
114
|
-
err.exit_code
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def print_help
|
|
118
|
-
@stdout.puts <<~HELP
|
|
119
|
-
textus #{VERSION} — reference implementation of #{PROTOCOL}
|
|
120
|
-
|
|
121
|
-
Usage (json output is the default):
|
|
122
|
-
textus list [--prefix=KEY] [--zone=Z]
|
|
123
|
-
textus where KEY
|
|
124
|
-
textus get KEY
|
|
125
|
-
textus put KEY --stdin --as=ROLE
|
|
126
|
-
textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]
|
|
127
|
-
textus blame KEY [--limit=N]
|
|
128
|
-
textus doctor
|
|
129
|
-
textus boot
|
|
130
|
-
|
|
131
|
-
textus key {delete,mv,uid}
|
|
132
|
-
textus rule {explain,lint,list}
|
|
133
|
-
textus schema {diff,init,migrate,show}
|
|
134
|
-
textus hook {list,run}
|
|
135
|
-
HELP
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
data/lib/textus/dispatcher.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Static verb → use-case map. Canonical lookup as of 0.27.0; replaces the
|
|
3
|
-
# Application::UseCase registry whose entries were populated by file-load
|
|
4
|
-
# side effects in 0.26.x.
|
|
5
|
-
module Dispatcher
|
|
6
|
-
VERBS = {
|
|
7
|
-
# Write
|
|
8
|
-
put: Textus::Write::Put,
|
|
9
|
-
propose: Textus::Write::Propose,
|
|
10
|
-
key_delete: Textus::Write::KeyDelete,
|
|
11
|
-
key_mv: Textus::Write::KeyMv,
|
|
12
|
-
accept: Textus::Write::Accept,
|
|
13
|
-
reject: Textus::Write::Reject,
|
|
14
|
-
enqueue: Textus::Write::Enqueue,
|
|
15
|
-
# Read
|
|
16
|
-
get: Textus::Read::Get,
|
|
17
|
-
list: Textus::Read::List,
|
|
18
|
-
where: Textus::Read::Where,
|
|
19
|
-
uid: Textus::Read::Uid,
|
|
20
|
-
blame: Textus::Read::Blame,
|
|
21
|
-
audit: Textus::Read::Audit,
|
|
22
|
-
freshness: Textus::Read::Freshness,
|
|
23
|
-
deps: Textus::Read::Deps,
|
|
24
|
-
rdeps: Textus::Read::Rdeps,
|
|
25
|
-
pulse: Textus::Read::Pulse,
|
|
26
|
-
rule_explain: Textus::Read::RuleExplain,
|
|
27
|
-
rule_list: Textus::Read::RuleList,
|
|
28
|
-
published: Textus::Read::Published,
|
|
29
|
-
schema_show: Textus::Read::SchemaEnvelope,
|
|
30
|
-
validate_all: Textus::Read::ValidateAll,
|
|
31
|
-
doctor: Textus::Read::Doctor,
|
|
32
|
-
boot: Textus::Read::Boot,
|
|
33
|
-
capabilities: Textus::Read::Capabilities,
|
|
34
|
-
jobs: Textus::Read::Jobs,
|
|
35
|
-
|
|
36
|
-
# Maintenance
|
|
37
|
-
zone_mv: Textus::Maintenance::ZoneMv,
|
|
38
|
-
key_mv_prefix: Textus::Maintenance::KeyMvPrefix,
|
|
39
|
-
key_delete_prefix: Textus::Maintenance::KeyDeletePrefix,
|
|
40
|
-
drain: Textus::Maintenance::Drain,
|
|
41
|
-
rule_lint: Textus::Maintenance::RuleLint,
|
|
42
|
-
}.freeze
|
|
43
|
-
|
|
44
|
-
def self.fetch(verb)
|
|
45
|
-
VERBS.fetch(verb.to_sym) { raise UsageError.new("unknown verb: #{verb.inspect}") }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Single home for the uniform use-case invocation protocol (ADR 0023):
|
|
49
|
-
# look up the verb, construct on (container:, call:), and invoke #call.
|
|
50
|
-
def self.invoke(verb, container:, call:, args: [], kwargs: {})
|
|
51
|
-
fetch(verb).new(container: container, call: call).call(*args, **kwargs)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# For every entry with an `intake.handler`, look up its handler_allowlist
|
|
5
|
-
# policy (if any) and verify the declared handler is allowed. Emits a
|
|
6
|
-
# failure when the handler is rejected by policy.
|
|
7
|
-
class HandlerAllowlist < Check
|
|
8
|
-
def call
|
|
9
|
-
out = []
|
|
10
|
-
manifest.data.entries.each do |mentry|
|
|
11
|
-
next unless mentry.intake?
|
|
12
|
-
|
|
13
|
-
handler = mentry.handler
|
|
14
|
-
|
|
15
|
-
allow = manifest.rules.for(mentry.key).handler_allowlist
|
|
16
|
-
next if allow.nil?
|
|
17
|
-
next if allow.allows?(handler)
|
|
18
|
-
|
|
19
|
-
out << {
|
|
20
|
-
"code" => "policy.handler_not_allowed",
|
|
21
|
-
"level" => "error",
|
|
22
|
-
"subject" => mentry.key,
|
|
23
|
-
"message" => "entry '#{mentry.key}' declares intake.handler='#{handler}' but the " \
|
|
24
|
-
"handler_allowlist policy permits only: #{allow.handlers.join(", ")}",
|
|
25
|
-
"fix" => "either change intake.handler to one of [#{allow.handlers.join(", ")}], " \
|
|
26
|
-
"or extend the handler_allowlist policy in .textus/manifest.yaml",
|
|
27
|
-
}
|
|
28
|
-
end
|
|
29
|
-
out
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Hooks < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
dir = File.join(root, "hooks")
|
|
8
|
-
return out unless File.directory?(dir)
|
|
9
|
-
|
|
10
|
-
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
-
events = Textus::Hooks::EventBus.new
|
|
12
|
-
rpc = Textus::Hooks::RpcRegistry.new
|
|
13
|
-
Textus.drain_hook_blocks
|
|
14
|
-
begin
|
|
15
|
-
load(f)
|
|
16
|
-
Textus.drain_hook_blocks.each { |b| b.call(Textus::Hooks::Loader::Dsl.new(events: events, rpc: rpc)) }
|
|
17
|
-
end
|
|
18
|
-
rescue StandardError, ScriptError => e
|
|
19
|
-
out << {
|
|
20
|
-
"code" => "hook.load_failed",
|
|
21
|
-
"level" => "error",
|
|
22
|
-
"subject" => File.basename(f),
|
|
23
|
-
"message" => "#{e.class}: #{e.message}",
|
|
24
|
-
"fix" => "open #{f} and fix the syntax/load error",
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
out
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class IntakeRegistration < Check
|
|
5
|
-
BUILTIN = %i[json csv markdown-links ical-events rss].freeze
|
|
6
|
-
|
|
7
|
-
def call
|
|
8
|
-
declared = collect_declared_handlers
|
|
9
|
-
registered = rpc.names(:resolve_handler).to_set
|
|
10
|
-
|
|
11
|
-
out = (declared - registered).map do |name|
|
|
12
|
-
{
|
|
13
|
-
"code" => "intake.handler_missing",
|
|
14
|
-
"level" => "error",
|
|
15
|
-
"subject" => name.to_s,
|
|
16
|
-
"message" => "manifest references intake handler '#{name}' but no resolve_handler hook for '#{name}' is registered",
|
|
17
|
-
"fix" => "create .textus/hooks/#{name}.rb with `Textus.hook { |reg| reg.on(:resolve_handler, :#{name}) { ... } }`",
|
|
18
|
-
}
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
(registered - declared - BUILTIN.to_set).each do |name|
|
|
22
|
-
out << {
|
|
23
|
-
"code" => "intake.handler_orphan",
|
|
24
|
-
"level" => "warning",
|
|
25
|
-
"subject" => name.to_s,
|
|
26
|
-
"message" => "resolve_handler hook '#{name}' is registered but no manifest entry references it",
|
|
27
|
-
"fix" => "remove the unused handler, or add an entry with `intake.handler: #{name}`",
|
|
28
|
-
}
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
out
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def collect_declared_handlers
|
|
37
|
-
set = Set.new
|
|
38
|
-
manifest.data.entries.each do |mentry|
|
|
39
|
-
set << mentry.handler.to_sym if mentry.intake?
|
|
40
|
-
end
|
|
41
|
-
set
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
data/lib/textus/domain/action.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Jobs
|
|
4
|
-
# Closed allow-list of runnable job types. The general `enqueue` surface
|
|
5
|
-
# (a later phase) can only push types registered here — that is the safety
|
|
6
|
-
# boundary that stops the "general runner" from running arbitrary code.
|
|
7
|
-
class Registry
|
|
8
|
-
Entry = Struct.new(:handler, :max_attempts, :required_role, keyword_init: true)
|
|
9
|
-
|
|
10
|
-
def initialize
|
|
11
|
-
@entries = {}
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# required_role: a role the caller must hold to enqueue this type via the
|
|
15
|
-
# general `enqueue` surface (nil = any caller). The closed allow-list is
|
|
16
|
-
# the primary safety boundary; this is defence-in-depth for destructive
|
|
17
|
-
# types.
|
|
18
|
-
def register(type, handler:, max_attempts: 3, required_role: nil)
|
|
19
|
-
@entries[type.to_s] = Entry.new(handler: handler, max_attempts: max_attempts, required_role: required_role)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def registered?(type)
|
|
23
|
-
@entries.key?(type.to_s)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def lookup(type)
|
|
27
|
-
@entries.fetch(type.to_s) do
|
|
28
|
-
raise Textus::UsageError.new(
|
|
29
|
-
"unregistered job type '#{type}'",
|
|
30
|
-
hint: "register the type in Domain::Jobs::Registry before enqueueing it",
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
# The CLOSED floor (ADR 0031 §4): predicate names every transition
|
|
7
|
-
# evaluates regardless of rules:. rules[].guard only ADDS to these.
|
|
8
|
-
module BaseGuards
|
|
9
|
-
# The minimal floor — only what the verb is meaningless without.
|
|
10
|
-
# schema_valid / etag_match / fresh_within are NOT here: they are
|
|
11
|
-
# composable-only, added per-key via rules[].guard (ADR 0031).
|
|
12
|
-
BASE = {
|
|
13
|
-
put: %w[zone_writable_by],
|
|
14
|
-
key_delete: %w[zone_writable_by],
|
|
15
|
-
key_mv: %w[zone_writable_by],
|
|
16
|
-
accept: %w[author_held target_is_canon],
|
|
17
|
-
reject: %w[author_held],
|
|
18
|
-
converge: %w[zone_writable_by],
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
def self.for(transition) = BASE.fetch(transition, [])
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
# Immutable context handed to every predicate. `manifest` is the
|
|
7
|
-
# manifest (pure, no I/O); `envelope` is the entry under evaluation
|
|
8
|
-
# (nil when no bytes exist yet, e.g. a fresh put). `origin`/`target`
|
|
9
|
-
# are dotted keys; `transition` is the verb symbol.
|
|
10
|
-
Evaluation = Data.define(
|
|
11
|
-
:actor, :transition, :origin, :target, :envelope, :manifest
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
# An ordered list of pure predicates over one Evaluation (ADR 0031).
|
|
7
|
-
# check! short-circuits on the first failing predicate that defines a
|
|
8
|
-
# bespoke #error (only zone_writable_by → WriteForbidden, the product's
|
|
9
|
-
# legible topology refusal); every other failure accumulates into
|
|
10
|
-
# GuardFailed naming the unmet predicate(s).
|
|
11
|
-
class Guard
|
|
12
|
-
attr_reader :predicates
|
|
13
|
-
|
|
14
|
-
def initialize(predicates)
|
|
15
|
-
@predicates = predicates
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def check!(eval)
|
|
19
|
-
accumulated = []
|
|
20
|
-
@predicates.each do |pred|
|
|
21
|
-
next if pred.call(eval)
|
|
22
|
-
raise pred.error(eval) if pred.respond_to?(:error)
|
|
23
|
-
|
|
24
|
-
accumulated << [pred.name, pred.reason]
|
|
25
|
-
end
|
|
26
|
-
raise Textus::GuardFailed.new(accumulated) unless accumulated.empty?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def explain(eval)
|
|
30
|
-
@predicates.map { |p| [p.name, p.call(eval), p.reason] }
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
# Builds the effective Guard for (transition, key): base floor ++
|
|
7
|
-
# the predicates declared under rules[].guard[transition]. The single
|
|
8
|
-
# place the closed floor and the open ceiling are composed.
|
|
9
|
-
class GuardFactory
|
|
10
|
-
def initialize(manifest:, schemas:, extra: {})
|
|
11
|
-
@manifest = manifest
|
|
12
|
-
@schemas = schemas
|
|
13
|
-
@extra = extra # transient per-call params, e.g. { if_etag: "..." }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def for(transition, key)
|
|
17
|
-
specs = BaseGuards.for(transition) + composed(transition, key)
|
|
18
|
-
predicates = specs.map { |spec| build(spec) }.uniq(&:name)
|
|
19
|
-
Guard.new(predicates)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def composed(transition, key)
|
|
25
|
-
guard_map = @manifest.rules.for(key).guard
|
|
26
|
-
return [] if guard_map.nil?
|
|
27
|
-
|
|
28
|
-
Array(guard_map[transition.to_s])
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def build(spec)
|
|
32
|
-
# etag_match takes a per-call param rather than a manifest one.
|
|
33
|
-
return Predicates::EtagMatch.new(if_etag: @extra[:if_etag]) if spec == "etag_match"
|
|
34
|
-
|
|
35
|
-
Predicates::Registry.build(spec, schemas: @schemas)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
class HandlerAllowlist
|
|
5
|
-
attr_reader :handlers
|
|
6
|
-
|
|
7
|
-
def initialize(handlers:)
|
|
8
|
-
@handlers = Array(handlers).map(&:to_s).freeze
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def allows?(handler)
|
|
12
|
-
@handlers.include?(handler.to_s)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# Predicate: the acting role must hold the 'author' capability in the
|
|
8
|
-
# active manifest (ADR 0030 capability roles). Folds in the old
|
|
9
|
-
# Write::AuthorityGate so accept/reject and rules[].guard share one
|
|
10
|
-
# implementation. No bespoke #error — failures accumulate into
|
|
11
|
-
# GuardFailed (ADR 0031).
|
|
12
|
-
class AuthorHeld
|
|
13
|
-
attr_reader :reason
|
|
14
|
-
|
|
15
|
-
def name = "author_held"
|
|
16
|
-
|
|
17
|
-
def call(eval)
|
|
18
|
-
holders = eval.manifest.policy.roles_with_capability("author")
|
|
19
|
-
return true if holders.include?(eval.actor.to_s)
|
|
20
|
-
|
|
21
|
-
@reason =
|
|
22
|
-
if holders.empty?
|
|
23
|
-
"no role holds the 'author' capability; #{eval.transition} is disabled"
|
|
24
|
-
else
|
|
25
|
-
"role '#{eval.actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
|
|
26
|
-
end
|
|
27
|
-
false
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# Advisory pre-flight etag check for policy explain. The
|
|
8
|
-
# authoritative compare-and-write stays in Envelope::IO::Writer
|
|
9
|
-
# (atomic write-then-audit, ADR 0017). Passes when no if_etag is
|
|
10
|
-
# supplied (params[:if_etag] nil) — guard does not require it.
|
|
11
|
-
class EtagMatch
|
|
12
|
-
attr_reader :reason
|
|
13
|
-
|
|
14
|
-
def initialize(if_etag: nil)
|
|
15
|
-
@if_etag = if_etag
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def name = "etag_match"
|
|
19
|
-
|
|
20
|
-
def call(eval)
|
|
21
|
-
return true if @if_etag.nil?
|
|
22
|
-
return true if eval.envelope.nil? # creating; Writer handles race
|
|
23
|
-
return true if eval.envelope.etag == @if_etag
|
|
24
|
-
|
|
25
|
-
@reason = "etag mismatch: wanted #{@if_etag}, have #{eval.envelope.etag}"
|
|
26
|
-
false
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|