textus 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Step
|
|
5
|
+
# Checks an artifact/store state and returns diagnostics. Replaces the
|
|
6
|
+
# :validate RPC. Receives only `caps:` (injected by the registry).
|
|
7
|
+
class Validate < Base
|
|
8
|
+
def self.required_kwargs = []
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/textus/step.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
# A Step is a unit of user-extensible behaviour discovered by convention at
|
|
5
|
+
# .textus/steps/<kind>/<name>.rb. Five kinds: fetch (external acquisition),
|
|
6
|
+
# transform (combine/reshape into an artifact), validate (check an artifact),
|
|
7
|
+
# observe (react to a lifecycle event). Replaces the Textus.hook block queue.
|
|
8
|
+
module Step
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -45,7 +45,8 @@ module Textus
|
|
|
45
45
|
def initialize(root)
|
|
46
46
|
@container = build_container(File.expand_path(root))
|
|
47
47
|
bootstrap_hooks
|
|
48
|
-
|
|
48
|
+
steps.publish(:store_loaded, ctx: Step::Context.for(container: @container,
|
|
49
|
+
call: Textus::Call.build(role: Role::DEFAULT)))
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# Build an agent Session oriented at the current cursor/manifest — the
|
|
@@ -54,26 +55,24 @@ module Textus
|
|
|
54
55
|
Textus::Session.new(
|
|
55
56
|
role: role,
|
|
56
57
|
cursor: audit_log.latest_seq,
|
|
57
|
-
|
|
58
|
+
propose_lane: manifest.policy.propose_lane_for(role),
|
|
58
59
|
contract_etag: Textus::Etag.for_contract(root),
|
|
59
60
|
)
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
def
|
|
63
|
-
|
|
63
|
+
def gate
|
|
64
|
+
@container.gate
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
as(role).public_send(verb, *args, **kwargs)
|
|
69
|
-
end
|
|
67
|
+
def as(role, dry_run: false, correlation_id: nil)
|
|
68
|
+
Textus::Surfaces::RoleScope.new(container: container, role: role, dry_run: dry_run, correlation_id: correlation_id)
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
private
|
|
73
72
|
|
|
74
73
|
def build_container(root)
|
|
75
74
|
manifest = Manifest.load(root)
|
|
76
|
-
Container.new(
|
|
75
|
+
container = Container.new(
|
|
77
76
|
root: root,
|
|
78
77
|
manifest: manifest,
|
|
79
78
|
schemas: Schemas.new(File.join(root, "schemas")),
|
|
@@ -83,16 +82,19 @@ module Textus
|
|
|
83
82
|
max_size: manifest.data.audit_config[:max_size],
|
|
84
83
|
keep: manifest.data.audit_config[:keep],
|
|
85
84
|
),
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
steps: Textus::Step::RegistryStore.new,
|
|
86
|
+
gate: nil,
|
|
88
87
|
)
|
|
88
|
+
gate = Textus::Gate.new(container)
|
|
89
|
+
container = container.with(gate: gate)
|
|
90
|
+
gate.instance_variable_set(:@container, container)
|
|
91
|
+
container
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def bootstrap_hooks
|
|
92
|
-
Ports::AuditSubscriber.new(audit_log).attach(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Hooks::Loader.new(events: events, rpc: rpc).load_dir(File.join(root, "hooks"))
|
|
95
|
+
Ports::AuditSubscriber.new(audit_log).attach(steps)
|
|
96
|
+
Textus::Step::Builtin.register_all(steps)
|
|
97
|
+
Textus::Step::Loader.new(registry: steps).load_dir(File.join(root, "steps"))
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Group < Verb
|
|
5
|
+
class << self
|
|
6
|
+
# Subcommands are auto-derived: any Verb descendant whose
|
|
7
|
+
# `parent_group` is this group counts as a subcommand. Sorted
|
|
8
|
+
# alphabetically by command_name for stable help output.
|
|
9
|
+
def subcommands
|
|
10
|
+
Textus::Surfaces::CLI::Runner.install!
|
|
11
|
+
Verb.descendants
|
|
12
|
+
.select { |k| k.parent_group == self && k.command_name }
|
|
13
|
+
.sort_by(&:command_name)
|
|
14
|
+
.to_h { |k| [k.command_name, k] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def needs_store?
|
|
18
|
+
# Delegate to the matched subcommand at parse time; default true.
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse(argv)
|
|
24
|
+
subs = self.class.subcommands
|
|
25
|
+
subname = argv.shift
|
|
26
|
+
if subname.nil?
|
|
27
|
+
raise UsageError.new(
|
|
28
|
+
"#{self.class.command_name} requires a subcommand: #{subs.keys.join(", ")}",
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@sub_klass = subs[subname]
|
|
33
|
+
unless @sub_klass
|
|
34
|
+
raise UsageError.new(
|
|
35
|
+
"unknown #{self.class.command_name} subcommand '#{subname}'. " \
|
|
36
|
+
"Valid: #{subs.keys.join(", ")}",
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
41
|
+
@sub.parse(argv)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(store)
|
|
45
|
+
@sub.call(@sub_klass.needs_store? ? store : nil)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
# Generates CLI::Verb (and CLI::Group) subclasses from per-verb contracts,
|
|
5
|
+
# so the CLI surface is a projection of the contract — the operator-facing
|
|
6
|
+
# mirror of MCP::Catalog (ADR 0063).
|
|
7
|
+
module Runner
|
|
8
|
+
# Subclassable base for contract-projected verbs. Carries the verb's
|
|
9
|
+
# contract (class attr `spec`) and the generic dispatch, exposing one
|
|
10
|
+
# overridable seam, #invoke, that defaults to the generic projection.
|
|
11
|
+
# Escape-hatch verbs subclass this and override #invoke to add behavior
|
|
12
|
+
# (suggestions, --stdin, BuildLock, multi-dispatch) WITHOUT restating the
|
|
13
|
+
# verb name — `spec.verb` remains the single source of dispatch.
|
|
14
|
+
class Base < Verb
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :spec
|
|
17
|
+
|
|
18
|
+
# ADR 0064: derive the CLI command name from the contract's cli_leaf
|
|
19
|
+
# when not set explicitly, so an escape-hatch class never restates its
|
|
20
|
+
# own name. The reconciliation spec proves command_name == cli_leaf for
|
|
21
|
+
# every such class, so this is an equivalence, not a behavior change.
|
|
22
|
+
def command_name(name = nil)
|
|
23
|
+
return super if name
|
|
24
|
+
|
|
25
|
+
super() || spec&.cli_leaf
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def spec = self.class.spec
|
|
30
|
+
|
|
31
|
+
def call(store)
|
|
32
|
+
invoke(store)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Default: pure contract projection. Override in subclasses for behavior.
|
|
36
|
+
def invoke(store)
|
|
37
|
+
Runner.dispatch(self, store, spec)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def flag_values(s = spec)
|
|
41
|
+
s.args.reject(&:positional).each_with_object({}) do |a, h|
|
|
42
|
+
raw = respond_to?(a.name) ? public_send(a.name) : nil
|
|
43
|
+
next if raw.nil?
|
|
44
|
+
|
|
45
|
+
h[a.name] = Runner.coerce(a, raw)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# Build a Command from the spec + parsed inputs, dispatch through Gate.
|
|
53
|
+
def dispatch(verb_instance, store, spec)
|
|
54
|
+
inputs = Textus::Contract::Binder.inputs_from_ordered(
|
|
55
|
+
spec, verb_instance.positional, verb_instance.flag_values(spec)
|
|
56
|
+
)
|
|
57
|
+
inputs = inputs.merge(Textus::Contract::Sources.from_stdin(spec, verb_instance.stdin)) if spec.cli_stdin
|
|
58
|
+
inputs = Textus::Contract::Sources.acquire(spec, inputs)
|
|
59
|
+
inputs = apply_cli_defaults(spec, inputs)
|
|
60
|
+
role = verb_instance.resolved_role(store)
|
|
61
|
+
|
|
62
|
+
invoke = lambda do |effective_inputs|
|
|
63
|
+
cmd = build_command(spec, effective_inputs, role)
|
|
64
|
+
store.gate.dispatch(cmd)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
result = if spec.around
|
|
68
|
+
scope = store.as(role)
|
|
69
|
+
Textus::Contract::Around.with(spec.around, scope: scope, inputs: inputs, session: nil, &invoke)
|
|
70
|
+
else
|
|
71
|
+
invoke.call(inputs)
|
|
72
|
+
end
|
|
73
|
+
verb_instance.emit(shape(spec, result, inputs))
|
|
74
|
+
rescue Textus::Contract::MissingArgs => e
|
|
75
|
+
raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_command(spec, inputs, role)
|
|
79
|
+
cmd_class = Textus::Gate::VERB_COMMAND.fetch(spec.verb) do
|
|
80
|
+
raise Textus::UsageError.new("no Command for verb: #{spec.verb}")
|
|
81
|
+
end
|
|
82
|
+
defaults = {}
|
|
83
|
+
spec.args.each do |a|
|
|
84
|
+
next if a.default == :__unset || inputs.key?(a.name)
|
|
85
|
+
next if a.default.nil? && a.required
|
|
86
|
+
|
|
87
|
+
defaults[a.name] = a.default
|
|
88
|
+
end
|
|
89
|
+
kwargs = defaults.merge(inputs)
|
|
90
|
+
kwargs[:role] = role if cmd_class.members.include?(:role) && !inputs.key?(:role) && spec.verb != :audit
|
|
91
|
+
check_missing_args!(spec, cmd_class, kwargs)
|
|
92
|
+
|
|
93
|
+
cmd_class.new(**kwargs.slice(*cmd_class.members))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def check_missing_args!(spec, cmd_class, kwargs)
|
|
97
|
+
params = cmd_class.instance_method(:initialize).parameters
|
|
98
|
+
required = if params == [[:rest]]
|
|
99
|
+
cmd_class.members
|
|
100
|
+
else
|
|
101
|
+
params.select { |t,| t == :keyreq }.map(&:last)
|
|
102
|
+
end
|
|
103
|
+
missing = required - kwargs.keys
|
|
104
|
+
return if missing.empty?
|
|
105
|
+
|
|
106
|
+
raise Textus::Contract::MissingArgs.new(spec, missing.map { |m| Struct.new(:wire, :name).new(m.to_s, m) })
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Fill CLI-specific defaults (cli_default:) for args the operator did not
|
|
110
|
+
# pass, where the CLI default diverges from the contract default the agent
|
|
111
|
+
# surfaces use — e.g. migrate/data_mv apply by default on the CLI but plan
|
|
112
|
+
# by default for agents (ADR 0068). The divergence is legible in the
|
|
113
|
+
# contract, not hidden in a hand class.
|
|
114
|
+
def apply_cli_defaults(spec, inputs)
|
|
115
|
+
spec.args.each_with_object(inputs.dup) do |a, h|
|
|
116
|
+
next if a.cli_default == :__unset || h.key?(a.name)
|
|
117
|
+
|
|
118
|
+
h[a.name] = a.cli_default
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Shape the use-case result for the CLI wire via the verb's :cli view
|
|
123
|
+
# (falling back to the default view). The view is called uniformly as
|
|
124
|
+
# (result, inputs); an inputs-aware view echoes an input such as the key
|
|
125
|
+
# (ADR 0067).
|
|
126
|
+
def shape(spec, result, inputs)
|
|
127
|
+
Textus::Contract::View.render(spec, :cli, result, inputs)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The default the CLI flag is generated against — `cli_default:` when the
|
|
131
|
+
# operator-facing default diverges from the contract default the agent
|
|
132
|
+
# surfaces use, else the contract `default`. This drives boolean flag
|
|
133
|
+
# polarity so a verb that applies-by-default on the CLI but plans-by-default
|
|
134
|
+
# for agents (migrate, data_mv) gets a `--dry-run` flag, not `--no-dry-run`.
|
|
135
|
+
def effective_default(arg)
|
|
136
|
+
arg.cli_default == :__unset ? arg.default : arg.cli_default
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def flagspec_for(arg)
|
|
140
|
+
wire = arg.wire.to_s.tr("_", "-")
|
|
141
|
+
if arg.type == :boolean
|
|
142
|
+
effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
|
|
143
|
+
else
|
|
144
|
+
"--#{wire}=VALUE"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# NB: compare arg.type by equality, not `case`/`===` — `Integer === arg.type`
|
|
149
|
+
# is false when arg.type is the Integer *class* (it tests instance-of), so a
|
|
150
|
+
# `when Integer` branch would silently never coerce.
|
|
151
|
+
def coerce(arg, raw)
|
|
152
|
+
return effective_default(arg) != true if arg.type == :boolean
|
|
153
|
+
return Integer(raw) if arg.type == Integer
|
|
154
|
+
|
|
155
|
+
raw
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ensure_group(name)
|
|
159
|
+
const = name.split("_").map(&:capitalize).join
|
|
160
|
+
return Group.const_get(const, false) if Group.const_defined?(const, false)
|
|
161
|
+
|
|
162
|
+
g = Class.new(Group) { command_name name }
|
|
163
|
+
Group.const_set(const, g)
|
|
164
|
+
g
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Contract verbs whose CLI behavior is a genuine `< Runner::Base` override
|
|
168
|
+
# — behavior the generic projection cannot express (ADR 0068/0069):
|
|
169
|
+
# get — raises UnknownKey with resolver suggestions (a CLI-only
|
|
170
|
+
# affordance; the agent surface deliberately returns nil)
|
|
171
|
+
# put — reads the entry JSON from --stdin (ADR 0089: just stores bytes,
|
|
172
|
+
# no --fetch transform)
|
|
173
|
+
# (build removed in ADR 0087: materialization is system-pushed via drain/serve)
|
|
174
|
+
BEHAVIORAL_HATCHES = %i[get put].freeze
|
|
175
|
+
|
|
176
|
+
# Contract verbs whose CLI is a plain `< Verb` command, not a projection at
|
|
177
|
+
# all — composite reports assembled outside the contract.
|
|
178
|
+
# (boot removed: its contract carries surfaces :cli + the :lean arg, so the
|
|
179
|
+
# generic projection now generates it; the hand-authored CLI::Verb::Boot is
|
|
180
|
+
# deleted in ADR 0101.)
|
|
181
|
+
# (doctor retained: hand-authored to preserve --check=NAME flag spelling and
|
|
182
|
+
# the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
|
|
183
|
+
# projection cannot yet express; kept in ADR 0101 pending a future pass.)
|
|
184
|
+
# (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
|
|
185
|
+
# driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
|
|
186
|
+
# read-through that once also drove it.)
|
|
187
|
+
NON_PROJECTED_CLI = %i[doctor].freeze
|
|
188
|
+
|
|
189
|
+
# The installer skips generation for either category.
|
|
190
|
+
HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
|
|
191
|
+
|
|
192
|
+
def hand_authored?(verb) = HAND_AUTHORED_VERBS.include?(verb)
|
|
193
|
+
|
|
194
|
+
def install!
|
|
195
|
+
@installed ||= {}
|
|
196
|
+
Textus::Gate::ROUTES.each_key do |cmd_class|
|
|
197
|
+
verb = Textus::Gate::VERB_COMMAND.key(cmd_class)
|
|
198
|
+
next unless verb
|
|
199
|
+
|
|
200
|
+
action_class = Textus::Gate::ROUTES[cmd_class].first
|
|
201
|
+
next unless action_class.respond_to?(:contract?) && action_class.contract?
|
|
202
|
+
|
|
203
|
+
spec = action_class.contract
|
|
204
|
+
next unless spec.cli?
|
|
205
|
+
next if hand_authored?(spec.verb)
|
|
206
|
+
next if @installed[spec.verb]
|
|
207
|
+
|
|
208
|
+
install_for(spec)
|
|
209
|
+
@installed[spec.verb] = true
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def install_for(spec)
|
|
214
|
+
group = spec.cli_group ? ensure_group(spec.cli_group) : nil
|
|
215
|
+
leaf = spec.cli_leaf
|
|
216
|
+
non_positional = spec.args.reject(&:positional)
|
|
217
|
+
|
|
218
|
+
klass = Class.new(Base)
|
|
219
|
+
klass.spec = spec
|
|
220
|
+
klass.command_name leaf
|
|
221
|
+
klass.parent_group group if group
|
|
222
|
+
klass.option :as_flag, "--as=ROLE"
|
|
223
|
+
klass.option :use_stdin, "--stdin" if spec.cli_stdin
|
|
224
|
+
non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
|
|
225
|
+
|
|
226
|
+
# Anchor the anonymous class to a constant so descendants discovery is
|
|
227
|
+
# stable. Name it after the verb under a Generated namespace.
|
|
228
|
+
const_name = spec.verb.to_s.split("_").map(&:capitalize).join
|
|
229
|
+
gen = "Gen#{const_name}"
|
|
230
|
+
Verb.const_set(gen, klass) unless Verb.const_defined?(gen, false)
|
|
231
|
+
klass
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class Doctor < Verb
|
|
6
|
+
command_name "doctor"
|
|
7
|
+
option :checks, "--check=NAME"
|
|
8
|
+
|
|
9
|
+
def call(store)
|
|
10
|
+
cmd = Textus::Command::Doctor.new(
|
|
11
|
+
checks: checks&.split(",")&.map(&:strip),
|
|
12
|
+
role: resolved_role(store),
|
|
13
|
+
)
|
|
14
|
+
res = store.gate.dispatch(cmd)
|
|
15
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class Get < Runner::Base
|
|
6
|
+
self.spec = Textus::Action::Get.contract
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
|
|
9
|
+
def invoke(store)
|
|
10
|
+
key = positional.shift or raise UsageError.new("get requires a key")
|
|
11
|
+
cmd = Textus::Command::Get.new(key: key, role: resolved_role(store))
|
|
12
|
+
result = store.gate.dispatch(cmd)
|
|
13
|
+
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
14
|
+
|
|
15
|
+
emit(result.to_h_for_wire)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class Init < Verb
|
|
6
|
+
command_name "init"
|
|
7
|
+
|
|
8
|
+
option :with_agent, "--with-agent"
|
|
9
|
+
|
|
10
|
+
def self.needs_store? = false
|
|
11
|
+
|
|
12
|
+
def call(_store)
|
|
13
|
+
target = File.join(@cwd, ".textus")
|
|
14
|
+
emit(Textus::Init.run(target, with_agent: !!with_agent))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
# Launches the MCP stdio server in the current process. Blocks on stdin;
|
|
6
|
+
# never returns until stdin closes. The connection acts as the `agent`
|
|
7
|
+
# role by default (ADR 0040): the agent channel proposes, it does not
|
|
8
|
+
# inherit the human's authority. Override per connection with --as, or
|
|
9
|
+
# TEXTUS_ROLE / .textus/role (same chain as every other verb).
|
|
10
|
+
class MCPServe < Verb
|
|
11
|
+
command_name "serve"
|
|
12
|
+
parent_group Group::MCP
|
|
13
|
+
option :as_flag, "--as=ROLE"
|
|
14
|
+
|
|
15
|
+
def call(store)
|
|
16
|
+
role = resolved_role(store, default: Textus::Role::AGENT)
|
|
17
|
+
Textus::Surfaces::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout, role: role).run
|
|
18
|
+
0
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class Put < Runner::Base
|
|
6
|
+
self.spec = Textus::Action::Put.contract
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
option :use_stdin, "--stdin"
|
|
9
|
+
|
|
10
|
+
def invoke(store)
|
|
11
|
+
key = positional.shift or raise UsageError.new("put requires a key")
|
|
12
|
+
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
13
|
+
|
|
14
|
+
payload = JSON.parse(@stdin.read)
|
|
15
|
+
cmd = Textus::Command::Put.new(
|
|
16
|
+
key: key,
|
|
17
|
+
meta: payload["_meta"] || {},
|
|
18
|
+
body: payload["body"] || "",
|
|
19
|
+
content: nil,
|
|
20
|
+
if_etag: payload["if_etag"],
|
|
21
|
+
role: resolved_role(store),
|
|
22
|
+
)
|
|
23
|
+
result = store.gate.dispatch(cmd)
|
|
24
|
+
emit(result.to_h_for_wire)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class SchemaDiff < Verb
|
|
6
|
+
command_name "diff"
|
|
7
|
+
parent_group Group::Schema
|
|
8
|
+
|
|
9
|
+
def call(store)
|
|
10
|
+
name = positional.shift or raise UsageError.new("schema diff NAME")
|
|
11
|
+
emit(Textus::Schema::Tools.diff(store, name: name))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class SchemaInit < Verb
|
|
6
|
+
command_name "init"
|
|
7
|
+
parent_group Group::Schema
|
|
8
|
+
|
|
9
|
+
option :from_key, "--from=KEY"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
name = positional.shift or raise UsageError.new("schema init NAME")
|
|
13
|
+
raise UsageError.new("schema init requires --from=KEY") unless from_key
|
|
14
|
+
|
|
15
|
+
emit(Textus::Schema::Tools.init(store, name: name, from: from_key))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class SchemaMigrate < Verb
|
|
6
|
+
command_name "migrate"
|
|
7
|
+
parent_group Group::Schema
|
|
8
|
+
|
|
9
|
+
option :rename, "--rename=O:N"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
name = positional.shift or raise UsageError.new("schema migrate NAME")
|
|
13
|
+
raise UsageError.new("schema migrate requires --rename=OLD:NEW") unless rename
|
|
14
|
+
|
|
15
|
+
emit(Textus::Schema::Tools.migrate(store, name: name, rename: rename))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class Watch < Verb
|
|
6
|
+
command_name "watch"
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
option :poll, "--poll=SECONDS"
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
watcher = Textus::Surfaces::Watcher.new(container: store.container)
|
|
12
|
+
watcher.run(poll: poll&.to_f)
|
|
13
|
+
0
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|