textus 0.55.2 → 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 +8 -9
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +0 -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/{dispatch → protocol}/binder.rb +1 -7
- 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/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
- 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 → protocol}/layout.rb +16 -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 +22 -21
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +2 -7
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +7 -7
- 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 +12 -12
- 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/server.rb +35 -24
- 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/value/etag.rb +7 -7
- 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 -53
- 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 -16
- metadata +169 -185
- data/SPEC.md +0 -909
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -141
- data/lib/textus/boot.rb +0 -181
- data/lib/textus/dispatch/contracts.rb +0 -63
- data/lib/textus/dispatch/handler_registry.rb +0 -21
- data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
- data/lib/textus/dispatch/middleware/auth.rb +0 -40
- data/lib/textus/dispatch/middleware/base.rb +0 -26
- data/lib/textus/dispatch/middleware/binder.rb +0 -20
- data/lib/textus/dispatch/middleware/cascade.rb +0 -53
- data/lib/textus/dispatch/pipeline.rb +0 -35
- 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/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/scratchpad_sources.rb +0 -55
- 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 -43
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -77
- data/lib/textus/format/json.rb +0 -81
- data/lib/textus/format/markdown.rb +0 -59
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -104
- data/lib/textus/format.rb +0 -61
- data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
- data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
- data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
- data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
- data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
- data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
- data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
- data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
- data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
- data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
- data/lib/textus/handlers/read/audit_entries.rb +0 -48
- data/lib/textus/handlers/read/blame_entry.rb +0 -71
- data/lib/textus/handlers/read/deps_entry.rb +0 -17
- data/lib/textus/handlers/read/get_entry.rb +0 -68
- data/lib/textus/handlers/read/list_keys.rb +0 -36
- data/lib/textus/handlers/read/pulse_entries.rb +0 -66
- data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
- data/lib/textus/handlers/read/uid_entry.rb +0 -18
- data/lib/textus/handlers/read/where_entry.rb +0 -18
- data/lib/textus/handlers/write/accept_proposal.rb +0 -39
- data/lib/textus/handlers/write/data_mv.rb +0 -55
- data/lib/textus/handlers/write/delete_key.rb +0 -17
- data/lib/textus/handlers/write/enqueue_job.rb +0 -27
- data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
- data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
- data/lib/textus/handlers/write/move_key.rb +0 -80
- data/lib/textus/handlers/write/propose_entry.rb +0 -29
- data/lib/textus/handlers/write/put_entry.rb +0 -29
- data/lib/textus/handlers/write/reject_proposal.rb +0 -29
- 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/predicates/author_held.rb +0 -22
- data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
- data/lib/textus/manifest/policy/predicates.rb +0 -54
- 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/orchestration.rb +0 -55
- 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.rb +0 -25
- 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 -49
- data/lib/textus/port/storage/interface.rb +0 -17
- data/lib/textus/port/store.rb +0 -149
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -40
- data/lib/textus/produce/publisher.rb +0 -21
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/registry.rb +0 -42
- data/lib/textus/schema/tools.rb +0 -108
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/store/container.rb +0 -173
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/entry/reader.rb +0 -47
- data/lib/textus/store/entry/writer.rb +0 -219
- data/lib/textus/store/envelope/meta.rb +0 -61
- data/lib/textus/store/freshness/drift_detector.rb +0 -93
- data/lib/textus/store/freshness/evaluator.rb +0 -20
- data/lib/textus/store/freshness/verdict.rb +0 -24
- data/lib/textus/store/freshness.rb +0 -8
- data/lib/textus/store/index/builder.rb +0 -76
- 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 -124
- data/lib/textus/store/jobs/queue.rb +0 -162
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention/base.rb +0 -52
- data/lib/textus/store/jobs/retention/sweep.rb +0 -55
- data/lib/textus/store/jobs/retention.rb +0 -8
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store.rb +0 -122
- data/lib/textus/surface/cli/verb/doctor.rb +0 -20
- 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/projector.rb +0 -20
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -90
- data/lib/textus/value/result.rb +0 -46
- data/lib/textus/verb_registry.rb +0 -417
- 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
|
@@ -1,21 +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
11
|
@store = store.with_role(role)
|
|
15
12
|
@stdin = stdin
|
|
16
13
|
@stdout = stdout
|
|
14
|
+
@shutdown = false
|
|
15
|
+
@cursor_manager = CursorManager.new(store: @store, role:)
|
|
17
16
|
|
|
18
|
-
@
|
|
17
|
+
@store = @cursor_manager.resume! || @store
|
|
18
|
+
|
|
19
|
+
@sdk = MCP_ADAPTER.server(
|
|
19
20
|
name: "textus",
|
|
20
21
|
version: Textus::VERSION,
|
|
21
22
|
tools: Catalog.build_tools(self),
|
|
@@ -40,13 +41,17 @@ module Textus
|
|
|
40
41
|
|
|
41
42
|
def dispatch(verb_name, args, _server_context)
|
|
42
43
|
str_args = deep_stringify_keys(args)
|
|
43
|
-
|
|
44
|
+
drifted = drift_detected?
|
|
45
|
+
|
|
44
46
|
result = Catalog.call(verb_name.to_s, store: @store, args: str_args)
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
47
54
|
::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
|
|
48
|
-
rescue Textus::ContractDrift => e
|
|
49
|
-
raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
|
|
50
55
|
rescue Textus::CursorExpired => e
|
|
51
56
|
raise_handler_error(e.message, Textus::CursorExpired::JSONRPC_CODE)
|
|
52
57
|
rescue Textus::Surface::MCP::ToolError => e
|
|
@@ -57,23 +62,29 @@ module Textus
|
|
|
57
62
|
|
|
58
63
|
private
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
def build_resources
|
|
64
|
-
machine_lane = @store.manifest.policy.machine_lane
|
|
75
|
+
machine_lane = @store.respond_to?(:container) ? @store.container.manifest.policy.machine_lane : nil
|
|
65
76
|
return [] unless machine_lane
|
|
66
77
|
|
|
67
|
-
@store.manifest.data.entries
|
|
68
|
-
|
|
69
|
-
|
|
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)) }
|
|
70
81
|
end
|
|
71
82
|
|
|
72
83
|
def handle_resource_read(uri, _server_context)
|
|
73
84
|
key = uri.delete_prefix("textus://").tr("/", ".")
|
|
74
85
|
env = @store.get(key:)
|
|
75
|
-
text = env
|
|
76
|
-
mime = mime_for_format(env
|
|
86
|
+
text = env["content"].is_a?(Hash) ? JSON.dump(env["content"]) : (env["body"] || "").to_s
|
|
87
|
+
mime = mime_for_format(env["format"])
|
|
77
88
|
[{ uri: uri, mimeType: mime, text: text }]
|
|
78
89
|
rescue Textus::Error => e
|
|
79
90
|
raise_handler_error("resource read failed: #{e.message}", -32_603)
|
|
@@ -81,10 +92,10 @@ module Textus
|
|
|
81
92
|
raise_handler_error("internal: #{e.message}", -32_603)
|
|
82
93
|
end
|
|
83
94
|
|
|
84
|
-
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]
|
|
85
98
|
|
|
86
|
-
# The SDK parses JSON with symbolize_names:true, making all nested hash keys symbols.
|
|
87
|
-
# Recursively stringify so Catalog.call receives string-keyed hashes throughout.
|
|
88
99
|
def deep_stringify_keys(obj)
|
|
89
100
|
case obj
|
|
90
101
|
when Hash then obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
|
@@ -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
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::Layout.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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
class Consumer
|
|
4
|
+
BACKOFF_DELAYS = [10, 30, 60].freeze
|
|
5
|
+
|
|
6
|
+
def initialize(registry:, container:, call:)
|
|
7
|
+
@registry = registry
|
|
8
|
+
@container = container
|
|
9
|
+
@call = call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def consume(row, workflow_queue)
|
|
13
|
+
workflow_queue.mark_processing(row[:id])
|
|
14
|
+
workflow = @registry.for(row[:workflow_type])
|
|
15
|
+
Runner.new(workflow, container: @container, call: @call).run(row[:key])
|
|
16
|
+
workflow_queue.complete(row[:id])
|
|
17
|
+
row.merge(state: "done")
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
handle_failure(row, workflow_queue, e)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def handle_failure(row, workflow_queue, error)
|
|
25
|
+
retry_count = (row[:retry_count] || 0) + 1
|
|
26
|
+
max_attempts = row[:max_attempts] || 3
|
|
27
|
+
|
|
28
|
+
if retry_count >= max_attempts
|
|
29
|
+
workflow_queue.fail(row[:id], retry_count:, error: error.message)
|
|
30
|
+
row.merge(state: "failed", error: error.message)
|
|
31
|
+
else
|
|
32
|
+
index = [retry_count - 1, BACKOFF_DELAYS.length - 1].min
|
|
33
|
+
workflow_queue.reschedule(row[:id], retry_count:, delay: BACKOFF_DELAYS[index], error: error.message)
|
|
34
|
+
row.merge(state: "retrying")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
module DSL
|
|
4
|
+
module Lifecycle
|
|
5
|
+
VALID_EXPIRE_ACTIONS = %i[drop archive].freeze
|
|
6
|
+
|
|
7
|
+
def ttl_value(cadence)
|
|
8
|
+
return nil if cadence == :__no_arg__
|
|
9
|
+
|
|
10
|
+
Textus::Value::Duration.seconds(cadence)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def expire_action(action = :__no_arg__)
|
|
14
|
+
return nil if action == :__no_arg__
|
|
15
|
+
|
|
16
|
+
sym = action.to_s.to_sym
|
|
17
|
+
raise ArgumentError.new("on_expire must be one of drop|archive, got #{action.inspect}") unless VALID_EXPIRE_ACTIONS.include?(sym)
|
|
18
|
+
|
|
19
|
+
sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def max_attempts_value(n)
|
|
23
|
+
n.to_i
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def priority_value(n)
|
|
27
|
+
n.to_i
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
module DSL
|
|
4
|
+
module PatternMatcher
|
|
5
|
+
def match_pattern?(pattern, key)
|
|
6
|
+
if pattern.end_with?(".**")
|
|
7
|
+
prefix = pattern.delete_suffix(".**")
|
|
8
|
+
key.start_with?("#{prefix}.")
|
|
9
|
+
elsif pattern.end_with?(".*")
|
|
10
|
+
prefix = pattern.delete_suffix(".*")
|
|
11
|
+
suffix = key.delete_prefix("#{prefix}.")
|
|
12
|
+
key != suffix && !suffix.include?(".")
|
|
13
|
+
else
|
|
14
|
+
key == pattern
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
module DSL
|
|
4
|
+
module StepBuilder
|
|
5
|
+
def build_step(name, callable = nil, timeout: nil, &block)
|
|
6
|
+
if callable.is_a?(Hash)
|
|
7
|
+
raise ArgumentError.new(
|
|
8
|
+
"step :#{name} no longer accepts options hash; pass timeout: keyword instead",
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
resolved = if callable.respond_to?(:call)
|
|
13
|
+
callable
|
|
14
|
+
elsif block
|
|
15
|
+
block
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError.new("step :#{name} requires a block or a callable (got neither)")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Step.new(name: name, callable: resolved, timeout: timeout)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_check(name, naming: nil, scope: nil, &block)
|
|
24
|
+
merge = lambda { |data, new_issues|
|
|
25
|
+
existing = Array(data&.dig("content", "issues"))
|
|
26
|
+
all = existing + new_issues
|
|
27
|
+
{ "content" => { "ok" => all.empty?, "issues" => all, "count" => all.size } }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if naming
|
|
31
|
+
raise ArgumentError.new("check :#{name} requires a scope: for naming checks") unless scope
|
|
32
|
+
|
|
33
|
+
lambda { |data, ctx|
|
|
34
|
+
entries = ctx.container.manifest.resolver.enumerate.select do |row|
|
|
35
|
+
match_pattern?(scope, row[:key].to_s)
|
|
36
|
+
end
|
|
37
|
+
new_issues = check_naming(
|
|
38
|
+
entries, pattern: naming, code: "check.#{name}",
|
|
39
|
+
fix: "rename to match #{naming.inspect}"
|
|
40
|
+
)
|
|
41
|
+
merge.call(data, new_issues)
|
|
42
|
+
}
|
|
43
|
+
elsif block
|
|
44
|
+
->(data, ctx) { merge.call(data, block.call(ctx) || []) }
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError.new("check :#{name} requires naming: or a block")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_parallel(steps)
|
|
51
|
+
Parallel.new(steps: steps)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/textus/workflow/dsl.rb
CHANGED
|
@@ -2,41 +2,131 @@ module Textus
|
|
|
2
2
|
module Workflow
|
|
3
3
|
module DSL
|
|
4
4
|
Step = Data.define(:name, :callable, :timeout)
|
|
5
|
+
Parallel = Data.define(:steps)
|
|
6
|
+
PublishTarget = Data.define(:to, :template, :tree, :inject_boot)
|
|
5
7
|
|
|
6
8
|
class Definition
|
|
7
|
-
|
|
9
|
+
include StepBuilder
|
|
10
|
+
include PatternMatcher
|
|
11
|
+
include Lifecycle
|
|
12
|
+
|
|
13
|
+
attr_reader :name, :steps, :on_patterns, :save_to, :notify_keys,
|
|
14
|
+
:publish_block, :publish_targets,
|
|
15
|
+
:handled_events, :workflow_type
|
|
16
|
+
|
|
17
|
+
def self.register(defn)
|
|
18
|
+
@definitions ||= []
|
|
19
|
+
@definitions << defn
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.definitions
|
|
23
|
+
@definitions || []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.clear_registered
|
|
27
|
+
@definitions = []
|
|
28
|
+
end
|
|
8
29
|
|
|
9
30
|
def initialize(name)
|
|
10
|
-
@name
|
|
11
|
-
@steps
|
|
12
|
-
@
|
|
13
|
-
@
|
|
31
|
+
@name = name
|
|
32
|
+
@steps = []
|
|
33
|
+
@on_patterns = []
|
|
34
|
+
@save_to = nil
|
|
35
|
+
@notify_keys = []
|
|
36
|
+
@publish_block = nil
|
|
37
|
+
@publish_targets = []
|
|
38
|
+
@priority = 0
|
|
39
|
+
@handled_events = []
|
|
40
|
+
@max_attempts = 3
|
|
41
|
+
@workflow_type = name
|
|
14
42
|
end
|
|
15
43
|
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
# ── Event handling (internal) ──
|
|
45
|
+
def handles(*event_types)
|
|
46
|
+
@handled_events.concat(event_types.map(&:to_s).freeze)
|
|
18
47
|
end
|
|
19
48
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
callable_or_opt
|
|
23
|
-
elsif block
|
|
24
|
-
block
|
|
25
|
-
else
|
|
26
|
-
raise ArgumentError.new("step :#{name} requires a block or a callable (got neither)")
|
|
27
|
-
end
|
|
28
|
-
t = callable_or_opt.is_a?(Hash) ? callable_or_opt[:timeout] : timeout
|
|
29
|
-
@steps << Step.new(name: name, callable: callable, timeout: t)
|
|
49
|
+
def type
|
|
50
|
+
@workflow_type
|
|
30
51
|
end
|
|
31
52
|
|
|
32
|
-
def
|
|
33
|
-
@
|
|
53
|
+
def max_attempts(n = nil)
|
|
54
|
+
return @max_attempts if n.nil?
|
|
55
|
+
|
|
56
|
+
@max_attempts = max_attempts_value(n)
|
|
34
57
|
end
|
|
35
58
|
|
|
59
|
+
# ── Trigger ──
|
|
60
|
+
def on(*patterns)
|
|
61
|
+
@on_patterns = patterns
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ── Steps ──
|
|
65
|
+
def step(name, callable = nil, timeout: nil, &)
|
|
66
|
+
@steps << build_step(name, callable, timeout:, &)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check(name, naming: nil, scope: nil, &)
|
|
70
|
+
callable = build_check(name, naming:, scope:, &)
|
|
71
|
+
@steps << Step.new(name: :"check_#{name}", callable: callable, timeout: nil)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parallel(&)
|
|
75
|
+
saved = @steps
|
|
76
|
+
@steps = []
|
|
77
|
+
instance_eval(&)
|
|
78
|
+
parallel_steps = @steps.dup
|
|
79
|
+
@steps = saved << build_parallel(parallel_steps)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ── Output ──
|
|
83
|
+
def publish(to: nil, template: nil, tree: nil, inject_boot: nil, &block)
|
|
84
|
+
if block
|
|
85
|
+
@publish_block = block
|
|
86
|
+
elsif to || tree
|
|
87
|
+
@publish_targets << PublishTarget.new(to: to, template: template, tree: tree, inject_boot: inject_boot)
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError.new("publish requires to: (file), tree:, or a block; use save to: for store keys")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def save(to:)
|
|
94
|
+
@save_to = to
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def notify(key)
|
|
98
|
+
@notify_keys << key
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ── Ordering ──
|
|
102
|
+
def priority(n = nil)
|
|
103
|
+
return @priority if n.nil?
|
|
104
|
+
|
|
105
|
+
@priority = priority_value(n)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ── Lifecycle ──
|
|
109
|
+
def every(cadence = :__no_arg__)
|
|
110
|
+
return @ttl if cadence == :__no_arg__
|
|
111
|
+
|
|
112
|
+
@ttl = ttl_value(cadence)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
alias ttl every
|
|
116
|
+
|
|
117
|
+
def on_expire(action = :__no_arg__)
|
|
118
|
+
@on_expire = expire_action(action)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ── Matching (internal) ──
|
|
36
122
|
def match?(key)
|
|
37
|
-
return false unless @
|
|
123
|
+
return false unless @on_patterns&.any?
|
|
124
|
+
|
|
125
|
+
@on_patterns.any? { |p| match_pattern?(p, key) }
|
|
126
|
+
end
|
|
38
127
|
|
|
39
|
-
|
|
128
|
+
def multi_match?
|
|
129
|
+
@on_patterns&.any? { |p| p.end_with?("**") }
|
|
40
130
|
end
|
|
41
131
|
end
|
|
42
132
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
class EventEmitter
|
|
4
|
+
def initialize(workflow_queue, registry:)
|
|
5
|
+
@workflow_queue = workflow_queue
|
|
6
|
+
@registry = registry
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def emit(event_type:, key:, lane: "artifacts")
|
|
10
|
+
workflows = @registry.workflows_for(event_type)
|
|
11
|
+
workflows.each do |wf|
|
|
12
|
+
@workflow_queue.push(
|
|
13
|
+
event_type: event_type.to_s, workflow_type: wf.type,
|
|
14
|
+
key:, lane:, max_attempts: wf.max_attempts
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
workflows.size
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Workflow
|
|
3
|
+
class Publisher
|
|
4
|
+
def initialize(container:, call:)
|
|
5
|
+
@container = container
|
|
6
|
+
@call = call
|
|
7
|
+
@store_engine = container.store_engine
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def publish(key, data, ctx, definition)
|
|
11
|
+
definition.publish_targets.each { |t| publish_one(key, data, ctx, target: t) }
|
|
12
|
+
|
|
13
|
+
write_to_key(definition.save_to, data, ctx) if definition.save_to
|
|
14
|
+
|
|
15
|
+
definition.notify_keys.each { |nk| notify_key(nk, ctx) }
|
|
16
|
+
|
|
17
|
+
definition.publish_block&.call(data, ctx)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def write_to_key(target_key, data, ctx) # rubocop:disable Lint/UnusedMethodArgument
|
|
23
|
+
res = @container.manifest.resolver.resolve(target_key)
|
|
24
|
+
normalized = Textus::Protocol::Format.data_to_payload(data, res.entry.format)
|
|
25
|
+
meta = stamped_meta(normalized[:meta])
|
|
26
|
+
@store_engine.put(key: target_key, meta:, body: normalized[:body], content: normalized[:content], call: @call)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def notify_key(target_key, ctx) # rubocop:disable Lint/UnusedMethodArgument
|
|
30
|
+
@store_engine.put(key: target_key, meta: {}, body: nil, content: {}, call: @call)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def write_and_pctx(key, data, ctx)
|
|
34
|
+
normalized = Textus::Protocol::Format.data_to_payload(data, ctx.entry.format)
|
|
35
|
+
meta = stamped_meta(normalized[:meta])
|
|
36
|
+
@store_engine.put(key:, meta:, body: normalized[:body], content: normalized[:content], call: @call)
|
|
37
|
+
Textus::Protocol::Manifest::Entry::Base::PublishContext.new(
|
|
38
|
+
container: @container,
|
|
39
|
+
reader: ->(key:) { @store_engine.read(key:) },
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def publish_one(key, data, ctx, target:)
|
|
44
|
+
pctx = write_and_pctx(key, data, ctx)
|
|
45
|
+
attrs = { "to" => target.to, "template" => target.template,
|
|
46
|
+
"tree" => target.tree }
|
|
47
|
+
attrs["inject_boot"] = true if target.inject_boot
|
|
48
|
+
pub_target = Textus::Protocol::Manifest::Policy::PublishTarget.new(attrs)
|
|
49
|
+
entry = ctx.entry.with_publish_targets([pub_target])
|
|
50
|
+
entry.publish_via(pctx)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stamped_meta(meta)
|
|
54
|
+
now = Time.now.utc.iso8601
|
|
55
|
+
(meta || {}).merge("generated_at" => now)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|