textus 0.55.1 → 0.56.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 +175 -2768
- data/README.md +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{gate → protocol}/binder.rb +2 -10
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -61
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- 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/core/sentinel.rb +0 -30
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/notebook_sources.rb +0 -50
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -3,20 +3,22 @@ module Textus
|
|
|
3
3
|
class CLI
|
|
4
4
|
class Verb
|
|
5
5
|
class Put < Runner::Base
|
|
6
|
-
self.spec = Textus::
|
|
6
|
+
self.spec = Textus::Protocol::VerbRegistry.for(:put)
|
|
7
7
|
option :as_flag, "--as=ROLE"
|
|
8
8
|
option :use_stdin, "--stdin"
|
|
9
|
+
option :as_format, "--as-format=FORMAT"
|
|
9
10
|
|
|
10
11
|
def invoke(store)
|
|
11
12
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
12
13
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
13
14
|
|
|
14
15
|
payload = JSON.parse(@stdin.read)
|
|
15
|
-
|
|
16
|
-
inputs = { key: key, meta: payload["_meta"] || {},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
Textus::Protocol::VerbRegistry.for(:put)
|
|
17
|
+
inputs = { key: key, meta: payload["_meta"] || {}, data: payload["data"],
|
|
18
|
+
if_etag: payload["if_etag"] }
|
|
19
|
+
inputs[:format] = as_format if as_format
|
|
20
|
+
s = store.with_role(resolved_role(store))
|
|
21
|
+
emit(s.put(**inputs))
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class WebServe < Verb
|
|
6
|
+
command_name "serve"
|
|
7
|
+
parent_group Group::Web
|
|
8
|
+
option :as_flag, "--as=ROLE"
|
|
9
|
+
option :port, "--port=N"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
role = resolved_role(store, default: Textus::Value::Role::AGENT)
|
|
13
|
+
Textus::Surface::Web.set :textus_session, store.with_role(role)
|
|
14
|
+
Textus::Surface::Web.set :port, (port || 9292).to_i
|
|
15
|
+
Textus::Surface::Web.run!
|
|
16
|
+
0
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/textus/surface/cli.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Textus
|
|
|
40
40
|
# `textus hook list --root=X`). Without this, `order!` below only sees
|
|
41
41
|
# options before the first verb token, so a trailing `--root` reached the
|
|
42
42
|
# verb's own parser and raised InvalidOption (#161 F5). TEXTUS_ROOT already
|
|
43
|
-
# works everywhere via
|
|
43
|
+
# works everywhere via Session, so this brings the flag to parity.
|
|
44
44
|
@root_arg = extract_root!(argv)
|
|
45
45
|
|
|
46
46
|
# Define --version/--help ourselves so OptionParser doesn't intercept them
|
|
@@ -99,7 +99,29 @@ module Textus
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def store
|
|
102
|
-
@store ||=
|
|
102
|
+
@store ||= Textus::Protocol::Session.open(store_root)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def store_root
|
|
106
|
+
@store_root ||= explicit_root || discovered_root
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def explicit_root
|
|
110
|
+
@root_arg || ENV.fetch("TEXTUS_ROOT", nil)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def discovered_root
|
|
114
|
+
dir = File.expand_path(@cwd)
|
|
115
|
+
loop do
|
|
116
|
+
if File.directory?(File.join(dir, ".textus")) && File.exist?(File.join(dir, ".textus", "manifest.yaml"))
|
|
117
|
+
return File.join(dir, ".textus")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
parent = File.dirname(dir)
|
|
121
|
+
raise Textus::IoError.new("no .textus directory found from #{@cwd}") if parent == dir
|
|
122
|
+
|
|
123
|
+
dir = parent
|
|
124
|
+
end
|
|
103
125
|
end
|
|
104
126
|
|
|
105
127
|
def dispatch(klass, argv)
|
|
@@ -131,14 +153,11 @@ module Textus
|
|
|
131
153
|
textus blame KEY [--limit=N]
|
|
132
154
|
textus pulse [--since=N]
|
|
133
155
|
textus boot
|
|
134
|
-
textus doctor
|
|
135
156
|
textus drain [PREFIX] --as=ROLE
|
|
136
157
|
textus watch
|
|
137
|
-
textus jobs
|
|
138
|
-
|
|
139
158
|
textus key {delete,mv,mv-prefix,delete-prefix,uid}
|
|
140
159
|
textus rule {explain,lint,list}
|
|
141
|
-
textus schema {
|
|
160
|
+
textus schema {show}
|
|
142
161
|
textus data {mv}
|
|
143
162
|
textus mcp {serve}
|
|
144
163
|
HELP
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module Dispatch
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def call(session, verb_name, raw_args = nil, **bound)
|
|
7
|
+
if raw_args
|
|
8
|
+
spec = Textus::Protocol::VerbRegistry.for(verb_name.to_sym)
|
|
9
|
+
raise Textus::UsageError.new("unknown verb: #{verb_name}") unless spec
|
|
10
|
+
|
|
11
|
+
bound = Textus::Protocol::Binder.inputs_from_wire(spec, raw_args)
|
|
12
|
+
end
|
|
13
|
+
session.public_send(verb_name.to_sym, **bound)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
module Adapters
|
|
5
|
+
class McpAdapter
|
|
6
|
+
def server(name:, version:, tools:, resources:, server_context:)
|
|
7
|
+
::MCP::Server.new(
|
|
8
|
+
name: name,
|
|
9
|
+
version: version,
|
|
10
|
+
tools: tools,
|
|
11
|
+
resources: resources,
|
|
12
|
+
server_context: server_context,
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tool(name:, description:, input_schema:, &)
|
|
17
|
+
::MCP::Tool.define(
|
|
18
|
+
name: name,
|
|
19
|
+
description: description,
|
|
20
|
+
input_schema: input_schema,
|
|
21
|
+
&
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,89 +1,53 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Surface
|
|
3
3
|
module MCP
|
|
4
|
-
# Derives the entire MCP tool surface from the per-verb contracts (ADR 0039).
|
|
5
|
-
# `build_tools` builds MCP::Tool instances for the SDK; `call` is the generic
|
|
6
|
-
# dispatch: map JSON args -> (positional, keyword) per the contract, invoke
|
|
7
|
-
# the verb through the role scope, then shape the return value. No per-tool code.
|
|
8
4
|
module Catalog
|
|
9
|
-
|
|
5
|
+
MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
|
|
10
6
|
|
|
11
7
|
module_function
|
|
12
8
|
|
|
13
|
-
WRITE_VERBS = %i[
|
|
14
|
-
put propose key_delete key_mv accept reject enqueue
|
|
15
|
-
].freeze
|
|
16
|
-
|
|
17
|
-
MAINTENANCE_VERBS = %i[
|
|
18
|
-
data_mv key_mv_prefix key_delete_prefix drain rule_lint
|
|
19
|
-
].freeze
|
|
20
|
-
|
|
21
|
-
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
22
9
|
def specs
|
|
23
|
-
Textus::
|
|
24
|
-
.select { |k| mcp_surfaced?(k) }
|
|
25
|
-
.map(&:contract)
|
|
10
|
+
Textus::Protocol::VerbRegistry.registered.select(&:mcp?)
|
|
26
11
|
end
|
|
27
12
|
|
|
28
|
-
# Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
|
|
29
13
|
def build_tools(mcp_server)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) do |server_context:, **args|
|
|
40
|
-
mcp_server.dispatch(name, args, server_context)
|
|
41
|
-
end
|
|
14
|
+
specs.map do |spec|
|
|
15
|
+
schema = spec.input_schema
|
|
16
|
+
schema = schema.reject { |k, v| k == :required && Array(v).empty? }
|
|
17
|
+
MCP_ADAPTER.tool(
|
|
18
|
+
name: spec.verb.to_s,
|
|
19
|
+
description: spec.summary,
|
|
20
|
+
input_schema: schema,
|
|
21
|
+
) do |server_context:, **args|
|
|
22
|
+
mcp_server.dispatch(spec.verb, args, server_context)
|
|
42
23
|
end
|
|
24
|
+
end
|
|
43
25
|
end
|
|
44
26
|
|
|
45
27
|
def names
|
|
46
|
-
|
|
47
|
-
Textus::Action::VERBS.select { |_, klass| mcp_surfaced?(klass) },
|
|
48
|
-
)
|
|
28
|
+
specs.map(&:verb).map(&:to_s)
|
|
49
29
|
end
|
|
50
30
|
|
|
51
|
-
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
52
|
-
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
53
|
-
# from this so it can never advertise a verb the agent cannot call, nor
|
|
54
|
-
# omit one it can (ADR 0056). Excludes write/maintenance verbs by verb
|
|
55
|
-
# identity (routing may be legacy UseCases or Dispatch::Actions).
|
|
56
31
|
def read_verbs
|
|
57
|
-
Textus::
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.keys.map(&:to_s)
|
|
32
|
+
Textus::Protocol::VerbRegistry.registered
|
|
33
|
+
.select { |s| s.read? && s.mcp? }
|
|
34
|
+
.map { |s| s.verb.to_s }
|
|
61
35
|
end
|
|
62
36
|
|
|
63
|
-
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
64
|
-
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
65
|
-
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
66
|
-
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
67
|
-
# (ADR 0056, ADR 0057).
|
|
68
37
|
def write_verbs
|
|
69
|
-
Textus::
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def mcp_surfaced?(klass)
|
|
75
|
-
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
38
|
+
Textus::Protocol::VerbRegistry.registered
|
|
39
|
+
.select { |s| s.write? && s.mcp? }
|
|
40
|
+
.map { |s| s.verb.to_s }
|
|
76
41
|
end
|
|
77
42
|
|
|
78
|
-
def call(name,
|
|
79
|
-
|
|
80
|
-
raise ToolError.new("unknown tool: #{name}") unless
|
|
43
|
+
def call(name, store:, args:)
|
|
44
|
+
spec = Textus::Protocol::VerbRegistry.for(name.to_sym)
|
|
45
|
+
raise ToolError.new("unknown tool: #{name}") unless spec&.mcp?
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
rescue Textus::
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
rescue Textus::ContractDrift, CursorExpired
|
|
47
|
+
Textus::Surface::Dispatch.call(store, name.to_sym, args)
|
|
48
|
+
rescue Textus::Protocol::MissingArgs => e
|
|
49
|
+
raise ToolError.new("#{name}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
50
|
+
rescue Textus::ContractDrift, Textus::CursorExpired
|
|
87
51
|
raise
|
|
88
52
|
rescue Textus::Error => e
|
|
89
53
|
raise ToolError.new("#{name}: #{e.message}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
class CursorManager
|
|
5
|
+
def initialize(store:, role:)
|
|
6
|
+
@store = store
|
|
7
|
+
@role = role
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resume!
|
|
11
|
+
saved = @store.container.store.cursor_store.get(@role)[:seq]
|
|
12
|
+
latest = @store.container.store.audit_log.latest[:seq]
|
|
13
|
+
@store.advance_cursor(saved) if saved.positive? && saved <= latest
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def advance!(seq:)
|
|
17
|
+
@store.advance_cursor(seq)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
module DriftAnnotation
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def apply(result, verb_name:, drifted:, is_read:, current_etag:, previous_etag:)
|
|
8
|
+
result["contract_drifted"] = drifted if verb_name == :pulse
|
|
9
|
+
|
|
10
|
+
if drifted && !is_read && verb_name != :boot
|
|
11
|
+
result["_warning"] = "contract drifted (was #{previous_etag}, now #{current_etag}); call boot() to re-arm"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Surface
|
|
3
3
|
module MCP
|
|
4
|
-
# Audit cursor fell off the keep window. Client should re-boot and
|
|
5
|
-
# resume from the new latest_seq.
|
|
6
|
-
class CursorExpired < Textus::Error
|
|
7
|
-
JSONRPC_CODE = -32_002
|
|
8
|
-
|
|
9
|
-
def initialize(message, details: {})
|
|
10
|
-
super("cursor_expired", message, details: details)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
4
|
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
15
5
|
# underlying Textus::Error or generic StandardError.
|
|
16
6
|
class ToolError < Textus::Error
|
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require "mcp"
|
|
4
2
|
|
|
5
3
|
module Textus
|
|
6
4
|
module Surface
|
|
7
5
|
module MCP
|
|
8
|
-
# MCP stdio server backed by the official mcp gem. The SDK owns protocol
|
|
9
|
-
# negotiation, tool dispatch, and JSON-RPC framing. This class owns the
|
|
10
|
-
# textus Session lifecycle (built lazily on first tool call) and delegates
|
|
11
|
-
# execution to Catalog.
|
|
12
6
|
class Server
|
|
7
|
+
CHECKPOINT_INTERVAL = 30
|
|
8
|
+
MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
|
|
9
|
+
|
|
13
10
|
def initialize(store:, role: Textus::Value::Role::DEFAULT, stdin: $stdin, stdout: $stdout)
|
|
14
|
-
@store = store
|
|
15
|
-
@role = role
|
|
11
|
+
@store = store.with_role(role)
|
|
16
12
|
@stdin = stdin
|
|
17
13
|
@stdout = stdout
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@session = Textus::Store::Session.new(
|
|
21
|
-
role: @role,
|
|
22
|
-
cursor: @store.audit_log.latest_seq,
|
|
23
|
-
propose_lane: @store.manifest.policy.propose_lane_for(@role),
|
|
24
|
-
contract_etag: contract_etag_now,
|
|
25
|
-
)
|
|
14
|
+
@shutdown = false
|
|
15
|
+
@cursor_manager = CursorManager.new(store: @store, role:)
|
|
26
16
|
|
|
27
|
-
@
|
|
17
|
+
@store = @cursor_manager.resume! || @store
|
|
18
|
+
|
|
19
|
+
@sdk = MCP_ADAPTER.server(
|
|
28
20
|
name: "textus",
|
|
29
21
|
version: Textus::VERSION,
|
|
30
22
|
tools: Catalog.build_tools(self),
|
|
@@ -34,7 +26,6 @@ module Textus
|
|
|
34
26
|
@sdk.resources_read_handler { |params, server_context:| handle_resource_read(params[:uri].to_s, server_context) }
|
|
35
27
|
end
|
|
36
28
|
|
|
37
|
-
# Runs the stdio line loop; delegates each JSON line to the SDK.
|
|
38
29
|
def run
|
|
39
30
|
@stdin.each_line do |line|
|
|
40
31
|
line = line.strip
|
|
@@ -48,55 +39,63 @@ module Textus
|
|
|
48
39
|
end
|
|
49
40
|
end
|
|
50
41
|
|
|
51
|
-
# Called from every MCP::Tool handler block in Catalog.
|
|
52
|
-
# The SDK parses JSON with symbolize_names: true — all nested keys are symbols.
|
|
53
|
-
# Deep-stringify so Catalog.call receives the string-key format it expects.
|
|
54
42
|
def dispatch(verb_name, args, _server_context)
|
|
55
43
|
str_args = deep_stringify_keys(args)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
drifted = drift_detected?
|
|
45
|
+
|
|
46
|
+
result = Catalog.call(verb_name.to_s, store: @store, args: str_args)
|
|
47
|
+
result = DriftAnnotation.apply(
|
|
48
|
+
result, verb_name:, drifted:, is_read: Catalog.read_verbs.include?(verb_name.to_s),
|
|
49
|
+
current_etag: short_etag(contract_etag_now),
|
|
50
|
+
previous_etag: short_etag(@store.contract_etag)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
post_dispatch!(verb_name)
|
|
59
54
|
::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
|
|
60
|
-
rescue Textus::
|
|
61
|
-
raise_handler_error(e.message, Textus::
|
|
62
|
-
rescue CursorExpired => e
|
|
63
|
-
raise_handler_error(e.message, CursorExpired::JSONRPC_CODE)
|
|
55
|
+
rescue Textus::CursorExpired => e
|
|
56
|
+
raise_handler_error(e.message, Textus::CursorExpired::JSONRPC_CODE)
|
|
64
57
|
rescue Textus::Surface::MCP::ToolError => e
|
|
65
58
|
raise_handler_error(e.message, ToolError::JSONRPC_CODE)
|
|
66
59
|
rescue StandardError => e
|
|
67
|
-
raise_handler_error("internal: #{e.
|
|
60
|
+
raise_handler_error("internal: #{e.message}", -32_603)
|
|
68
61
|
end
|
|
69
62
|
|
|
70
63
|
private
|
|
71
64
|
|
|
72
|
-
def
|
|
73
|
-
@
|
|
74
|
-
|
|
65
|
+
def drift_detected?
|
|
66
|
+
@store.contract_etag != contract_etag_now
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def post_dispatch!(verb_name)
|
|
70
|
+
@store = @cursor_manager.advance!(seq: @store.container.store.audit_log.latest[:seq]) if verb_name == :pulse
|
|
71
|
+
@store = @store.with_role(@store.role) if verb_name == :boot
|
|
75
72
|
end
|
|
76
73
|
|
|
77
74
|
def build_resources
|
|
78
|
-
machine_lane = @store.manifest.policy.machine_lane
|
|
75
|
+
machine_lane = @store.respond_to?(:container) ? @store.container.manifest.policy.machine_lane : nil
|
|
79
76
|
return [] unless machine_lane
|
|
80
77
|
|
|
81
|
-
@store.manifest.data.entries
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
entries = @store.respond_to?(:container) ? @store.container.manifest.data.entries : []
|
|
79
|
+
entries.select { |e| e.lane == machine_lane }
|
|
80
|
+
.map { |e| ::MCP::Resource.new(uri: "textus://#{e.key.tr(".", "/")}", name: e.key, mime_type: mime_for_format(e.format)) }
|
|
84
81
|
end
|
|
85
82
|
|
|
86
83
|
def handle_resource_read(uri, _server_context)
|
|
87
|
-
key
|
|
88
|
-
env = @store.
|
|
89
|
-
text = env
|
|
90
|
-
mime = mime_for_format(
|
|
84
|
+
key = uri.delete_prefix("textus://").tr("/", ".")
|
|
85
|
+
env = @store.get(key:)
|
|
86
|
+
text = env["content"].is_a?(Hash) ? JSON.dump(env["content"]) : (env["body"] || "").to_s
|
|
87
|
+
mime = mime_for_format(env["format"])
|
|
91
88
|
[{ uri: uri, mimeType: mime, text: text }]
|
|
92
89
|
rescue Textus::Error => e
|
|
93
90
|
raise_handler_error("resource read failed: #{e.message}", -32_603)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
raise_handler_error("internal: #{e.message}", -32_603)
|
|
94
93
|
end
|
|
95
94
|
|
|
96
|
-
def contract_etag_now = Textus::Value::Etag.for_contract(@store.
|
|
95
|
+
def contract_etag_now = Textus::Value::Etag.for_contract(@store.layout)
|
|
96
|
+
|
|
97
|
+
def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
|
|
97
98
|
|
|
98
|
-
# The SDK parses JSON with symbolize_names:true, making all nested hash keys symbols.
|
|
99
|
-
# Recursively stringify so Catalog.call receives string-keyed hashes throughout.
|
|
100
99
|
def deep_stringify_keys(obj)
|
|
101
100
|
case obj
|
|
102
101
|
when Hash then obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
|
@@ -107,9 +106,10 @@ module Textus
|
|
|
107
106
|
|
|
108
107
|
def mime_for_format(format)
|
|
109
108
|
case format.to_s
|
|
110
|
-
when "json"
|
|
111
|
-
when "yaml"
|
|
112
|
-
|
|
109
|
+
when "json" then "application/json"
|
|
110
|
+
when "yaml" then "application/yaml"
|
|
111
|
+
when "markdown" then "text/markdown"
|
|
112
|
+
else "text/plain"
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require "securerandom"
|
|
4
2
|
|
|
5
3
|
module Textus
|
|
@@ -7,22 +5,30 @@ module Textus
|
|
|
7
5
|
class Watcher
|
|
8
6
|
def initialize(container:)
|
|
9
7
|
@container = container
|
|
8
|
+
@registry = Textus::Workflow::Registry.new(container.workflows)
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def tick
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
call = Textus::Value::Call.build(role: Textus::Value::Role::AUTOMATION, correlation_id: SecureRandom.uuid)
|
|
13
|
+
|
|
14
|
+
Textus::Workflow::Scheduler.new(
|
|
15
|
+
registry: @registry,
|
|
16
|
+
workflow_queue: @container.store.workflow_queue,
|
|
17
|
+
reader: ->(key:) { @container.store_engine.read(key:) },
|
|
18
|
+
).seed_expired
|
|
19
|
+
|
|
20
|
+
consumer = Textus::Workflow::Consumer.new(registry: @registry, container: @container, call:)
|
|
21
|
+
loop do
|
|
22
|
+
row = @container.store.workflow_queue.pop
|
|
23
|
+
break unless row
|
|
24
|
+
|
|
25
|
+
consumer.consume(row, @container.store.workflow_queue)
|
|
26
|
+
end
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def run(poll: nil)
|
|
24
|
-
interval = poll || @container.manifest.data.worker_config[:poll]
|
|
25
|
-
lock =
|
|
30
|
+
interval = poll || @container.manifest.data.worker_config[:poll] || 2
|
|
31
|
+
lock = Infra::Locks::WatcherLock.new(@container.root)
|
|
26
32
|
lock.acquire
|
|
27
33
|
begin
|
|
28
34
|
loop do
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "sinatra/base"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Surface
|
|
6
|
+
class Web < Sinatra::Base
|
|
7
|
+
set :textus_session, nil
|
|
8
|
+
set :port, 9292
|
|
9
|
+
set :bind, "0.0.0.0"
|
|
10
|
+
set :server, "puma"
|
|
11
|
+
set :show_exceptions, false
|
|
12
|
+
|
|
13
|
+
get "/api/status" do
|
|
14
|
+
content_type :json
|
|
15
|
+
s = settings.textus_session
|
|
16
|
+
boot = s.boot
|
|
17
|
+
JSON.generate(protocol: boot["protocol"], lanes: boot["lanes"]&.map { |l| l["name"] }, store_root: boot["store_root"])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
post "/api/:verb" do
|
|
21
|
+
content_type :json
|
|
22
|
+
session = settings.textus_session.with_role(request.env["HTTP_X_TEXTUS_ROLE"] || "agent")
|
|
23
|
+
body = begin
|
|
24
|
+
JSON.parse(request.body.read)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
result = Textus::Surface::Dispatch.call(session, params[:verb], body)
|
|
29
|
+
JSON.generate(result)
|
|
30
|
+
rescue Textus::Error => e
|
|
31
|
+
halt status_for(e), JSON.generate(error: e.message, code: e.code.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
not_found do
|
|
35
|
+
content_type :json
|
|
36
|
+
JSON.generate(error: "not found")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def status_for(err)
|
|
42
|
+
case err
|
|
43
|
+
when Textus::UnknownKey, Textus::UsageError then 404
|
|
44
|
+
when Textus::EtagMismatch then 409
|
|
45
|
+
else 422
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/textus/value/call.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
module
|
|
3
|
-
# Parses a duration value into whole seconds. Accepts a bare integer (or
|
|
4
|
-
# integer-string) of seconds, or `<n><unit>` with unit s/m/h/d. Returns
|
|
5
|
-
# nil for nil or any unparseable value.
|
|
2
|
+
module Value
|
|
6
3
|
module Duration
|
|
7
4
|
UNIT_SECONDS = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }.freeze
|
|
8
5
|
|
data/lib/textus/value/etag.rb
CHANGED
|
@@ -16,8 +16,9 @@ module Textus
|
|
|
16
16
|
# mid-session edit to any of these makes the cached orientation stale, so
|
|
17
17
|
# the session must re-boot (ADR 0074). The composite is one digest over the
|
|
18
18
|
# sorted per-file listing, so it is order-stable.
|
|
19
|
-
def self.for_contract(
|
|
20
|
-
|
|
19
|
+
def self.for_contract(layout)
|
|
20
|
+
root = layout.root
|
|
21
|
+
listing = contract_files(layout).map do |path|
|
|
21
22
|
rel = path.delete_prefix(root).delete_prefix("/")
|
|
22
23
|
"#{rel}:#{for_file(path)}"
|
|
23
24
|
end.join("\n")
|
|
@@ -26,12 +27,11 @@ module Textus
|
|
|
26
27
|
|
|
27
28
|
# manifest.yaml, then every hook and schema file. Dir.glob already returns
|
|
28
29
|
# sorted paths (Ruby 3.0+), keeping the digest independent of FS order.
|
|
29
|
-
def self.contract_files(
|
|
30
|
-
geom = Textus::Store::Geometry.new(root)
|
|
30
|
+
def self.contract_files(layout)
|
|
31
31
|
[
|
|
32
|
-
|
|
33
|
-
*Dir.glob(File.join(
|
|
34
|
-
*Dir.glob(
|
|
32
|
+
layout.manifest_path,
|
|
33
|
+
*Dir.glob(File.join(layout.hooks_dir, "**", "*.rb")),
|
|
34
|
+
*Dir.glob(layout.schemas_glob).select { |f| File.file?(f) },
|
|
35
35
|
]
|
|
36
36
|
end
|
|
37
37
|
end
|
data/lib/textus/version.rb
CHANGED