textus 0.51.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +13 -13
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +19 -11
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +17 -63
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/role.rb +1 -1
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -4
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +133 -107
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -69
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
data/lib/textus/mcp/errors.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Manifest fingerprint changed mid-session. Client should re-boot.
|
|
4
|
-
class ContractDrift < Textus::Error
|
|
5
|
-
JSONRPC_CODE = -32_001
|
|
6
|
-
|
|
7
|
-
def initialize(message, details: {})
|
|
8
|
-
super("contract_drift", message, details: details)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Audit cursor fell off the keep window. Client should re-boot and
|
|
13
|
-
# resume from the new latest_seq.
|
|
14
|
-
class CursorExpired < Textus::Error
|
|
15
|
-
JSONRPC_CODE = -32_002
|
|
16
|
-
|
|
17
|
-
def initialize(message, details: {})
|
|
18
|
-
super("cursor_expired", message, details: details)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
23
|
-
# underlying Textus::Error or generic StandardError.
|
|
24
|
-
class ToolError < Textus::Error
|
|
25
|
-
JSONRPC_CODE = -32_000
|
|
26
|
-
|
|
27
|
-
def initialize(message, details: {})
|
|
28
|
-
super("tool_error", message, details: details)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
data/lib/textus/mcp/server.rb
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module MCP
|
|
5
|
-
# Stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05. One line per
|
|
6
|
-
# message (NDJSON). Holds a single Session for the lifetime of stdin.
|
|
7
|
-
class Server
|
|
8
|
-
PROTOCOL_VERSION = "2024-11-05"
|
|
9
|
-
SERVER_INFO = { "name" => "textus", "version" => Textus::VERSION }.freeze
|
|
10
|
-
|
|
11
|
-
def initialize(store:, stdin: $stdin, stdout: $stdout, role: Textus::Role::DEFAULT)
|
|
12
|
-
@store = store
|
|
13
|
-
@stdin = stdin
|
|
14
|
-
@stdout = stdout
|
|
15
|
-
@role = role
|
|
16
|
-
@session = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run
|
|
20
|
-
@stdin.each_line do |line|
|
|
21
|
-
line = line.strip
|
|
22
|
-
next if line.empty?
|
|
23
|
-
|
|
24
|
-
handle_line(line)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def handle_line(line)
|
|
31
|
-
msg = JSON.parse(line)
|
|
32
|
-
rescue JSON::ParserError => e
|
|
33
|
-
emit_error(nil, -32_700, "parse error: #{e.message}")
|
|
34
|
-
else
|
|
35
|
-
dispatch(msg)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def dispatch(msg)
|
|
39
|
-
rid = msg["id"]
|
|
40
|
-
case msg["method"]
|
|
41
|
-
when "initialize" then handle_initialize(rid, msg["params"] || {})
|
|
42
|
-
when "tools/list" then handle_tools_list(rid)
|
|
43
|
-
when "tools/call" then handle_tools_call(rid, msg["params"] || {})
|
|
44
|
-
when "ping" then emit_result(rid, {})
|
|
45
|
-
when "shutdown" then emit_result(rid, nil)
|
|
46
|
-
when "notifications/initialized" then nil
|
|
47
|
-
else emit_error(rid, -32_601, "method not found: #{msg["method"]}")
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def handle_initialize(rid, _params)
|
|
52
|
-
# The acting role IS the resolved connection role (ADR 0040): the MCP
|
|
53
|
-
# transport defaults to `agent`, which can write the queue, so its
|
|
54
|
-
# propose_zone resolves directly. If a connection's role cannot propose,
|
|
55
|
-
# propose_zone is nil and the `propose` tool reports that honestly.
|
|
56
|
-
propose_zone = @store.manifest.policy.propose_zone_for(@role)
|
|
57
|
-
|
|
58
|
-
@session = Session.new(
|
|
59
|
-
role: @role,
|
|
60
|
-
cursor: @store.audit_log.latest_seq,
|
|
61
|
-
propose_zone: propose_zone,
|
|
62
|
-
contract_etag: contract_etag,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# ADR 0075: announce the connection to connect-time hooks with the
|
|
66
|
-
# resolved role. Distinct from :store_loaded (fired at Store.new under
|
|
67
|
-
# the default role, before any connection's role is known).
|
|
68
|
-
@store.events.publish(
|
|
69
|
-
:session_opened,
|
|
70
|
-
ctx: Hooks::Context.new(scope: @store.as(@role)),
|
|
71
|
-
role: @role,
|
|
72
|
-
cursor: @session.cursor,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
emit_result(rid, {
|
|
76
|
-
"protocolVersion" => PROTOCOL_VERSION,
|
|
77
|
-
"serverInfo" => SERVER_INFO,
|
|
78
|
-
"capabilities" => { "tools" => {} },
|
|
79
|
-
})
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def handle_tools_list(rid)
|
|
83
|
-
emit_result(rid, { "tools" => Catalog.tool_schemas })
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def handle_tools_call(rid, params)
|
|
87
|
-
unless @session
|
|
88
|
-
emit_error(rid, -32_002, "session not initialized; call 'initialize' first")
|
|
89
|
-
return
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
name = params["name"]
|
|
93
|
-
args = params["arguments"] || {}
|
|
94
|
-
|
|
95
|
-
# ADR 0083: the contract-drift guard gates mutating verbs — every MCP
|
|
96
|
-
# verb that is NOT a pure read (Write:: + the destructive Maintenance::
|
|
97
|
-
# verbs reconcile/zone_mv/key_*_prefix). Reads and boot bypass it (a stale
|
|
98
|
-
# read returns on-disk truth; boot re-orients). Keying on read_verbs
|
|
99
|
-
# (not write_verbs) keeps the destructive Maintenance:: verbs gated.
|
|
100
|
-
@session.check_etag!(contract_etag) unless Catalog.read_verbs.include?(name)
|
|
101
|
-
|
|
102
|
-
result = Catalog.call(name, session: @session, store: @store, args: args)
|
|
103
|
-
@session = @session.advance_cursor(@store.audit_log.latest_seq) if name == "pulse"
|
|
104
|
-
@session = @session.with(contract_etag: contract_etag) if name == "boot"
|
|
105
|
-
|
|
106
|
-
emit_result(rid, {
|
|
107
|
-
"content" => [{ "type" => "text", "text" => JSON.dump(result) }],
|
|
108
|
-
"isError" => false,
|
|
109
|
-
})
|
|
110
|
-
rescue ContractDrift => e
|
|
111
|
-
emit_error(rid, ContractDrift::JSONRPC_CODE, e.message)
|
|
112
|
-
rescue CursorExpired => e
|
|
113
|
-
emit_error(rid, CursorExpired::JSONRPC_CODE, e.message)
|
|
114
|
-
rescue ToolError => e
|
|
115
|
-
emit_error(rid, ToolError::JSONRPC_CODE, e.message)
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
emit_error(rid, -32_603, "internal: #{e.class}: #{e.message}")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def contract_etag
|
|
121
|
-
Textus::Etag.for_contract(@store.root)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def emit_result(rid, result)
|
|
125
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "result" => result })
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def emit_error(rid, code, message)
|
|
129
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "error" => { "code" => code, "message" => message } })
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def write(obj)
|
|
133
|
-
@stdout.puts(JSON.dump(obj))
|
|
134
|
-
@stdout.flush
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
data/lib/textus/mcp/session.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Kept for name stability (ADR 0039). The JSON schemas are DERIVED from
|
|
4
|
-
# per-verb contracts; this delegates to MCP::Catalog. The hand-written
|
|
5
|
-
# array is gone — a kwarg rename now updates the schema automatically (and
|
|
6
|
-
# the signature guard fails if the contract lags the use-case).
|
|
7
|
-
module ToolSchemas
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def all
|
|
11
|
-
Catalog.tool_schemas
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/textus/mcp.rb
DELETED
data/lib/textus/mustache.rb
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Mustache
|
|
3
|
-
MAX_DEPTH = 8
|
|
4
|
-
TAG = %r{\{\{(?<sigil>[#^/!&]?)\s*(?<name>[\w.-]+)\s*\}\}}
|
|
5
|
-
|
|
6
|
-
def self.render(template, context, strict: false, depth: 0) # rubocop:disable Metrics/AbcSize
|
|
7
|
-
raise TemplateError.new("template recursion depth #{depth} exceeded #{MAX_DEPTH}") if depth > MAX_DEPTH
|
|
8
|
-
|
|
9
|
-
out = +""
|
|
10
|
-
pos = 0
|
|
11
|
-
while (m = template.match(TAG, pos))
|
|
12
|
-
out << template[pos...m.begin(0)]
|
|
13
|
-
case m[:sigil]
|
|
14
|
-
when "!"
|
|
15
|
-
# comment, skip
|
|
16
|
-
when "#"
|
|
17
|
-
section, new_pos = parse_section(template, m, m[:name])
|
|
18
|
-
value = lookup(context, m[:name])
|
|
19
|
-
out << render_section(section, value, context, strict, depth)
|
|
20
|
-
pos = new_pos
|
|
21
|
-
next
|
|
22
|
-
when "^"
|
|
23
|
-
section, new_pos = parse_section(template, m, m[:name])
|
|
24
|
-
value = lookup(context, m[:name])
|
|
25
|
-
if falsy?(value)
|
|
26
|
-
raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH
|
|
27
|
-
|
|
28
|
-
out << render(section, context, strict: strict, depth: depth + 1)
|
|
29
|
-
end
|
|
30
|
-
pos = new_pos
|
|
31
|
-
next
|
|
32
|
-
when "/"
|
|
33
|
-
raise TemplateError.new("unexpected closing tag #{m[:name]}")
|
|
34
|
-
else
|
|
35
|
-
val = lookup(context, m[:name])
|
|
36
|
-
if val.nil?
|
|
37
|
-
raise TemplateError.new("missing variable: #{m[:name]}") if strict
|
|
38
|
-
else
|
|
39
|
-
out << val.to_s
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
pos = m.end(0)
|
|
43
|
-
end
|
|
44
|
-
out << template[pos..]
|
|
45
|
-
out
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.parse_section(template, open_match, name)
|
|
49
|
-
open_re = /\{\{#\s*#{Regexp.escape(name)}\s*\}\}|\{\{\^\s*#{Regexp.escape(name)}\s*\}\}/
|
|
50
|
-
close_re = %r{\{\{/\s*#{Regexp.escape(name)}\s*\}\}}
|
|
51
|
-
both = Regexp.union(open_re, close_re)
|
|
52
|
-
depth = 1
|
|
53
|
-
cursor = open_match.end(0)
|
|
54
|
-
while (m = template.match(both, cursor))
|
|
55
|
-
if m[0].start_with?("{{/")
|
|
56
|
-
depth -= 1
|
|
57
|
-
return [template[open_match.end(0)...m.begin(0)], m.end(0)] if depth.zero?
|
|
58
|
-
else
|
|
59
|
-
depth += 1
|
|
60
|
-
end
|
|
61
|
-
cursor = m.end(0)
|
|
62
|
-
end
|
|
63
|
-
raise TemplateError.new("unclosed section: #{name}")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.render_section(section, value, context, strict, depth)
|
|
67
|
-
raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH
|
|
68
|
-
|
|
69
|
-
case value
|
|
70
|
-
when Array
|
|
71
|
-
value.map { |v| render(section, scope_for(context, v), strict: strict, depth: depth + 1) }.join
|
|
72
|
-
when Hash
|
|
73
|
-
render(section, merge(context, value), strict: strict, depth: depth + 1)
|
|
74
|
-
when true
|
|
75
|
-
render(section, context, strict: strict, depth: depth + 1)
|
|
76
|
-
when false, nil
|
|
77
|
-
# falsy in regular section: render nothing.
|
|
78
|
-
# render_section is only called for inverted sections when falsy? is true at the call site,
|
|
79
|
-
# so this branch is only hit for normal sections with falsy values.
|
|
80
|
-
""
|
|
81
|
-
else
|
|
82
|
-
render(section, context, strict: strict, depth: depth + 1)
|
|
83
|
-
end || ""
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def self.lookup(context, name)
|
|
87
|
-
# Implicit iterator: {{.}} refers to the current scope itself (used when
|
|
88
|
-
# iterating arrays of primitive values).
|
|
89
|
-
return context["."] if name == "." && context.is_a?(Hash) && context.key?(".")
|
|
90
|
-
return context[name] if context.is_a?(Hash) && context.key?(name)
|
|
91
|
-
|
|
92
|
-
name.split(".").reduce(context) do |acc, seg|
|
|
93
|
-
return nil unless acc.is_a?(Hash)
|
|
94
|
-
|
|
95
|
-
acc[seg]
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Build the rendering scope for one iteration of a section. Hash items
|
|
100
|
-
# merge into the outer context; primitive items (strings, numbers) bind
|
|
101
|
-
# to the implicit iterator under key ".".
|
|
102
|
-
def self.scope_for(context, item)
|
|
103
|
-
return merge(context, item) if item.is_a?(Hash)
|
|
104
|
-
|
|
105
|
-
base = context.is_a?(Hash) ? context : {}
|
|
106
|
-
base.merge("." => item)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def self.merge(base, override)
|
|
110
|
-
return base unless override.is_a?(Hash)
|
|
111
|
-
|
|
112
|
-
base.merge(override)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def self.falsy?(v) = v.nil? || v == false || v == [] || v == ""
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Ports
|
|
5
|
-
# ADR 0093: on a canon write, converge the derived entries that depend on the
|
|
6
|
-
# written key (rdeps ∩ derived) by running Produce — scoped + non-destructive.
|
|
7
|
-
# This IS reconcile narrowed to a write's blast radius; there is no separate
|
|
8
|
-
# "reactive materialize" subsystem. Per-entry source.on_write (sync|async)
|
|
9
|
-
# picks inline-under-lock vs deferred. A write INTO a derived entry does not
|
|
10
|
-
# fan out (recursion guard). Failures never reach the writer (Produce.converge
|
|
11
|
-
# isolates them). Attached at Store boot, alongside AuditSubscriber.
|
|
12
|
-
class ProduceOnWriteSubscriber
|
|
13
|
-
def initialize(container)
|
|
14
|
-
@container = container
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def attach(bus)
|
|
18
|
-
bus.on(:entry_written, :produce_on_write) do |ctx:, key:, **|
|
|
19
|
-
call = Textus::Call.build(role: ctx.role, correlation_id: ctx.correlation_id)
|
|
20
|
-
on_write(key: key, call: call)
|
|
21
|
-
end
|
|
22
|
-
self
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def on_write(key:, call:)
|
|
26
|
-
return if derived_write?(key) # recursion guard: produce output is not a source change
|
|
27
|
-
|
|
28
|
-
affected = Textus::Read::Rdeps.new(container: @container).call(key)["rdeps"]
|
|
29
|
-
producible = affected.select { |k| producible?(k) }
|
|
30
|
-
return if producible.empty?
|
|
31
|
-
|
|
32
|
-
if any_sync?(producible)
|
|
33
|
-
Textus::Produce::Engine.converge(container: @container, call: call, keys: producible)
|
|
34
|
-
else
|
|
35
|
-
Textus::Produce::Engine::AsyncRunner.enqueue(container: @container, call: call, keys: producible)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def derived_write?(key)
|
|
42
|
-
@container.manifest.resolver.resolve(key).entry.derived?
|
|
43
|
-
rescue Textus::Error
|
|
44
|
-
false
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# The producible scope mirrors Produce::Engine#produce_one: derived
|
|
48
|
-
# entries render+publish, and nested publish_tree entries mirror their
|
|
49
|
-
# source subtree (ADR 0047). Including the latter restores reactive
|
|
50
|
-
# re-mirroring on a write into a tree's source — dropped when the scope
|
|
51
|
-
# narrowed to `derived?` only.
|
|
52
|
-
def producible?(key)
|
|
53
|
-
entry = @container.manifest.resolver.resolve(key).entry
|
|
54
|
-
entry.derived? || !entry.publish_tree.nil?
|
|
55
|
-
rescue Textus::Error
|
|
56
|
-
false
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Only derived entries carry a source with on_write semantics; a nested
|
|
60
|
-
# publish_tree entry has no source and defaults to async.
|
|
61
|
-
def any_sync?(keys)
|
|
62
|
-
keys.any? do |k|
|
|
63
|
-
entry = @container.manifest.resolver.resolve(k).entry
|
|
64
|
-
entry.derived? && entry.source.sync?
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
# Single home for the fetch lifecycle event vocabulary (ADR 0048 D5).
|
|
4
|
-
# Produce::Acquire::Intake (the ingest executor driven by reconcile + hook) emits through
|
|
5
|
-
# this seam so the event names and payload shapes live in one place with one
|
|
6
|
-
# derived hook context.
|
|
7
|
-
class Events
|
|
8
|
-
def self.from(container:, call:)
|
|
9
|
-
new(
|
|
10
|
-
events: container.events,
|
|
11
|
-
hook_context: Textus::Hooks::Context.for(container: container, call: call),
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(events:, hook_context:)
|
|
16
|
-
@events = events
|
|
17
|
-
@hook_context = hook_context
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def started(key, mode: :sync)
|
|
21
|
-
@events.publish(:entry_fetch_started, ctx: @hook_context, key: key, mode: mode)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def failed(key, error)
|
|
25
|
-
@events.publish(:entry_fetch_failed, ctx: @hook_context, key: key,
|
|
26
|
-
error_class: error.class.name, error_message: error.message)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def fetched(key, envelope, change)
|
|
30
|
-
return if change == :unchanged
|
|
31
|
-
|
|
32
|
-
@events.publish(:entry_fetched, ctx: @hook_context, key: key, envelope: envelope, change: change)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
data/lib/textus/read/audit.rb
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "time"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Read
|
|
6
|
-
# Queries .textus/.run/audit/audit.log. Filters: key, zone, role, verb, since,
|
|
7
|
-
# correlation_id, limit. Reads the log file as JSON-Lines (legacy TSV
|
|
8
|
-
# rows produce nil and are skipped).
|
|
9
|
-
class Audit
|
|
10
|
-
# Value object that carries all filter parameters for an audit query.
|
|
11
|
-
# `matches?` checks the manifest-independent predicates so the loop body
|
|
12
|
-
# only needs to handle the zone check (which requires manifest access).
|
|
13
|
-
Query = Data.define(:key, :zone, :role, :verb, :since, :seq_since, :correlation_id, :limit) do
|
|
14
|
-
# rubocop:disable Metrics/ParameterLists
|
|
15
|
-
def self.build(key: nil, zone: nil, role: nil, verb: nil,
|
|
16
|
-
since: nil, seq_since: nil, correlation_id: nil, limit: nil)
|
|
17
|
-
new(key:, zone:, role:, verb:, since:, seq_since:, correlation_id:, limit:)
|
|
18
|
-
end
|
|
19
|
-
# rubocop:enable Metrics/ParameterLists
|
|
20
|
-
|
|
21
|
-
def matches?(row)
|
|
22
|
-
return false if key && row["key"] != key
|
|
23
|
-
return false if role && row["role"] != role
|
|
24
|
-
return false if verb && row["verb"] != verb
|
|
25
|
-
return false if since && (row["ts"].nil? || Time.parse(row["ts"]) < since)
|
|
26
|
-
return false if seq_since && (row["seq"].nil? || row["seq"] <= seq_since)
|
|
27
|
-
return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
|
|
28
|
-
|
|
29
|
-
true
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
extend Textus::Contract::DSL
|
|
34
|
-
|
|
35
|
-
verb :audit
|
|
36
|
-
summary "Query the audit log with optional filters."
|
|
37
|
-
surfaces :cli
|
|
38
|
-
cli "audit"
|
|
39
|
-
# #call(**filters) — args map to Query.build keyword params (ADR 0063)
|
|
40
|
-
arg :key, String, required: false, description: "filter to rows for this key"
|
|
41
|
-
arg :zone, String, required: false, description: "filter to keys in this zone"
|
|
42
|
-
arg :role, String, required: false, description: "filter to rows written under this role"
|
|
43
|
-
arg :verb, String, required: false, description: "filter to rows for this verb"
|
|
44
|
-
arg :since, String, required: false,
|
|
45
|
-
coerce: ->(s) { Textus::Read::Audit.parse_since(s, now: Time.now) },
|
|
46
|
-
description: "ISO-8601 timestamp or relative offset (e.g. 1h, 30m)"
|
|
47
|
-
arg :seq_since, Integer, required: false, description: "return rows with seq > this cursor value"
|
|
48
|
-
arg :correlation_id, String, required: false, description: "filter to rows with this correlation_id"
|
|
49
|
-
arg :limit, Integer, required: false, description: "maximum number of rows to return"
|
|
50
|
-
view(:cli) { |rows, _i| { "verb" => "audit", "rows" => rows } }
|
|
51
|
-
|
|
52
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
53
|
-
@manifest = container.manifest
|
|
54
|
-
@root = container.root
|
|
55
|
-
@log_path = Textus::Layout.audit_log(container.root)
|
|
56
|
-
@audit_log = container.audit_log
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def call(**filters)
|
|
60
|
-
query = Query.build(**filters)
|
|
61
|
-
check_cursor_expiry!(query.seq_since)
|
|
62
|
-
|
|
63
|
-
files = all_log_files
|
|
64
|
-
return [] if files.empty?
|
|
65
|
-
|
|
66
|
-
rows = []
|
|
67
|
-
files.each do |file|
|
|
68
|
-
File.foreach(file) do |line|
|
|
69
|
-
parsed = parse_row(line.chomp)
|
|
70
|
-
next unless parsed
|
|
71
|
-
next unless query.matches?(parsed)
|
|
72
|
-
next if query.zone && !key_in_zone?(parsed["key"], query.zone)
|
|
73
|
-
|
|
74
|
-
rows << parsed
|
|
75
|
-
break if limit_reached?(rows, query)
|
|
76
|
-
end
|
|
77
|
-
break if limit_reached?(rows, query)
|
|
78
|
-
end
|
|
79
|
-
rows
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Accepts ISO8601 ("2026-01-15", "2026-01-15T10:00:00Z") or a relative
|
|
83
|
-
# offset matching /\A(\d+)([smhd])\z/. Returns nil for unparseable input.
|
|
84
|
-
def self.parse_since(str, now: Time.now.utc)
|
|
85
|
-
return nil if str.nil? || str.empty?
|
|
86
|
-
return Time.parse(str) if str =~ /\A\d{4}-\d{2}-\d{2}/
|
|
87
|
-
|
|
88
|
-
m = str.match(/\A(\d+)([smhd])\z/) or return nil
|
|
89
|
-
mult = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }[m[2]]
|
|
90
|
-
now - (m[1].to_i * mult)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def limit_reached?(rows, query) = query.limit && rows.length >= query.limit
|
|
96
|
-
|
|
97
|
-
def check_cursor_expiry!(seq_since)
|
|
98
|
-
return unless seq_since
|
|
99
|
-
|
|
100
|
-
log = @audit_log || Textus::Ports::AuditLog.new(@root)
|
|
101
|
-
min = log.min_available_seq
|
|
102
|
-
raise Textus::CursorExpired.new(requested: seq_since, min_available: min) if min && seq_since < min - 1
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def all_log_files
|
|
106
|
-
rotated = Dir.glob(File.join(Textus::Layout.audit_dir(@root), "audit.log.*"))
|
|
107
|
-
.reject { |p| p.end_with?(".meta.json") }
|
|
108
|
-
.sort_by { |p| -p.scan(/\d+$/).first.to_i } # .5 .4 .3 .2 .1 → oldest first
|
|
109
|
-
active = File.exist?(@log_path) ? [@log_path] : []
|
|
110
|
-
rotated + active
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def parse_row(line)
|
|
114
|
-
return nil if line.empty?
|
|
115
|
-
return nil unless line.start_with?("{")
|
|
116
|
-
|
|
117
|
-
JSON.parse(line)
|
|
118
|
-
rescue JSON::ParserError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def key_in_zone?(key, zone)
|
|
123
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
124
|
-
mentry && mentry.zone == zone
|
|
125
|
-
rescue Textus::Error
|
|
126
|
-
false
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
data/lib/textus/read/boot.rb
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Dispatched use case for the `boot` verb. The orientation envelope is
|
|
4
|
-
# built by the Textus::Boot library module; this class is the uniform
|
|
5
|
-
# (container:, call:) entry point that Dispatcher::VERBS resolves to.
|
|
6
|
-
# Boot is role-independent, so `call` is not consulted.
|
|
7
|
-
class Boot
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :boot
|
|
11
|
-
summary "Return the orientation contract: zones, entries, schemas, write_flows, agent_quickstart."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
arg :lean, :boolean,
|
|
14
|
-
description: "return only orientation essentials (zones, agent_quickstart, contract_etag) for cheap session-start injection"
|
|
15
|
-
|
|
16
|
-
def initialize(container:, call:)
|
|
17
|
-
@container = container
|
|
18
|
-
@call = call
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def call(lean: false)
|
|
22
|
-
Textus::Boot.build(container: @container, lean: lean)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# A machine-readable projection of the contract surface: every verb, the
|
|
4
|
-
# transports it reaches, and its full argument schema — sourced from the
|
|
5
|
-
# same Contract DSL the CLI/MCP/boot already project from (ADR 0039/0063).
|
|
6
|
-
#
|
|
7
|
-
# Integrators assert their docs against this in CI so they can't drift
|
|
8
|
-
# (#161 F4 — patrick-nexus docs claimed "MCP exposes 3 verbs" while ~20 are
|
|
9
|
-
# surfaced). It also makes the per-surface `dry_run` default asymmetry
|
|
10
|
-
# (#161 F6) self-documenting: each arg carries both `default` (agent wire)
|
|
11
|
-
# and `cli_default` (CLI), so the divergence is visible, not folklore.
|
|
12
|
-
#
|
|
13
|
-
# Pure contract introspection — it reads no store data; `container` is
|
|
14
|
-
# accepted only for the uniform use-case constructor.
|
|
15
|
-
class Capabilities
|
|
16
|
-
extend Textus::Contract::DSL
|
|
17
|
-
|
|
18
|
-
verb :capabilities
|
|
19
|
-
summary "Machine-readable contract surface: every verb, its transports, and arg schema."
|
|
20
|
-
surfaces :cli, :mcp
|
|
21
|
-
arg :verb, String, required: false, description: "filter to a single verb by name"
|
|
22
|
-
view { |result, _i| result }
|
|
23
|
-
|
|
24
|
-
def initialize(container: nil, call: nil); end
|
|
25
|
-
|
|
26
|
-
def call(verb: nil)
|
|
27
|
-
klasses = Textus::Dispatcher::VERBS.values.select { |k| contract?(k) }
|
|
28
|
-
rows = klasses.map { |k| project(k.contract) }
|
|
29
|
-
rows.select! { |r| r["verb"] == verb } if verb
|
|
30
|
-
{ "verbs" => rows.sort_by { |r| r["verb"] } }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def contract?(klass)
|
|
36
|
-
klass.respond_to?(:contract?) && klass.contract?
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def project(spec)
|
|
40
|
-
{
|
|
41
|
-
"verb" => spec.verb.to_s,
|
|
42
|
-
"summary" => spec.summary,
|
|
43
|
-
"surfaces" => spec.surfaces.map(&:to_s) + ["ruby"],
|
|
44
|
-
"cli" => spec.cli? ? spec.cli_path : nil,
|
|
45
|
-
"args" => spec.args.map { |a| project_arg(a) },
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def project_arg(arg)
|
|
50
|
-
out = {
|
|
51
|
-
"name" => arg.wire.to_s,
|
|
52
|
-
"type" => json_type(arg.type),
|
|
53
|
-
"required" => arg.required,
|
|
54
|
-
"positional" => arg.positional,
|
|
55
|
-
}
|
|
56
|
-
out["description"] = arg.description if arg.description
|
|
57
|
-
out["default"] = arg.default unless arg.default.nil?
|
|
58
|
-
out["cli_default"] = arg.cli_default unless arg.cli_default == :__unset
|
|
59
|
-
out["session_default"] = arg.session_default.to_s if arg.session_default
|
|
60
|
-
out
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def json_type(type)
|
|
64
|
-
Textus::Contract.json_type(type)
|
|
65
|
-
rescue ArgumentError
|
|
66
|
-
"string"
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|