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,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Store-wide schema + role-authority validation: walks every entry and runs
|
|
4
|
-
# the Validator over it. Consumed internally by `doctor`'s schema_violations
|
|
5
|
-
# check and exposed as a Ruby store method (`store.validate_all`).
|
|
6
|
-
#
|
|
7
|
-
# Ruby-only, like `Read::Freshness`: it declares a contract (so it round-trips
|
|
8
|
-
# through the routing<->contract bijection, ADR 0105) but omits `surfaces`, so
|
|
9
|
-
# it gets no CLI or MCP projection. The public `validate-all` CLI verb was
|
|
10
|
-
# removed in v0.5 (`doctor --check=schema_violations` replaces it).
|
|
11
|
-
class ValidateAll
|
|
12
|
-
extend Textus::Contract::DSL
|
|
13
|
-
|
|
14
|
-
verb :validate_all
|
|
15
|
-
summary "Internal store-wide schema + role-authority validation; backs " \
|
|
16
|
-
"doctor's schema_violations check. No public surface (ADR 0105)."
|
|
17
|
-
|
|
18
|
-
def initialize(container:, call:)
|
|
19
|
-
@container = container
|
|
20
|
-
@call = call
|
|
21
|
-
@manifest = container.manifest
|
|
22
|
-
@schemas = container.schemas
|
|
23
|
-
@audit_log = container.audit_log
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def call
|
|
27
|
-
Validator.new(
|
|
28
|
-
reader: Get.new(container: @container, call: @call),
|
|
29
|
-
manifest: @manifest,
|
|
30
|
-
audit_log: @audit_log,
|
|
31
|
-
schema_for: ->(name) { @schemas.fetch_or_nil(name) },
|
|
32
|
-
).call
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
data/lib/textus/read/where.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Where
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :where
|
|
7
|
-
summary "Resolve a key to its zone, owner, and path without reading the body."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted key to locate (returns zone, owner, path; does not read content)"
|
|
11
|
-
|
|
12
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
-
@manifest = container.manifest
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(key)
|
|
17
|
-
res = @manifest.resolver.resolve(key)
|
|
18
|
-
mentry = res.entry
|
|
19
|
-
path = res.path
|
|
20
|
-
{ "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/textus/role_scope.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Thin role-scoped facade over a Container. Closes over a role default
|
|
3
|
-
# and a dry_run flag, then forwards every verb in Dispatcher::VERBS to
|
|
4
|
-
# the corresponding use case.
|
|
5
|
-
#
|
|
6
|
-
# Replaces the per-call Session under the 0.27.0 architecture: a Store
|
|
7
|
-
# exposes #as(role) to get a RoleScope, and Store#put / Store#get / etc
|
|
8
|
-
# delegate to RoleScope under the default role.
|
|
9
|
-
class RoleScope
|
|
10
|
-
attr_reader :container, :role, :correlation_id
|
|
11
|
-
|
|
12
|
-
def dry_run?
|
|
13
|
-
@dry_run
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def initialize(container:, role:, dry_run: false, correlation_id: nil)
|
|
17
|
-
@container = container
|
|
18
|
-
@role = role.to_s
|
|
19
|
-
@dry_run = dry_run
|
|
20
|
-
@correlation_id = correlation_id
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def with_role(role)
|
|
24
|
-
self.class.new(container: @container, role: role, dry_run: @dry_run, correlation_id: @correlation_id)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def with_correlation_id(cid)
|
|
28
|
-
self.class.new(container: @container, role: @role, dry_run: @dry_run, correlation_id: cid)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def hook_context
|
|
32
|
-
@hook_context ||= Textus::Hooks::Context.new(scope: self)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def with_dry_run
|
|
36
|
-
self.class.new(container: @container, role: @role, dry_run: true, correlation_id: @correlation_id)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Single bind + invoke site for every surface. `inputs` is the uniform
|
|
40
|
-
# by-name hash (the binder's currency). MCP/CLI build it from their raw
|
|
41
|
-
# transport shape and call this directly; the per-verb Ruby methods below
|
|
42
|
-
# normalize positional+keyword Ruby args into `inputs` and delegate here.
|
|
43
|
-
def dispatch_bound(verb, inputs, session: nil)
|
|
44
|
-
klass = Textus::Dispatcher::VERBS[verb]
|
|
45
|
-
spec = (klass.contract if klass.respond_to?(:contract?) && klass.contract?)
|
|
46
|
-
|
|
47
|
-
invoke = lambda do |effective_inputs|
|
|
48
|
-
args, kwargs =
|
|
49
|
-
if spec
|
|
50
|
-
Textus::Contract::Binder.bind(spec, effective_inputs, session: session)
|
|
51
|
-
else
|
|
52
|
-
[[], effective_inputs]
|
|
53
|
-
end
|
|
54
|
-
call_value = Textus::Call.build(role: @role, correlation_id: @correlation_id, dry_run: @dry_run)
|
|
55
|
-
Textus::Dispatcher.invoke(verb, container: @container, call: call_value, args: args, kwargs: kwargs)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
if spec&.around
|
|
59
|
-
Textus::Contract::Around.with(spec.around, scope: self, inputs: inputs, session: session, &invoke)
|
|
60
|
-
else
|
|
61
|
-
invoke.call(inputs)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
Textus::Dispatcher::VERBS.each_key do |verb|
|
|
66
|
-
define_method(verb) do |*args, **kwargs|
|
|
67
|
-
klass = Textus::Dispatcher::VERBS[verb]
|
|
68
|
-
inputs =
|
|
69
|
-
if klass.respond_to?(:contract?) && klass.contract?
|
|
70
|
-
Textus::Contract::Binder.inputs_from_ordered(klass.contract, args, kwargs)
|
|
71
|
-
else
|
|
72
|
-
kwargs
|
|
73
|
-
end
|
|
74
|
-
dispatch_bound(verb, inputs)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
data/lib/textus/write/accept.rb
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class Accept
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :accept
|
|
7
|
-
summary "apply a queued proposal to its target zone; requires the author capability"
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "accept"
|
|
10
|
-
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
11
|
-
|
|
12
|
-
def initialize(container:, call:)
|
|
13
|
-
@container = container
|
|
14
|
-
@call = call
|
|
15
|
-
@manifest = container.manifest
|
|
16
|
-
@schemas = container.schemas
|
|
17
|
-
@events = container.events
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def call(pending_key)
|
|
21
|
-
env = Textus::Read::Get.new(container: @container, call: @call).call(pending_key)
|
|
22
|
-
proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
23
|
-
target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
24
|
-
action = proposal["action"] || "put"
|
|
25
|
-
|
|
26
|
-
guard.for(:accept, target).check!(
|
|
27
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
28
|
-
actor: @call.role, transition: :accept, origin: pending_key,
|
|
29
|
-
target: target, envelope: env, manifest: @manifest
|
|
30
|
-
),
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
case action
|
|
34
|
-
when "put"
|
|
35
|
-
put_op.call(target, meta: env.meta["frontmatter"] || {}, body: env.body)
|
|
36
|
-
when "delete"
|
|
37
|
-
delete_op.call(target)
|
|
38
|
-
else
|
|
39
|
-
raise ProposalError.new("unknown action: #{action}")
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
delete_op.call(pending_key)
|
|
43
|
-
@events.publish(:proposal_accepted, ctx: hook_context, key: pending_key, target_key: target)
|
|
44
|
-
{ "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def guard
|
|
50
|
-
@guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def hook_context = @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
54
|
-
def put_op = @put_op ||= Textus::Write::Put.new(container: @container, call: @call)
|
|
55
|
-
def delete_op = @delete_op ||= Textus::Write::KeyDelete.new(container: @container, call: @call)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class KeyDelete
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :key_delete
|
|
7
|
-
summary "Delete one entry by key. Single-key, lower blast radius than " \
|
|
8
|
-
"key_delete_prefix; guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
|
|
9
|
-
surfaces :cli, :mcp
|
|
10
|
-
cli "key delete"
|
|
11
|
-
arg :key, String, required: true, positional: true,
|
|
12
|
-
description: "dotted entry key to delete"
|
|
13
|
-
arg :if_etag, String,
|
|
14
|
-
description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
|
|
15
|
-
# `call` already returns a wire hash {protocol, ok, key, deleted}; identity response.
|
|
16
|
-
|
|
17
|
-
def initialize(container:, call:)
|
|
18
|
-
@container = container
|
|
19
|
-
@call = call
|
|
20
|
-
@manifest = container.manifest
|
|
21
|
-
@events = container.events
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def call(key, if_etag: nil, suppress_events: false)
|
|
25
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
26
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
27
|
-
|
|
28
|
-
guard_for(:key_delete, key, if_etag: if_etag).check!(eval_for(:key_delete, target_key: key))
|
|
29
|
-
|
|
30
|
-
writer.delete(key, mentry: mentry, if_etag: if_etag)
|
|
31
|
-
|
|
32
|
-
unless suppress_events
|
|
33
|
-
@events.publish(:entry_deleted,
|
|
34
|
-
ctx: hook_context,
|
|
35
|
-
key: key)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def guard_for(transition, key, if_etag: nil)
|
|
44
|
-
Textus::Domain::Policy::GuardFactory.new(
|
|
45
|
-
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
46
|
-
).for(transition, key)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def eval_for(transition, target_key:, envelope: nil)
|
|
50
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
51
|
-
actor: @call.role, transition: transition, origin: nil,
|
|
52
|
-
target: target_key, envelope: envelope, manifest: @manifest
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def hook_context
|
|
57
|
-
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def writer
|
|
61
|
-
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
data/lib/textus/write/key_mv.rb
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class KeyMv
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :key_mv
|
|
7
|
-
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "key mv"
|
|
10
|
-
arg :old_key, String, required: true, positional: true,
|
|
11
|
-
description: "current dotted key"
|
|
12
|
-
arg :new_key, String, required: true, positional: true,
|
|
13
|
-
description: "new dotted key (must be the same zone and format as old_key)"
|
|
14
|
-
arg :dry_run, :boolean,
|
|
15
|
-
description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
|
|
16
|
-
"defaults to false, so omitting it applies the move immediately " \
|
|
17
|
-
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
18
|
-
# `call` already returns a wire hash; identity response.
|
|
19
|
-
|
|
20
|
-
def initialize(container:, call:)
|
|
21
|
-
@container = container
|
|
22
|
-
@call = call
|
|
23
|
-
@manifest = container.manifest
|
|
24
|
-
@events = container.events
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def call(old_key, new_key, dry_run: false)
|
|
28
|
-
old_res, new_res = prepare(old_key, new_key)
|
|
29
|
-
return dry_run_result(old_key, new_key, old_res, new_res) if dry_run
|
|
30
|
-
|
|
31
|
-
ensure_uid!(old_key, old_res.entry)
|
|
32
|
-
envelope = writer.move(
|
|
33
|
-
from_key: old_key, to_key: new_key,
|
|
34
|
-
new_mentry: new_res.entry
|
|
35
|
-
)
|
|
36
|
-
publish_renamed(old_key, new_key, envelope)
|
|
37
|
-
success_result(old_key, new_key, old_res, new_res, envelope)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def hook_context
|
|
43
|
-
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def prepare(old_key, new_key)
|
|
47
|
-
Textus::Manifest::Data.validate_key!(old_key)
|
|
48
|
-
Textus::Manifest::Data.validate_key!(new_key)
|
|
49
|
-
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
50
|
-
|
|
51
|
-
old_res = @manifest.resolver.resolve(old_key)
|
|
52
|
-
new_res = @manifest.resolver.resolve(new_key)
|
|
53
|
-
raise UnknownKey.new(old_key) unless reader.exists?(old_key)
|
|
54
|
-
|
|
55
|
-
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
56
|
-
guard_for(:key_mv, old_key).check!(eval_for(:key_mv, target_key: old_key))
|
|
57
|
-
guard_for(:key_mv, new_key).check!(eval_for(:key_mv, target_key: new_key))
|
|
58
|
-
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
|
|
59
|
-
|
|
60
|
-
[old_res, new_res]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def validate_zone_and_format!(old_mentry, new_mentry)
|
|
64
|
-
if old_mentry.zone != new_mentry.zone
|
|
65
|
-
raise UsageError.new(
|
|
66
|
-
"mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
|
|
67
|
-
"Use put+delete for cross-zone moves.",
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
return if old_mentry.format == new_mentry.format
|
|
71
|
-
|
|
72
|
-
raise UsageError.new("mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# If the source file lacks a UID, rewrite it in-place via the writer
|
|
76
|
-
# so a UID gets injected before the move. This produces one "put"
|
|
77
|
-
# audit row, then the "mv" row from Writer#move.
|
|
78
|
-
def ensure_uid!(old_key, old_mentry)
|
|
79
|
-
pre_env = reader.read(old_key)
|
|
80
|
-
return if pre_env.uid
|
|
81
|
-
|
|
82
|
-
writer.put(
|
|
83
|
-
old_key, mentry: old_mentry,
|
|
84
|
-
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
85
|
-
meta: pre_env.meta, body: pre_env.body, content: pre_env.content,
|
|
86
|
-
)
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def publish_renamed(old_key, new_key, envelope)
|
|
91
|
-
@events.publish(:entry_renamed,
|
|
92
|
-
ctx: hook_context,
|
|
93
|
-
key: new_key,
|
|
94
|
-
from_key: old_key,
|
|
95
|
-
to_key: new_key,
|
|
96
|
-
envelope: envelope)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def dry_run_result(old_key, new_key, old_res, new_res)
|
|
100
|
-
pre_env = reader.read(old_key)
|
|
101
|
-
{
|
|
102
|
-
"protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
|
|
103
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
104
|
-
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
105
|
-
"uid" => pre_env.uid
|
|
106
|
-
}
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def success_result(old_key, new_key, old_res, new_res, envelope)
|
|
110
|
-
{
|
|
111
|
-
"protocol" => PROTOCOL, "ok" => true,
|
|
112
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
113
|
-
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
114
|
-
"uid" => envelope.uid,
|
|
115
|
-
"envelope" => envelope.to_h_for_wire
|
|
116
|
-
}
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def guard_for(transition, key, if_etag: nil)
|
|
120
|
-
Textus::Domain::Policy::GuardFactory.new(
|
|
121
|
-
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
122
|
-
).for(transition, key)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def eval_for(transition, target_key:, envelope: nil)
|
|
126
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
127
|
-
actor: @call.role, transition: transition, origin: nil,
|
|
128
|
-
target: target_key, envelope: envelope, manifest: @manifest
|
|
129
|
-
)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def writer
|
|
133
|
-
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def reader
|
|
137
|
-
@reader ||= Textus::Envelope::IO::Reader.from(container: @container)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
data/lib/textus/write/propose.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
# Queue a proposal: resolve the acting role's propose_zone, prefix the key,
|
|
4
|
-
# and write there via the Put verb. Was inlined in the MCP `propose` tool
|
|
5
|
-
# and the CLI propose verb; promoted to a first-class verb so all three
|
|
6
|
-
# transports share one implementation (ADR 0036, ADR 0039).
|
|
7
|
-
class Propose
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :propose
|
|
11
|
-
summary "Write a proposal to the role's propose_zone. Auto-prefixes the key."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
cli_stdin :json
|
|
14
|
-
arg :key, String, required: true, positional: true,
|
|
15
|
-
description: "key relative to propose_zone, e.g. 'decisions.feature-x'"
|
|
16
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
17
|
-
description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
|
|
18
|
-
arg :body, String,
|
|
19
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
20
|
-
arg :content, Hash,
|
|
21
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
22
|
-
# ADR 0069: every surface receives the raw Envelope and self-shapes — no
|
|
23
|
-
# surface pre-wires the result. Emitting the full wire envelope on every
|
|
24
|
-
# surface is a superset of the old `{uid, etag, key}` (the accepted
|
|
25
|
-
# breaking change; MCP/Ruby now get the full envelope too).
|
|
26
|
-
view { |env, _i| env.to_h_for_wire }
|
|
27
|
-
|
|
28
|
-
def initialize(container:, call:)
|
|
29
|
-
@container = container
|
|
30
|
-
@call = call
|
|
31
|
-
@manifest = container.manifest
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# if_etag is intentionally absent: a proposal is always a fresh queue write.
|
|
35
|
-
def call(key, meta: nil, body: nil, content: nil)
|
|
36
|
-
zone = @manifest.policy.propose_zone_for(@call.role)
|
|
37
|
-
unless zone
|
|
38
|
-
raise Textus::Error.new(
|
|
39
|
-
"propose_forbidden",
|
|
40
|
-
"role '#{@call.role}' has no writable propose_zone",
|
|
41
|
-
details: { "role" => @call.role },
|
|
42
|
-
hint: "the manifest must define a queue zone and '#{@call.role}' must hold the 'propose' capability",
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
Textus::Dispatcher.invoke(
|
|
47
|
-
:put, container: @container, call: @call,
|
|
48
|
-
args: ["#{zone}.#{key}"],
|
|
49
|
-
kwargs: { meta: meta || {}, body: body, content: content }
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
data/lib/textus/write/put.rb
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class Put
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :put
|
|
7
|
-
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :key, String, required: true, positional: true,
|
|
10
|
-
description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
|
|
11
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
12
|
-
description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
|
|
13
|
-
arg :body, String,
|
|
14
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
15
|
-
arg :content, Hash,
|
|
16
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
17
|
-
arg :if_etag, String,
|
|
18
|
-
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
19
|
-
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
20
|
-
|
|
21
|
-
def initialize(container:, call:)
|
|
22
|
-
@container = container
|
|
23
|
-
@call = call
|
|
24
|
-
@manifest = container.manifest
|
|
25
|
-
@events = container.events
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
29
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
30
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
31
|
-
guard_for(:put, key, if_etag: if_etag).check!(eval_for(:put, target_key: key))
|
|
32
|
-
|
|
33
|
-
envelope = writer.put(
|
|
34
|
-
key,
|
|
35
|
-
mentry: mentry,
|
|
36
|
-
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
37
|
-
meta: meta, body: body, content: content,
|
|
38
|
-
),
|
|
39
|
-
if_etag: if_etag,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
@events.publish(:entry_written,
|
|
43
|
-
ctx: hook_context,
|
|
44
|
-
key: key,
|
|
45
|
-
envelope: envelope)
|
|
46
|
-
|
|
47
|
-
envelope
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def guard_for(transition, key, if_etag: nil)
|
|
53
|
-
Textus::Domain::Policy::GuardFactory.new(
|
|
54
|
-
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
55
|
-
).for(transition, key)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def eval_for(transition, target_key:, envelope: nil)
|
|
59
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
60
|
-
actor: @call.role, transition: transition, origin: nil,
|
|
61
|
-
target: target_key, envelope: envelope, manifest: @manifest
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def hook_context
|
|
66
|
-
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def writer
|
|
70
|
-
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
data/lib/textus/write/reject.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class Reject
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :reject
|
|
7
|
-
summary "discard a queued proposal without applying it"
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "reject"
|
|
10
|
-
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
11
|
-
|
|
12
|
-
def initialize(container:, call:)
|
|
13
|
-
@container = container
|
|
14
|
-
@call = call
|
|
15
|
-
@manifest = container.manifest
|
|
16
|
-
@schemas = container.schemas
|
|
17
|
-
@events = container.events
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def call(pending_key)
|
|
21
|
-
guard.for(:reject, pending_key).check!(
|
|
22
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
23
|
-
actor: @call.role, transition: :reject, origin: pending_key,
|
|
24
|
-
target: pending_key, envelope: nil, manifest: @manifest
|
|
25
|
-
),
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
29
|
-
unless mentry.in_proposal_zone?(@manifest.policy)
|
|
30
|
-
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
env = Textus::Read::Get.new(
|
|
34
|
-
container: @container, call: @call,
|
|
35
|
-
).call(pending_key)
|
|
36
|
-
proposal = env.meta&.dig("proposal") or
|
|
37
|
-
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
38
|
-
target_key = proposal["target_key"] or
|
|
39
|
-
raise ProposalError.new("proposal missing target_key")
|
|
40
|
-
|
|
41
|
-
delete_op.call(pending_key, suppress_events: true)
|
|
42
|
-
|
|
43
|
-
@events.publish(:proposal_rejected,
|
|
44
|
-
ctx: hook_context,
|
|
45
|
-
key: pending_key,
|
|
46
|
-
target_key: target_key)
|
|
47
|
-
|
|
48
|
-
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def guard
|
|
54
|
-
@guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def hook_context
|
|
58
|
-
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def delete_op
|
|
62
|
-
@delete_op ||= Textus::Write::KeyDelete.new(
|
|
63
|
-
container: @container, call: @call,
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|