textus 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/retention/apply.rb +0 -52
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Action
|
|
7
|
+
class RuleLint < Base
|
|
8
|
+
extend Textus::Contract::DSL
|
|
9
|
+
|
|
10
|
+
verb :rule_lint
|
|
11
|
+
summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
|
|
12
|
+
surfaces :cli, :mcp
|
|
13
|
+
cli "rule lint"
|
|
14
|
+
arg :candidate_yaml, String, required: true, wire_name: :against, source: :file,
|
|
15
|
+
description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
|
|
16
|
+
view { |v, _i| v.to_h }
|
|
17
|
+
|
|
18
|
+
def initialize(candidate_yaml:)
|
|
19
|
+
super()
|
|
20
|
+
@candidate_yaml = candidate_yaml
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(container:, **)
|
|
24
|
+
root = container.root
|
|
25
|
+
live_rules = current_rules(root)
|
|
26
|
+
candidate_rules = parse_candidate(@candidate_yaml)
|
|
27
|
+
|
|
28
|
+
live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
|
|
29
|
+
candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
|
|
30
|
+
|
|
31
|
+
steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
|
|
32
|
+
{ "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
|
|
33
|
+
end
|
|
34
|
+
(live_by_match.keys - candidate_by_match.keys).each do |match|
|
|
35
|
+
steps << { "op" => "remove_rule", "match" => match }
|
|
36
|
+
end
|
|
37
|
+
(live_by_match.keys & candidate_by_match.keys).each do |match|
|
|
38
|
+
next if live_by_match[match] == candidate_by_match[match]
|
|
39
|
+
|
|
40
|
+
steps << {
|
|
41
|
+
"op" => "change_rule",
|
|
42
|
+
"match" => match,
|
|
43
|
+
"from" => live_by_match[match],
|
|
44
|
+
"to" => candidate_by_match[match],
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Textus::Jobs::Plan.new(steps: steps, warnings: [])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def current_rules(root)
|
|
54
|
+
raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
|
|
55
|
+
Array(raw["rules"])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_candidate(yaml_text)
|
|
59
|
+
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
60
|
+
raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
Array(raw["rules"])
|
|
63
|
+
rescue Psych::Exception => e
|
|
64
|
+
raise UsageError.new("candidate YAML parse error: #{e.message}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class RuleList < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :rule_list
|
|
9
|
+
summary "List every rule block in the manifest."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "rule list"
|
|
12
|
+
view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
|
|
13
|
+
|
|
14
|
+
def call(container:, **)
|
|
15
|
+
manifest = container.manifest
|
|
16
|
+
manifest.rules.blocks.map do |block|
|
|
17
|
+
row = { "match" => block.match }
|
|
18
|
+
self.class::LIST_FIELDS.each do |field|
|
|
19
|
+
value = block.public_send(field)
|
|
20
|
+
row[field.to_s] = serialize(field, value) unless value.nil?
|
|
21
|
+
end
|
|
22
|
+
row
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def serialize(field, value)
|
|
31
|
+
case field
|
|
32
|
+
when :retention
|
|
33
|
+
{ "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
|
|
34
|
+
when :react
|
|
35
|
+
value.to_h
|
|
36
|
+
else
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class SchemaEnvelope < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :schema_show
|
|
9
|
+
summary "Return the schema (field shape) for an entry's family, by key."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "schema show"
|
|
12
|
+
arg :key, String, required: true, positional: true,
|
|
13
|
+
description: "any key in the family whose schema you want; returns required/optional fields and their types"
|
|
14
|
+
|
|
15
|
+
def initialize(key:)
|
|
16
|
+
super()
|
|
17
|
+
@key = key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(container:, **)
|
|
21
|
+
manifest = container.manifest
|
|
22
|
+
schemas = container.schemas
|
|
23
|
+
mentry = manifest.resolver.resolve(@key).entry
|
|
24
|
+
schema = schemas.fetch_or_nil(mentry.schema)
|
|
25
|
+
{ "protocol" => PROTOCOL, "key" => @key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Uid < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :uid
|
|
9
|
+
summary "Return the stable UID of an entry without reading its body."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "key uid"
|
|
12
|
+
arg :key, String, required: true, positional: true, description: "entry key"
|
|
13
|
+
view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
|
|
14
|
+
|
|
15
|
+
def initialize(key:)
|
|
16
|
+
super()
|
|
17
|
+
@key = key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(container:, call:)
|
|
21
|
+
Textus::Action::Get.new(key: @key).call(container: container, call: call).uid
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.new(*args, **kwargs)
|
|
25
|
+
return super(**kwargs) unless args.any?
|
|
26
|
+
|
|
27
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
28
|
+
mapped = positional.zip(args).to_h
|
|
29
|
+
super(**mapped.merge(kwargs))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Where < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :where
|
|
9
|
+
summary "Resolve a key to its zone, owner, and path without reading the body."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted key to locate (returns zone, owner, path; does not read content)"
|
|
13
|
+
|
|
14
|
+
def initialize(key:)
|
|
15
|
+
super()
|
|
16
|
+
@key = key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
20
|
+
manifest = container.manifest
|
|
21
|
+
res = manifest.resolver.resolve(@key)
|
|
22
|
+
mentry = res.entry
|
|
23
|
+
path = res.path
|
|
24
|
+
{ "protocol" => PROTOCOL, "key" => @key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.new(*args, **kwargs)
|
|
28
|
+
return super(**kwargs) unless args.any?
|
|
29
|
+
|
|
30
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
31
|
+
mapped = positional.zip(args).to_h
|
|
32
|
+
super(**mapped.merge(kwargs))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class WriteVerb < Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def auth(container)
|
|
9
|
+
Textus::Gate::Auth.new(container)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def writer(container, call)
|
|
13
|
+
Textus::Envelope::Writer.from(container: container, call: call)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reader(container)
|
|
17
|
+
Textus::Envelope::Reader.from(container: container)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_with_cascade(target_key, container:, call:)
|
|
21
|
+
result = yield
|
|
22
|
+
cascade_to_rdeps(target_key, container, call) if target_key
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cascade_to_rdeps(key, container, call)
|
|
27
|
+
rdeps = Textus::Action::Rdeps.new(key: key).call(container: container, call: call).fetch("rdeps", [])
|
|
28
|
+
producible = rdeps.select { |dep_key| producible?(dep_key, container) }
|
|
29
|
+
return if producible.empty?
|
|
30
|
+
|
|
31
|
+
producible.each do |dep_key|
|
|
32
|
+
Textus::Jobs::Materialize.new(key: dep_key).call(container:, call:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def producible?(key, container)
|
|
37
|
+
entry = container.manifest.resolver.resolve(key).entry
|
|
38
|
+
!entry.publish_tree.nil?
|
|
39
|
+
rescue Textus::Error
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/textus/boot.rb
CHANGED
|
@@ -1,60 +1,21 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
# Read-only "what's in this store and how do I use it" envelope.
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# registered hooks, write flows, and the CLI verb catalog.
|
|
6
|
-
#
|
|
7
|
-
# Boot is side-effect-free.
|
|
3
|
+
# Boot is side-effect-free. Reads from pre-computed artifacts and
|
|
4
|
+
# the store catalog rather than computing inline.
|
|
8
5
|
module Boot
|
|
9
6
|
PROTOCOL_ID = PROTOCOL
|
|
10
7
|
|
|
11
|
-
#
|
|
12
|
-
# role name and the manifest, and returns guidance for that verb with the
|
|
13
|
-
# live zone named by kind (ADR 0034). A role holding multiple verbs gets one
|
|
14
|
-
# joined string; roles whose verbs have no template are omitted.
|
|
15
|
-
WRITE_FLOW_TEMPLATES = {
|
|
16
|
-
author: lambda do |name, manifest|
|
|
17
|
-
"edit files in #{zone_label(manifest, :canon, "your canon zone")}, " \
|
|
18
|
-
"then 'textus put KEY --as=#{name}'"
|
|
19
|
-
end,
|
|
20
|
-
keep: lambda do |name, manifest|
|
|
21
|
-
"keep durable notes in #{zone_label(manifest, :workspace, "your workspace")}: " \
|
|
22
|
-
"'textus put KEY --as=#{name}' (no accept needed)"
|
|
23
|
-
end,
|
|
24
|
-
propose: lambda do |name, manifest|
|
|
25
|
-
authority = manifest.policy.roles_with_capability("author").first || "the author-holder"
|
|
26
|
-
"propose changes by writing #{manifest.policy.queue_zone}.* entries with --as=#{name} " \
|
|
27
|
-
"and a 'proposal:' frontmatter block; the #{authority} role runs 'textus accept' to apply"
|
|
28
|
-
end,
|
|
29
|
-
converge: lambda do |_name, manifest|
|
|
30
|
-
machine = zone_label(manifest, :machine, "machine")
|
|
31
|
-
"'textus drain' materializes derived #{machine} entries from their sources and " \
|
|
32
|
-
"refreshes stale intake #{machine} entries from their declared source; " \
|
|
33
|
-
"derived files are never hand-edited (reactive on canon writes, or a full pass on demand)"
|
|
34
|
-
end,
|
|
35
|
-
}.freeze
|
|
36
|
-
|
|
37
|
-
def self.write_flows_for(manifest)
|
|
38
|
-
manifest.data.role_caps.each_with_object({}) do |(name, caps), acc|
|
|
39
|
-
flows = caps.filter_map do |verb|
|
|
40
|
-
tmpl = WRITE_FLOW_TEMPLATES[verb.to_sym]
|
|
41
|
-
tmpl&.call(name, manifest)
|
|
42
|
-
end
|
|
43
|
-
acc[name] = flows.join(" / ") unless flows.empty?
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Human-readable name(s) for the live zone(s) of a given kind, or `fallback`
|
|
8
|
+
# Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
|
|
48
9
|
# when the manifest declares none. Lets write-flow guidance name the live
|
|
49
|
-
#
|
|
50
|
-
def self.
|
|
51
|
-
|
|
52
|
-
|
|
10
|
+
# lane by kind instead of a hardcoded instance name (ADR 0034).
|
|
11
|
+
def self.lane_label(manifest, kind, fallback)
|
|
12
|
+
lanes = manifest.policy.lanes_of_kind(kind)
|
|
13
|
+
lanes.empty? ? fallback : lanes.join(", ")
|
|
53
14
|
end
|
|
54
15
|
|
|
55
16
|
# Static, store-independent parts of the agent-facing protocol. The
|
|
56
17
|
# `recipes` and `role_resolution` blocks are derived per-manifest in
|
|
57
|
-
# agent_protocol(...) because
|
|
18
|
+
# agent_protocol(...) because lane and role names are user-configurable.
|
|
58
19
|
AGENT_PROTOCOL_TEMPLATE = {
|
|
59
20
|
"envelope_shape" => {
|
|
60
21
|
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
@@ -79,6 +40,7 @@ module Textus
|
|
|
79
40
|
{ "name" => "boot" },
|
|
80
41
|
{ "name" => "list" },
|
|
81
42
|
{ "name" => "get" },
|
|
43
|
+
{ "name" => "ingest" },
|
|
82
44
|
{ "name" => "where" },
|
|
83
45
|
{ "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
|
|
84
46
|
{ "name" => "put" },
|
|
@@ -91,18 +53,16 @@ module Textus
|
|
|
91
53
|
{ "name" => "blame" },
|
|
92
54
|
{ "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
|
|
93
55
|
{ "name" => "doctor" },
|
|
94
|
-
{ "name" => "hook", "summary" => "list and run registered hooks: 'hook list', 'hook run NAME'" },
|
|
95
56
|
{ "name" => "jobs" },
|
|
96
57
|
{ "name" => "pulse" },
|
|
97
|
-
{ "name" => "capabilities" },
|
|
98
58
|
].freeze
|
|
99
59
|
|
|
100
60
|
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
101
61
|
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
102
62
|
def self.contract_summaries
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
63
|
+
Textus::Action::VERBS.values
|
|
64
|
+
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
65
|
+
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
106
66
|
end
|
|
107
67
|
|
|
108
68
|
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
@@ -119,41 +79,33 @@ module Textus
|
|
|
119
79
|
def self.agent_quickstart(manifest, audit_log)
|
|
120
80
|
agent_role = manifest.policy.proposer_role
|
|
121
81
|
|
|
122
|
-
|
|
123
|
-
|
|
82
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |lane_name, acc|
|
|
83
|
+
next unless agent_role
|
|
84
|
+
|
|
85
|
+
verb = manifest.policy.verb_for_lane(lane_name)
|
|
86
|
+
writers = manifest.policy.roles_with_capability(verb)
|
|
87
|
+
acc << lane_name if writers.include?(agent_role)
|
|
124
88
|
end
|
|
125
89
|
|
|
126
|
-
|
|
90
|
+
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
127
91
|
|
|
128
92
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# internal scan, ADR 0085) nor omit one it can
|
|
134
|
-
# (schema_show/rules); write_verbs drops the old `put KEY --as=… --stdin` CLI
|
|
135
|
-
# framing (role is connection-resolved over MCP; there is no stdin).
|
|
136
|
-
# writable_zones / propose_zone below carry the agent's write authority.
|
|
137
|
-
"read_verbs" => Textus::MCP::Catalog.read_verbs,
|
|
138
|
-
"write_verbs" => agent_role ? Textus::MCP::Catalog.write_verbs : [],
|
|
139
|
-
"writable_zones" => writable_zones,
|
|
140
|
-
"propose_zone" => propose_zone,
|
|
93
|
+
"read_verbs" => Textus::Surfaces::MCP::Catalog.read_verbs,
|
|
94
|
+
"write_verbs" => agent_role ? Textus::Surfaces::MCP::Catalog.write_verbs : [],
|
|
95
|
+
"writable_lanes" => writable_lanes,
|
|
96
|
+
"propose_lane" => propose_lane,
|
|
141
97
|
"latest_seq" => audit_log.latest_seq,
|
|
142
98
|
}
|
|
143
99
|
end
|
|
144
100
|
|
|
145
|
-
# Recipes reference verbs, not a transport's CLI strings (ADR 0056): every
|
|
146
|
-
# step names a verb the agent can call (each transport frames it — CLI as
|
|
147
|
-
# `textus get KEY`, MCP as the `get` tool) or is a plain materialize step. This
|
|
148
|
-
# keeps shell lines out of the surface an MCP agent reads.
|
|
149
101
|
def self.recipes(manifest)
|
|
150
|
-
queue = manifest.policy.
|
|
151
|
-
feeds =
|
|
102
|
+
queue = manifest.policy.queue_lane
|
|
103
|
+
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
152
104
|
{
|
|
153
105
|
"read" => {
|
|
154
106
|
"purpose" => "find and read an entry",
|
|
155
107
|
"steps" => [
|
|
156
|
-
"list (
|
|
108
|
+
"list (lane:, prefix:) — discover keys without reading bodies",
|
|
157
109
|
"get KEY — returns the entry envelope",
|
|
158
110
|
],
|
|
159
111
|
},
|
|
@@ -168,17 +120,17 @@ module Textus
|
|
|
168
120
|
"propose" => {
|
|
169
121
|
"purpose" => "agent suggests a change for human review",
|
|
170
122
|
"agent_steps" => [
|
|
171
|
-
"propose KEY — writes the change into the #{queue}
|
|
123
|
+
"propose KEY — writes the change into the #{queue} lane for review",
|
|
172
124
|
],
|
|
173
125
|
"human_steps" => [
|
|
174
|
-
"accept #{queue}.KEY — promotes the proposal into its target
|
|
126
|
+
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
175
127
|
],
|
|
176
128
|
},
|
|
177
129
|
"drain" => {
|
|
178
130
|
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
179
131
|
"steps" => [
|
|
180
132
|
"pulse — its `stale` list names entries past their ttl",
|
|
181
|
-
"drain (
|
|
133
|
+
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
182
134
|
],
|
|
183
135
|
},
|
|
184
136
|
}
|
|
@@ -196,76 +148,56 @@ module Textus
|
|
|
196
148
|
)
|
|
197
149
|
end
|
|
198
150
|
|
|
199
|
-
def self.build(container
|
|
151
|
+
def self.build(container:)
|
|
200
152
|
manifest = container.manifest
|
|
201
153
|
etag = Textus::Etag.for_contract(container.root)
|
|
202
154
|
|
|
203
|
-
if lean
|
|
204
|
-
return {
|
|
205
|
-
"protocol" => PROTOCOL_ID,
|
|
206
|
-
"store_root" => container.root,
|
|
207
|
-
"zones" => zones_for(manifest),
|
|
208
|
-
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
209
|
-
"contract_etag" => etag,
|
|
210
|
-
}
|
|
211
|
-
end
|
|
212
|
-
|
|
213
155
|
{
|
|
214
156
|
"protocol" => PROTOCOL_ID,
|
|
215
157
|
"store_root" => container.root,
|
|
216
|
-
"zones" => zones_for(manifest),
|
|
217
|
-
"entries" => entries_for(manifest),
|
|
218
|
-
"hooks" => hooks_for_container(container),
|
|
219
|
-
"write_flows" => write_flows_for(manifest),
|
|
220
|
-
"cli_verbs" => CLI_VERBS.map(&:dup),
|
|
221
|
-
"agent_protocol" => agent_protocol(manifest),
|
|
222
|
-
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
223
158
|
"contract_etag" => etag,
|
|
224
|
-
"
|
|
225
|
-
|
|
159
|
+
"lanes" => lanes_for(manifest),
|
|
160
|
+
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
161
|
+
"orientation" => read_artifact_content(container, "artifacts.orientation"),
|
|
162
|
+
"context" => read_boot_context(container),
|
|
163
|
+
"index_key" => "artifacts.index",
|
|
164
|
+
"agent_protocol" => agent_protocol(manifest),
|
|
165
|
+
}.compact
|
|
226
166
|
end
|
|
227
167
|
|
|
228
|
-
def self.
|
|
229
|
-
manifest.
|
|
230
|
-
|
|
231
|
-
kind = manifest.policy.declared_kind(name)
|
|
232
|
-
row["kind"] = kind.to_s if kind
|
|
233
|
-
purpose = manifest.data.zone_descs[name]
|
|
234
|
-
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
235
|
-
row
|
|
236
|
-
end
|
|
237
|
-
end
|
|
168
|
+
def self.read_artifact_content(container, key)
|
|
169
|
+
res = container.manifest.resolver.resolve(key)
|
|
170
|
+
return nil unless res.path && File.exist?(res.path)
|
|
238
171
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"zone" => e.zone,
|
|
245
|
-
"schema" => e.schema,
|
|
246
|
-
"nested" => e.is_a?(Textus::Manifest::Entry::Nested),
|
|
247
|
-
"owner" => e.owner,
|
|
248
|
-
"format" => e.format,
|
|
249
|
-
"derived" => derived,
|
|
250
|
-
"intake" => e.intake?,
|
|
251
|
-
"publish_to" => Array(e.publish_to),
|
|
252
|
-
}
|
|
253
|
-
end
|
|
172
|
+
call = Textus::Call.build(role: Textus::Role::DEFAULT)
|
|
173
|
+
env = Textus::Action::Get.new(key: key).call(container: container, call: call)
|
|
174
|
+
env&.content
|
|
175
|
+
rescue Textus::Error
|
|
176
|
+
nil
|
|
254
177
|
end
|
|
255
178
|
|
|
256
|
-
def self.
|
|
257
|
-
|
|
179
|
+
def self.read_boot_context(container)
|
|
180
|
+
res = container.manifest.resolver.resolve("knowledge.boot")
|
|
181
|
+
return nil unless res.path && File.exist?(res.path)
|
|
182
|
+
|
|
183
|
+
call = Textus::Call.build(role: Textus::Role::DEFAULT)
|
|
184
|
+
env = Textus::Action::Get.new(key: "knowledge.boot").call(container: container, call: call)
|
|
185
|
+
body = env&.body&.strip
|
|
186
|
+
body.nil? || body.empty? ? nil : body
|
|
187
|
+
rescue Textus::Error
|
|
188
|
+
nil
|
|
258
189
|
end
|
|
259
190
|
|
|
260
|
-
def self.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
191
|
+
def self.lanes_for(manifest)
|
|
192
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
193
|
+
verb = manifest.policy.verb_for_lane(name)
|
|
194
|
+
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
195
|
+
kind = manifest.policy.declared_kind(name)
|
|
196
|
+
row["kind"] = kind.to_s if kind
|
|
197
|
+
purpose = manifest.data.lane_descs[name]
|
|
198
|
+
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
199
|
+
row
|
|
267
200
|
end
|
|
268
|
-
sections
|
|
269
201
|
end
|
|
270
202
|
end
|
|
271
203
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Command
|
|
3
|
+
Get = Data.define(:key, :role)
|
|
4
|
+
Put = Data.define(:key, :meta, :body, :content, :if_etag, :role)
|
|
5
|
+
Propose = Data.define(:key, :meta, :body, :content, :role, :pending_key) do
|
|
6
|
+
def initialize(key:, role:, meta: nil, body: nil, content: nil, pending_key: nil)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
KeyDelete = Data.define(:key, :if_etag, :role)
|
|
11
|
+
KeyMv = Data.define(:old_key, :new_key, :dry_run, :role)
|
|
12
|
+
Accept = Data.define(:pending_key, :role)
|
|
13
|
+
Reject = Data.define(:pending_key, :role)
|
|
14
|
+
Enqueue = Data.define(:type, :args, :role)
|
|
15
|
+
Ingest = Data.define(:kind, :slug, :url, :path, :zone, :label, :role) do
|
|
16
|
+
def initialize(kind:, slug:, role:, url: nil, path: nil, zone: nil, label: nil)
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
List = Data.define(:prefix, :lane, :role)
|
|
21
|
+
Where = Data.define(:key, :role)
|
|
22
|
+
Uid = Data.define(:key, :role)
|
|
23
|
+
Blame = Data.define(:key, :limit, :role)
|
|
24
|
+
Audit = Data.define(:key, :lane, :verb, :since, :seq_since, :correlation_id, :limit, :role)
|
|
25
|
+
Deps = Data.define(:key, :role)
|
|
26
|
+
Rdeps = Data.define(:key, :role)
|
|
27
|
+
Pulse = Data.define(:since, :role)
|
|
28
|
+
RuleExplain = Data.define(:key, :detail, :role)
|
|
29
|
+
RuleList = Data.define(:role)
|
|
30
|
+
RuleLint = Data.define(:candidate_yaml, :role)
|
|
31
|
+
Published = Data.define(:role)
|
|
32
|
+
SchemaShow = Data.define(:key, :role)
|
|
33
|
+
Doctor = Data.define(:checks, :role)
|
|
34
|
+
Boot = Data.define(:role)
|
|
35
|
+
Jobs = Data.define(:state, :action, :job_id, :role)
|
|
36
|
+
DataMv = Data.define(:from, :to, :dry_run, :role)
|
|
37
|
+
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run, :role)
|
|
38
|
+
KeyDeletePrefix = Data.define(:prefix, :dry_run, :role)
|
|
39
|
+
Drain = Data.define(:prefix, :lane, :role)
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/textus/container.rb
CHANGED