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
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
|