textus 0.54.2 → 0.55.2
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 +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
|
@@ -1,42 +0,0 @@
|
|
|
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
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
data/lib/textus/action/uid.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
|
data/lib/textus/action/where.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
|
@@ -1,44 +0,0 @@
|
|
|
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/call.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
require "securerandom"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
# Immutable per-invocation value. Carries who is acting (role), the
|
|
5
|
-
# request correlation id, the wall clock, and the dry_run flag — the
|
|
6
|
-
# bits Use Cases need that are not part of the Container.
|
|
7
|
-
Call = Data.define(:role, :correlation_id, :now, :dry_run) do
|
|
8
|
-
def self.build(role:, correlation_id: nil, now: nil, dry_run: false)
|
|
9
|
-
new(
|
|
10
|
-
role: role.to_s,
|
|
11
|
-
correlation_id: correlation_id || SecureRandom.uuid,
|
|
12
|
-
now: now || Textus::Ports::Clock.new.now,
|
|
13
|
-
dry_run: dry_run,
|
|
14
|
-
)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def dry_run? = dry_run
|
|
18
|
-
|
|
19
|
-
def with_role(new_role)
|
|
20
|
-
self.class.new(
|
|
21
|
-
role: new_role.to_s,
|
|
22
|
-
correlation_id: correlation_id,
|
|
23
|
-
now: now,
|
|
24
|
-
dry_run: dry_run,
|
|
25
|
-
)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
data/lib/textus/command.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "dry-struct"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
# Single capability record handed to every use case. Replaces the
|
|
7
|
-
# ReadCaps/WriteCaps/HookCaps trio from 0.26.x. Built once per Store
|
|
8
|
-
# (see Store#initialize); Store delegates its readers to this record,
|
|
9
|
-
# so this struct is the single source of truth for the field set.
|
|
10
|
-
class Container < Dry::Struct
|
|
11
|
-
attribute :manifest, Types::Any
|
|
12
|
-
attribute :file_store, Types::Any
|
|
13
|
-
attribute :schemas, Types::Any
|
|
14
|
-
attribute :root, Types::String
|
|
15
|
-
attribute :audit_log, Types::Any
|
|
16
|
-
attribute :workflows, Types::Any
|
|
17
|
-
attribute :gate, Types::Any
|
|
18
|
-
|
|
19
|
-
def with(**attrs) = self.class.new(to_h.merge(attrs))
|
|
20
|
-
|
|
21
|
-
def initialize(*)
|
|
22
|
-
super
|
|
23
|
-
freeze
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
# Registry of named, stateful wrappers a verb may declare via `around :name`.
|
|
4
|
-
# A resource implements
|
|
5
|
-
# `wrap(scope:, inputs:, session:) { |effective_inputs| ... }`:
|
|
6
|
-
# it may adjust the inputs before the call and post-process the result after
|
|
7
|
-
# — exactly what build's lock and pulse's cursor need, without a hand-authored
|
|
8
|
-
# CLI class (ADR 0068). `session:` is the dispatching session (nil for the
|
|
9
|
-
# sessionless CLI/Ruby surfaces, present for MCP), so a session-aware resource
|
|
10
|
-
# like the cursor can defer to the session's own state instead of its file.
|
|
11
|
-
module Around
|
|
12
|
-
@registry = {}
|
|
13
|
-
|
|
14
|
-
module_function
|
|
15
|
-
|
|
16
|
-
def register(name, resource)
|
|
17
|
-
@registry[name] = resource
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def fetch(name)
|
|
21
|
-
@registry.fetch(name) { raise "no around resource registered: #{name.inspect}" }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def with(name, scope:, inputs:, session: nil, &call)
|
|
25
|
-
fetch(name).wrap(scope: scope, inputs: inputs, session: session, &call)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
# Raised when a required arg is absent from the bound input. Surface
|
|
4
|
-
# adapters translate this to their native error (MCP ToolError, CLI
|
|
5
|
-
# UsageError); a direct Ruby call lets it surface as-is.
|
|
6
|
-
class MissingArgs < Textus::Error
|
|
7
|
-
attr_reader :spec, :missing
|
|
8
|
-
|
|
9
|
-
def initialize(spec, missing)
|
|
10
|
-
@spec = spec
|
|
11
|
-
@missing = missing
|
|
12
|
-
super("missing_args", "#{spec.verb}: missing #{missing.map(&:wire).join(", ")}")
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# The single argument binder for every surface (spike: collapses the three
|
|
17
|
-
# historical implementations — MCP::Catalog.map_args, CLI::Runner.call_args,
|
|
18
|
-
# and RoleScope's default-injection loop — into one algorithm).
|
|
19
|
-
#
|
|
20
|
-
# Input is a uniform `inputs` hash keyed by arg NAME (the use-case kwarg
|
|
21
|
-
# name, never the wire name): each surface normalizes its own raw transport
|
|
22
|
-
# shape (MCP JSON keyed by wire-name, CLI argv+flags, Ruby args+kwargs) into
|
|
23
|
-
# this hash. Binder owns the shared algorithm and nothing transport-specific:
|
|
24
|
-
#
|
|
25
|
-
# 1. validate every required arg is present in `inputs`;
|
|
26
|
-
# 2. for absentees, fall back to session_default (when a session is given)
|
|
27
|
-
# then to the literal default; otherwise omit the arg entirely;
|
|
28
|
-
# 3. split into the (positional, keyword) pair to splat into the use-case,
|
|
29
|
-
# routing by `arg.positional`.
|
|
30
|
-
#
|
|
31
|
-
# Returns `[positional_array, keyword_hash]` — exactly what
|
|
32
|
-
# `RoleScope#<verb>(*pos, **kw)` expects.
|
|
33
|
-
module Binder
|
|
34
|
-
module_function
|
|
35
|
-
|
|
36
|
-
# Validation is unconditional: a `required:` arg absent from `inputs` is a
|
|
37
|
-
# contract violation on every surface (ADR 0069). `required:` is now an
|
|
38
|
-
# honest contract invariant, not a surface policy — args the use-case
|
|
39
|
-
# treats as optional (e.g. `meta`, whose real requiredness lives in schema
|
|
40
|
-
# validation downstream) are declared `required: false`, so this check
|
|
41
|
-
# never fires spuriously and never needs an opt-out.
|
|
42
|
-
def bind(spec, inputs, session: nil)
|
|
43
|
-
missing = spec.required_args.reject { |a| inputs.key?(a.name) }
|
|
44
|
-
raise MissingArgs.new(spec, missing) unless missing.empty?
|
|
45
|
-
|
|
46
|
-
pos = []
|
|
47
|
-
kw = {}
|
|
48
|
-
spec.args.each do |a|
|
|
49
|
-
if inputs.key?(a.name)
|
|
50
|
-
value = inputs[a.name]
|
|
51
|
-
elsif a.session_default && session
|
|
52
|
-
value = session.public_send(a.session_default)
|
|
53
|
-
elsif !a.default.nil?
|
|
54
|
-
value = a.default
|
|
55
|
-
else
|
|
56
|
-
next
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if a.positional
|
|
60
|
-
pos << value
|
|
61
|
-
else
|
|
62
|
-
kw[a.name] = value
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
[pos, kw]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Normalize an ordered positional list + a by-name keyword hash (the shape
|
|
69
|
-
# CLI argv+flags and Ruby args+kwargs both arrive in) into the uniform
|
|
70
|
-
# by-name `inputs` hash bind expects. Positionals beyond what was supplied
|
|
71
|
-
# are dropped so bind's required-check sees them as absent.
|
|
72
|
-
def inputs_from_ordered(spec, ordered_positionals, by_name_keywords)
|
|
73
|
-
names = spec.args.select(&:positional).map(&:name)
|
|
74
|
-
names.zip(ordered_positionals).to_h.compact.merge(by_name_keywords)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Normalize a raw transport hash keyed by WIRE name (the shape MCP JSON
|
|
78
|
-
# arrives in) into the uniform by-name `inputs` hash bind expects. Keys
|
|
79
|
-
# not declared on the contract are ignored.
|
|
80
|
-
def inputs_from_wire(spec, raw)
|
|
81
|
-
raw ||= {}
|
|
82
|
-
spec.args.each_with_object({}) do |a, h|
|
|
83
|
-
h[a.name] = raw[a.wire.to_s] if raw.key?(a.wire.to_s)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
module Resources
|
|
4
|
-
# Serializes builds across every surface (CLI, MCP, Ruby). Previously the
|
|
5
|
-
# CLI verb wrapped each build in a BuildLock by hand; lifting it into the
|
|
6
|
-
# contract means the MCP surface inherits the single-writer guarantee and
|
|
7
|
-
# cannot collide with a concurrent CLI or background build.
|
|
8
|
-
class BuildLock
|
|
9
|
-
def wrap(scope:, inputs:, session: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
10
|
-
Textus::Ports::BuildLock.with(root: scope.container.root) { yield(inputs) }
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
Textus::Contract::Around.register(:build_lock, Textus::Contract::Resources::BuildLock.new)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
module Resources
|
|
4
|
-
# Reads the persisted file cursor as the `since` default when the caller
|
|
5
|
-
# did not supply one, runs pulse, then persists the returned cursor.
|
|
6
|
-
# Replaces CLI::Verb::Pulse's hand-coded CursorStore read/write (ADR 0068).
|
|
7
|
-
#
|
|
8
|
-
# A session-bearing surface (MCP) carries its own cursor via the contract's
|
|
9
|
-
# `session_default: :cursor`, so this defers entirely when a session is
|
|
10
|
-
# present — it is the sessionless CLI/Ruby surfaces that need the file.
|
|
11
|
-
class Cursor
|
|
12
|
-
def wrap(scope:, inputs:, session:)
|
|
13
|
-
return yield(inputs) if session
|
|
14
|
-
|
|
15
|
-
store = Textus::CursorStore.new(root: scope.container.root, role: scope.role)
|
|
16
|
-
effective = inputs.key?(:since) ? inputs : inputs.merge(since: store.read)
|
|
17
|
-
result = yield(effective)
|
|
18
|
-
store.write(result["cursor"]) if result.is_a?(Hash) && result["cursor"]
|
|
19
|
-
result
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
Textus::Contract::Around.register(:cursor, Textus::Contract::Resources::Cursor.new)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Contract
|
|
5
|
-
# CLI-only input acquisition. Transforms entries of the uniform `inputs`
|
|
6
|
-
# hash that declare a `source:`/`coerce:`, and builds `inputs` from a
|
|
7
|
-
# `cli_stdin` envelope — so put/propose/migrate/rule_lint/audit need no
|
|
8
|
-
# hand-authored CLI class (ADR 0068). MCP receives typed JSON, so these
|
|
9
|
-
# never run there.
|
|
10
|
-
module Sources
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
# Apply per-arg :file sources (value is a path -> file contents) and
|
|
14
|
-
# :coerce callables to a by-name inputs hash. Returns a new hash.
|
|
15
|
-
def acquire(spec, inputs)
|
|
16
|
-
spec.args.each_with_object(inputs.dup) do |a, h|
|
|
17
|
-
next unless h.key?(a.name)
|
|
18
|
-
|
|
19
|
-
h[a.name] = File.read(h[a.name]) if a.source == :file
|
|
20
|
-
h[a.name] = a.coerce.call(h[a.name]) if a.coerce
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Parse a cli_stdin :json envelope into a by-name inputs hash, mapping
|
|
25
|
-
# envelope keys (wire-names) to arg names.
|
|
26
|
-
def from_stdin(spec, stream)
|
|
27
|
-
return {} unless spec.cli_stdin == :json
|
|
28
|
-
|
|
29
|
-
raw = stream.read.to_s
|
|
30
|
-
return {} if raw.strip.empty? # no envelope piped -> required args surface as missing
|
|
31
|
-
|
|
32
|
-
envelope = JSON.parse(raw)
|
|
33
|
-
spec.args.each_with_object({}) do |a, h|
|
|
34
|
-
h[a.name] = envelope[a.wire.to_s] if envelope.key?(a.wire.to_s)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/textus/contract/view.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Contract
|
|
3
|
-
# Renders a use-case result for a surface, using the verb's declared view
|
|
4
|
-
# (falling back to the default). The single replacement for the old
|
|
5
|
-
# response/cli_response split and the Proc#arity sniff: views are always
|
|
6
|
-
# called as (result, inputs); a one-parameter view ignores inputs.
|
|
7
|
-
module View
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def render(spec, surface, result, inputs)
|
|
11
|
-
spec.view(surface).call(result, inputs)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/textus/contract.rb
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Declarative, co-located interface contract for a verb. One source of truth
|
|
3
|
-
# for the agent-facing summary, the argument schema, which transports expose
|
|
4
|
-
# the verb, and how the return value is shaped for the wire. CLI/Ruby/MCP and
|
|
5
|
-
# boot project from this; the MCP catalog is fully derived from it (ADR 0039).
|
|
6
|
-
module Contract
|
|
7
|
-
# One argument of a verb. `positional: true` means it is passed to the
|
|
8
|
-
# use-case as a positional (e.g. `get(key)`); otherwise as a keyword.
|
|
9
|
-
# `session_default` names a zero-arg method on `Textus::Session` (Symbol)
|
|
10
|
-
# that supplies the value when the wire arg is absent; `nil` means no default.
|
|
11
|
-
# `wire_name` is the name the arg carries on the wire (MCP JSON property / CLI
|
|
12
|
-
# envelope key) when it must differ from the use-case kwarg `name` — e.g. `put`
|
|
13
|
-
# takes the `meta:` kwarg but exposes `_meta` on the wire to match what `get`
|
|
14
|
-
# returns and what the CLI `--stdin` envelope already speaks (ADR 0057).
|
|
15
|
-
# `source: :file` (CLI only) reads the arg's value as a path -> file
|
|
16
|
-
# contents; `coerce:` is a callable applied to the raw value (CLI only);
|
|
17
|
-
# `cli_default:` supplies a CLI-specific default that diverges from the
|
|
18
|
-
# contract `default` the agent surfaces use (`:__unset` sentinel = none).
|
|
19
|
-
Arg = Data.define(
|
|
20
|
-
:name, :type, :required, :positional, :session_default,
|
|
21
|
-
:description, :wire_name, :default, :source, :coerce, :cli_default
|
|
22
|
-
) do
|
|
23
|
-
# The name used on the wire (defaults to the kwarg name).
|
|
24
|
-
def wire = wire_name || name
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
JSON_TYPES = {
|
|
28
|
-
String => "string", Integer => "integer", Hash => "object",
|
|
29
|
-
Array => "array", :boolean => "boolean"
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
def self.json_type(type)
|
|
33
|
-
JSON_TYPES.fetch(type) { raise ArgumentError.new("no JSON type mapping for #{type.inspect}") }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
Spec = Data.define(:verb, :summary, :args, :surfaces, :views, :cli, :around, :cli_stdin) do
|
|
37
|
-
def mcp? = surfaces.include?(:mcp)
|
|
38
|
-
def cli? = surfaces.include?(:cli)
|
|
39
|
-
|
|
40
|
-
# The output shaper for a surface; falls back to the default view. Every
|
|
41
|
-
# view is invoked uniformly as `view.call(result, inputs)` — a view that
|
|
42
|
-
# declares one parameter ignores `inputs` (procs tolerate extra args).
|
|
43
|
-
def view(surface = :default) = views[surface] || views.fetch(:default)
|
|
44
|
-
|
|
45
|
-
# Operator-facing command path. Defaults to the verb token; grouped verbs
|
|
46
|
-
# declare e.g. `cli "schema show"`.
|
|
47
|
-
def cli_path = cli || verb.to_s
|
|
48
|
-
def cli_words = cli_path.split
|
|
49
|
-
def cli_group = cli_words.size > 1 ? cli_words.first : nil
|
|
50
|
-
def cli_leaf = cli_words.last
|
|
51
|
-
|
|
52
|
-
def required_args = args.select(&:required)
|
|
53
|
-
|
|
54
|
-
# JSON-Schema object for MCP tools/list inputSchema.
|
|
55
|
-
# Outer keys (:type, :properties, :required) are symbols; inner property
|
|
56
|
-
# keys are strings — matches the MCP/JSON wire shape expected by clients.
|
|
57
|
-
def input_schema
|
|
58
|
-
props = args.to_h do |a|
|
|
59
|
-
h = { "type" => Contract.json_type(a.type) }
|
|
60
|
-
h["description"] = a.description if a.description
|
|
61
|
-
[a.wire.to_s, h]
|
|
62
|
-
end
|
|
63
|
-
{ type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Mixed onto a use-case class via `extend`. Calls accumulate into ivars,
|
|
68
|
-
# frozen into a Spec on first read of `.contract`.
|
|
69
|
-
module DSL
|
|
70
|
-
def verb(name = nil)
|
|
71
|
-
if name
|
|
72
|
-
raise "contract already built; declare verb before reading .contract" if defined?(@__contract) && @__contract
|
|
73
|
-
|
|
74
|
-
@__verb = name
|
|
75
|
-
else
|
|
76
|
-
@__verb
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def summary(text = nil)
|
|
81
|
-
if text
|
|
82
|
-
raise "contract already built; declare summary before reading .contract" if defined?(@__contract) && @__contract
|
|
83
|
-
|
|
84
|
-
@__summary = text
|
|
85
|
-
else
|
|
86
|
-
@__summary
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def surfaces(*list)
|
|
91
|
-
if list.empty?
|
|
92
|
-
@__surfaces ||= []
|
|
93
|
-
else
|
|
94
|
-
raise "contract already built; declare surfaces before reading .contract" if defined?(@__contract) && @__contract
|
|
95
|
-
|
|
96
|
-
@__surfaces = list
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def cli(path = nil)
|
|
101
|
-
if path
|
|
102
|
-
raise "contract already built; declare cli before reading .contract" if defined?(@__contract) && @__contract
|
|
103
|
-
|
|
104
|
-
@__cli = path.to_s
|
|
105
|
-
else
|
|
106
|
-
@__cli
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Declare a stateful wrapper resource (Contract::Around) to run around
|
|
111
|
-
# dispatch — e.g. `around :cursor` (pulse) or `around :build_lock` (build).
|
|
112
|
-
def around(name = nil)
|
|
113
|
-
return @__around unless name
|
|
114
|
-
|
|
115
|
-
raise "contract already built; declare around before reading .contract" if defined?(@__contract) && @__contract
|
|
116
|
-
|
|
117
|
-
@__around = name
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def arg(name, type, required: false, positional: false, session_default: nil, description: nil, wire_name: nil, default: nil, source: nil, coerce: nil, cli_default: :__unset) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
|
|
121
|
-
raise "contract already built; declare args before reading .contract" if defined?(@__contract) && @__contract
|
|
122
|
-
|
|
123
|
-
(@__args ||= []) << Arg.new(
|
|
124
|
-
name: name, type: type, required: required,
|
|
125
|
-
positional: positional, session_default: session_default,
|
|
126
|
-
description: description, wire_name: wire_name, default: default,
|
|
127
|
-
source: source, coerce: coerce, cli_default: cli_default
|
|
128
|
-
)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Verb-level: the CLI reads its inputs from a stdin envelope of this mode.
|
|
132
|
-
# `:json` parses stdin as a JSON object and distributes its keys to args
|
|
133
|
-
# by wire-name. nil means no stdin acquisition.
|
|
134
|
-
def cli_stdin(mode = :__read)
|
|
135
|
-
return @__cli_stdin if mode == :__read
|
|
136
|
-
|
|
137
|
-
raise "contract already built; declare cli_stdin before reading .contract" if defined?(@__contract) && @__contract
|
|
138
|
-
|
|
139
|
-
@__cli_stdin = mode
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Declare an output shaper. `view { ... }` is the default (MCP + Ruby);
|
|
143
|
-
# `view(:cli) { ... }` overrides for the CLI. Both receive (result, inputs).
|
|
144
|
-
def view(surface = :default, &blk)
|
|
145
|
-
return (@__views ||= {})[surface] unless blk
|
|
146
|
-
|
|
147
|
-
raise "contract already built; declare view before reading .contract" if defined?(@__contract) && @__contract
|
|
148
|
-
|
|
149
|
-
(@__views ||= {})[surface] = blk
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def contract?
|
|
153
|
-
!@__verb.nil?
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
157
|
-
# @__contract uses double-underscore to match the other accumulator ivars
|
|
158
|
-
# (@__verb, @__args, etc.) and avoid name collision with user-defined `@contract`.
|
|
159
|
-
def contract
|
|
160
|
-
@__contract ||= Spec.new(
|
|
161
|
-
verb: @__verb,
|
|
162
|
-
summary: @__summary,
|
|
163
|
-
args: (@__args || []).freeze,
|
|
164
|
-
surfaces: (@__surfaces || []).freeze,
|
|
165
|
-
views: ((@__views ||= {})[:default] ||= ->(v, _i) { v }) && @__views,
|
|
166
|
-
cli: @__cli,
|
|
167
|
-
around: @__around,
|
|
168
|
-
cli_stdin: @__cli_stdin,
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|