textus 0.51.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +13 -13
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +19 -11
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +17 -63
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/role.rb +1 -1
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -4
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +133 -107
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -69
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -1,34 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "open3"
|
|
2
4
|
|
|
3
5
|
module Textus
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
# author, date, subject) that introduced the file state at that audit
|
|
7
|
-
# row. Falls back to `git => nil` when not in a git repo or when the
|
|
8
|
-
# file is untracked.
|
|
9
|
-
class Blame
|
|
6
|
+
module Action
|
|
7
|
+
class Blame < Base
|
|
10
8
|
extend Textus::Contract::DSL
|
|
11
9
|
|
|
12
|
-
verb
|
|
13
|
-
summary
|
|
10
|
+
verb :blame
|
|
11
|
+
summary "Annotate audit rows for a key with the git commit that introduced each file state."
|
|
14
12
|
surfaces :cli
|
|
15
|
-
cli
|
|
16
|
-
arg :key,
|
|
13
|
+
cli "blame"
|
|
14
|
+
arg :key, String, required: true, positional: true, description: "entry key to blame"
|
|
17
15
|
arg :limit, Integer, required: false, description: "maximum number of audit rows to return"
|
|
18
16
|
view(:cli) { |rows, inputs| { "verb" => "blame", "key" => inputs[:key], "rows" => rows } }
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:, limit: nil)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
@limit = limit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(container:, **)
|
|
21
27
|
@container = container
|
|
22
|
-
@manifest
|
|
23
|
-
@root
|
|
28
|
+
@manifest = container.manifest
|
|
29
|
+
@root = container.root
|
|
30
|
+
|
|
31
|
+
audit_rows = Textus::Action::Audit.new(key: @key, limit: @limit).call(container: container)
|
|
32
|
+
path = resolve_path(@key)
|
|
33
|
+
return audit_rows.map { |row| row.merge("git" => nil) } unless git_tracked?(path)
|
|
34
|
+
|
|
35
|
+
audit_rows.map { |row| row.merge("git" => git_commit_at(path, timestamp: row["ts"])) }
|
|
24
36
|
end
|
|
25
37
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
path = resolve_path(key)
|
|
29
|
-
return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
|
|
38
|
+
def self.new(*args, **kwargs)
|
|
39
|
+
return super(**kwargs) unless args.any?
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
42
|
+
mapped = positional.zip(args).to_h
|
|
43
|
+
super(**mapped.merge(kwargs))
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
private
|
|
@@ -37,9 +49,6 @@ module Textus
|
|
|
37
49
|
res = @manifest.resolver.resolve(key)
|
|
38
50
|
mentry = res.entry
|
|
39
51
|
path = res.path
|
|
40
|
-
# Nested entries resolve to a file under the entry path; leaf entries
|
|
41
|
-
# already have a fully-resolved path. Either way `path` is what git
|
|
42
|
-
# needs to know about.
|
|
43
52
|
path || Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
44
53
|
rescue Textus::Error
|
|
45
54
|
nil
|
|
@@ -60,7 +69,6 @@ module Textus
|
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
def git_repo?
|
|
63
|
-
# Walk up from store root to find a .git directory.
|
|
64
72
|
dir = @root
|
|
65
73
|
loop do
|
|
66
74
|
return true if File.directory?(File.join(dir, ".git"))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Boot < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :boot
|
|
9
|
+
summary "Return the orientation contract: zones, entries, schemas, write_flows, agent_quickstart."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :lean, :boolean,
|
|
12
|
+
description: "return only orientation essentials (zones, agent_quickstart, contract_etag) for cheap session-start injection"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(lean: nil)
|
|
17
|
+
super()
|
|
18
|
+
@lean = lean
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, **)
|
|
22
|
+
Textus::Boot.build(container: container, lean: !@lean.nil?)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Action
|
|
7
|
+
class DataMv < Base
|
|
8
|
+
extend Textus::Contract::DSL
|
|
9
|
+
|
|
10
|
+
verb :data_mv
|
|
11
|
+
summary "Rename a data lane — manifest + files. Refuses if destination exists."
|
|
12
|
+
surfaces :cli, :mcp
|
|
13
|
+
cli "data mv"
|
|
14
|
+
arg :from, String, required: true, positional: true, description: "current data lane name"
|
|
15
|
+
arg :to, String, required: true, positional: true,
|
|
16
|
+
description: "new data lane name; refused if a lane by this name already exists"
|
|
17
|
+
arg :dry_run, :boolean, default: false,
|
|
18
|
+
description: "when true, returns the planned zone move without applying it; " \
|
|
19
|
+
"defaults to false, so omitting it applies the move immediately"
|
|
20
|
+
view { |v, _i| v.to_h }
|
|
21
|
+
|
|
22
|
+
BURN = :sync
|
|
23
|
+
|
|
24
|
+
def initialize(from:, to:, dry_run: false)
|
|
25
|
+
super()
|
|
26
|
+
@from = from
|
|
27
|
+
@to = to
|
|
28
|
+
@dry_run = dry_run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(container:, **)
|
|
32
|
+
manifest = container.manifest
|
|
33
|
+
root = container.root
|
|
34
|
+
|
|
35
|
+
raise UsageError.new("from and to required") if @from.nil? || @to.nil? || @from.empty? || @to.empty?
|
|
36
|
+
raise UsageError.new("data lane '#{@from}' not declared") unless manifest.data.declared_lane_kinds.key?(@from)
|
|
37
|
+
|
|
38
|
+
dest_dir = File.join(root, "data", @to)
|
|
39
|
+
raise UsageError.new("destination 'data/#{@to}' already exists") if File.exist?(dest_dir)
|
|
40
|
+
|
|
41
|
+
affected_keys = manifest.data.entries.select { |entry| entry.lane == @from }.map(&:key)
|
|
42
|
+
|
|
43
|
+
steps = [{ "op" => "rename_zone", "from" => @from, "to" => @to }]
|
|
44
|
+
steps += affected_keys.map do |key|
|
|
45
|
+
{ "op" => "mv", "from" => key, "to" => "#{@to}#{key[@from.length..]}" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
plan = Textus::Background::Plan.new(steps: steps, warnings: [])
|
|
49
|
+
return plan if @dry_run
|
|
50
|
+
|
|
51
|
+
rewrite_manifest!(root)
|
|
52
|
+
FileUtils.mv(File.join(root, "data", @from), dest_dir)
|
|
53
|
+
plan
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def rewrite_manifest!(root)
|
|
59
|
+
path = File.join(root, "manifest.yaml")
|
|
60
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
61
|
+
raw["lanes"].each { |lane| lane["name"] = @to if lane["name"] == @from }
|
|
62
|
+
raw["entries"].each do |entry|
|
|
63
|
+
entry["lane"] = @to if entry["lane"] == @from
|
|
64
|
+
entry["key"] = entry["key"].sub(/\A#{Regexp.escape(@from)}(\.|\z)/, "#{@to}\\1")
|
|
65
|
+
entry["path"] = entry["path"].sub(%r{\A(data/)?#{Regexp.escape(@from)}(/|\z)}, "\\1#{@to}\\2")
|
|
66
|
+
end
|
|
67
|
+
File.write(path, YAML.dump(raw))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Deps < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :deps
|
|
9
|
+
summary "List the keys a derived entry depends on (its projection/external sources)."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted key of the derived entry whose source keys you want"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(key:)
|
|
17
|
+
super()
|
|
18
|
+
@key = key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, **)
|
|
22
|
+
entry = container.manifest.data.entries.find { |e| e.key == @key }
|
|
23
|
+
deps =
|
|
24
|
+
if entry&.derived?
|
|
25
|
+
src = entry.source
|
|
26
|
+
if src.projection?
|
|
27
|
+
Array(src.select).compact
|
|
28
|
+
elsif src.external?
|
|
29
|
+
Array(src.sources).compact
|
|
30
|
+
else
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
{ "key" => @key, "deps" => deps.uniq }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.new(*args, **kwargs)
|
|
40
|
+
return super(**kwargs) unless args.any?
|
|
41
|
+
|
|
42
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
43
|
+
mapped = positional.zip(args).to_h
|
|
44
|
+
super(**mapped.merge(kwargs))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Doctor < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :doctor
|
|
9
|
+
summary "Run health checks on the textus store and report any issues."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "doctor"
|
|
12
|
+
arg :checks, Array, required: false, description: "subset of check names to run (default: all)"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(checks: nil)
|
|
17
|
+
super()
|
|
18
|
+
@checks = checks
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, call:, **)
|
|
22
|
+
Textus::Doctor.build(container: container, checks: @checks, role: call.role)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Drain < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :drain
|
|
9
|
+
summary "Seed refresh + sweep jobs then drain the queue to empty. " \
|
|
10
|
+
"Identical to one Watcher tick. Use when no watcher is running."
|
|
11
|
+
surfaces :cli, :mcp
|
|
12
|
+
arg :prefix, String, description: "restrict to keys under this dotted prefix"
|
|
13
|
+
arg :lane, String, description: "restrict to entries in this lane"
|
|
14
|
+
|
|
15
|
+
BURN = :sync
|
|
16
|
+
|
|
17
|
+
def initialize(prefix: nil, lane: nil)
|
|
18
|
+
super()
|
|
19
|
+
@prefix = prefix
|
|
20
|
+
@lane = lane
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(container:, call:)
|
|
24
|
+
queue = Textus::Ports::Queue.new(root: container.root)
|
|
25
|
+
Textus::Background::Planner::Plan.seed(
|
|
26
|
+
container: container,
|
|
27
|
+
queue: queue,
|
|
28
|
+
role: call.role,
|
|
29
|
+
)
|
|
30
|
+
queue.reclaim(now: Textus::Ports::Clock.new.now)
|
|
31
|
+
summary = Textus::Background::Worker.for(container:, queue:).drain
|
|
32
|
+
{
|
|
33
|
+
"protocol" => Textus::PROTOCOL,
|
|
34
|
+
"ok" => summary.failed.zero?,
|
|
35
|
+
"completed" => summary.completed,
|
|
36
|
+
"failed" => summary.failed,
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Enqueue < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :enqueue
|
|
9
|
+
summary "Push a registered job type onto the convergence queue, to be run by drain/serve."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "enqueue"
|
|
12
|
+
arg :type, String, required: true, positional: true,
|
|
13
|
+
description: "registered job type (e.g. materialize, re-pull, sweep)"
|
|
14
|
+
arg :args, Hash, default: {},
|
|
15
|
+
description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
|
|
16
|
+
|
|
17
|
+
BURN = :sync
|
|
18
|
+
|
|
19
|
+
def initialize(type:, args: {})
|
|
20
|
+
super()
|
|
21
|
+
@type = type
|
|
22
|
+
@job_args = args
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def args
|
|
26
|
+
{ type: @type, args: @job_args }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(container:, call:)
|
|
30
|
+
action_class = begin
|
|
31
|
+
Textus::Background::Job.fetch(@type.to_s)
|
|
32
|
+
rescue Textus::UsageError
|
|
33
|
+
raise Textus::UsageError.new("unregistered job type '#{@type}'")
|
|
34
|
+
end
|
|
35
|
+
if action_class.const_defined?(:REQUIRED_ROLE) && call.role != action_class::REQUIRED_ROLE
|
|
36
|
+
raise Textus::Error.new(
|
|
37
|
+
"forbidden",
|
|
38
|
+
"role '#{call.role}' is not authorized to enqueue this job type (requires '#{action_class::REQUIRED_ROLE}')",
|
|
39
|
+
details: { "role" => call.role, "required_role" => action_class::REQUIRED_ROLE },
|
|
40
|
+
exit_code: 77,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
job = Textus::Ports::Queue::Job.new(
|
|
45
|
+
type: @type,
|
|
46
|
+
args: @job_args,
|
|
47
|
+
enqueued_by: call.role,
|
|
48
|
+
max_attempts: 3,
|
|
49
|
+
)
|
|
50
|
+
Textus::Ports::Queue.new(root: container.root).enqueue(job)
|
|
51
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Get < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :get
|
|
9
|
+
summary "Read one entry - a pure on-disk read annotated with a freshness " \
|
|
10
|
+
"verdict; never ingests (quarantine freshness is drain + hook " \
|
|
11
|
+
"only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
|
|
12
|
+
"freshness)."
|
|
13
|
+
surfaces :cli, :mcp
|
|
14
|
+
arg :key, String, required: true, positional: true,
|
|
15
|
+
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
16
|
+
view { |v, _i| v.to_h_for_wire }
|
|
17
|
+
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(container:, call:, file_stat: Textus::Ports::Storage::FileStat.new)
|
|
26
|
+
@container = container
|
|
27
|
+
@call = call
|
|
28
|
+
@manifest = container.manifest
|
|
29
|
+
@file_store = container.file_store
|
|
30
|
+
@file_stat = file_stat
|
|
31
|
+
annotated_envelope(@key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.new(*args, **kwargs)
|
|
35
|
+
return super(**kwargs) unless args.any?
|
|
36
|
+
|
|
37
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
38
|
+
mapped = positional.zip(args).to_h
|
|
39
|
+
super(**mapped.merge(kwargs))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def annotated_envelope(key)
|
|
45
|
+
envelope = read_raw_envelope(key)
|
|
46
|
+
return nil if envelope.nil?
|
|
47
|
+
|
|
48
|
+
entry = @manifest.resolver.resolve(key).entry
|
|
49
|
+
envelope.with(freshness: evaluator.verdict(entry))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def evaluator
|
|
53
|
+
@evaluator ||= Textus::Core::Freshness::Evaluator.new(
|
|
54
|
+
manifest: @manifest,
|
|
55
|
+
file_stat: @file_stat,
|
|
56
|
+
clock: @call,
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_raw_envelope(key)
|
|
61
|
+
res = @manifest.resolver.resolve(key)
|
|
62
|
+
mentry = res.entry
|
|
63
|
+
path = res.path
|
|
64
|
+
return nil unless @file_store.exists?(path)
|
|
65
|
+
|
|
66
|
+
raw = @file_store.read(path)
|
|
67
|
+
parsed = Textus::Entry.for_format(mentry.format).parse(raw, path: path)
|
|
68
|
+
Textus::Envelope.build(
|
|
69
|
+
key: key,
|
|
70
|
+
mentry: mentry,
|
|
71
|
+
path: path,
|
|
72
|
+
meta: parsed["_meta"],
|
|
73
|
+
body: parsed["body"],
|
|
74
|
+
etag: Textus::Etag.for_bytes(raw),
|
|
75
|
+
content: parsed["content"],
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Jobs < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :jobs
|
|
9
|
+
summary "List queued jobs by state; retry a dead-lettered job or purge a state."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "jobs"
|
|
12
|
+
arg :state, String, default: "ready", description: "ready|leased|done|failed"
|
|
13
|
+
arg :action, String, default: nil, description: "retry|purge (optional)"
|
|
14
|
+
arg :job_id, String, default: nil, description: "job id (required for action=retry)"
|
|
15
|
+
|
|
16
|
+
BURN = :sync
|
|
17
|
+
|
|
18
|
+
def initialize(state: "ready", action: nil, job_id: nil)
|
|
19
|
+
super()
|
|
20
|
+
@state = state
|
|
21
|
+
@action = action
|
|
22
|
+
@job_id = job_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(container:, **)
|
|
26
|
+
queue = Textus::Ports::Queue.new(root: container.root)
|
|
27
|
+
case @action
|
|
28
|
+
when "retry"
|
|
29
|
+
queue.retry_failed(@job_id)
|
|
30
|
+
when "purge"
|
|
31
|
+
queue.purge(@state)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "state" => @state, "jobs" => queue.list(@state) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyDelete < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_delete
|
|
9
|
+
summary "Delete one entry by key. Single-key, lower blast radius than key_delete_prefix; " \
|
|
10
|
+
"guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
|
|
11
|
+
surfaces :cli, :mcp
|
|
12
|
+
cli "key delete"
|
|
13
|
+
arg :key, String, required: true, positional: true,
|
|
14
|
+
description: "dotted entry key to delete"
|
|
15
|
+
arg :if_etag, String,
|
|
16
|
+
description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
|
|
17
|
+
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:, if_etag: nil)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
@if_etag = if_etag
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(container:, call:)
|
|
27
|
+
run_with_cascade(@key, container:, call:) do
|
|
28
|
+
Textus::Manifest::Data.validate_key!(@key)
|
|
29
|
+
mentry = container.manifest.resolver.resolve(@key).entry
|
|
30
|
+
|
|
31
|
+
auth(container).check_action!(action: :key_delete, actor: call.role, key: @key, extra: { if_etag: @if_etag })
|
|
32
|
+
|
|
33
|
+
writer(container, call).delete(@key, mentry:, if_etag: @if_etag)
|
|
34
|
+
|
|
35
|
+
container.steps.publish(
|
|
36
|
+
:entry_deleted,
|
|
37
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
38
|
+
key: @key,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => @key, "deleted" => true }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyDeletePrefix < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_delete_prefix
|
|
9
|
+
summary "Bulk-delete every leaf key under prefix."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "key delete-prefix"
|
|
12
|
+
arg :prefix, String, required: true, positional: true,
|
|
13
|
+
description: "every leaf key under this dotted prefix is deleted"
|
|
14
|
+
arg :dry_run, :boolean, default: false,
|
|
15
|
+
description: "when true, returns the keys that would be deleted without deleting them; " \
|
|
16
|
+
"defaults to false, so omitting it deletes immediately"
|
|
17
|
+
view { |v, _i| v.to_h }
|
|
18
|
+
|
|
19
|
+
BURN = :sync
|
|
20
|
+
|
|
21
|
+
def initialize(prefix:, dry_run: false)
|
|
22
|
+
super()
|
|
23
|
+
@prefix = prefix
|
|
24
|
+
@dry_run = dry_run
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(container:, call:)
|
|
28
|
+
raise UsageError.new("prefix required") if @prefix.nil? || @prefix.empty?
|
|
29
|
+
|
|
30
|
+
leaves = Textus::Action::List.new(prefix: @prefix).call(container: container)
|
|
31
|
+
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
32
|
+
|
|
33
|
+
warnings = leaves.empty? ? ["no keys under #{@prefix}"] : []
|
|
34
|
+
steps = leaves.map { |key| { "op" => "delete", "key" => key } }
|
|
35
|
+
|
|
36
|
+
plan = Textus::Background::Plan.new(steps: steps, warnings: warnings)
|
|
37
|
+
return plan if @dry_run
|
|
38
|
+
|
|
39
|
+
steps.each do |step|
|
|
40
|
+
Textus::Action::KeyDelete.new(key: step["key"]).call(container: container, call: call)
|
|
41
|
+
end
|
|
42
|
+
plan
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|