textus 0.54.2 → 0.55.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
data/lib/textus/layout.rb
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Single source of truth for every path textus owns under a store root.
|
|
3
|
-
# All disposable runtime state nests under <root>/.state/ so the
|
|
4
|
-
# tracked/disposable boundary is a directory boundary. ADR 0038.
|
|
5
|
-
module Layout
|
|
6
|
-
RUN = ".state"
|
|
7
|
-
DATA = "data"
|
|
8
|
-
|
|
9
|
-
def self.data(root)
|
|
10
|
-
File.join(root, DATA)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.data_lane(root, lane_name)
|
|
14
|
-
File.join(data(root), lane_name)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.run(root)
|
|
18
|
-
File.join(root, RUN)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.cursors(root)
|
|
22
|
-
File.join(run(root), "cursors")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.cursor(root, role)
|
|
26
|
-
File.join(cursors(root), role.to_s)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def self.locks(root)
|
|
30
|
-
File.join(run(root), "locks")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.build_lock(root)
|
|
34
|
-
File.join(run(root), "build.lock")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.watcher_lock(root)
|
|
38
|
-
File.join(run(root), "watcher.lock")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.queue(root)
|
|
42
|
-
File.join(run(root), "queue")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.queue_state(root, state)
|
|
46
|
-
File.join(queue(root), state.to_s)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.audit_dir(root)
|
|
50
|
-
File.join(run(root), "audit")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Sentinels are machine-generated (the published target's sha), not authored
|
|
54
|
-
# source, so they live on the runtime side under `.state/` — git-ignored,
|
|
55
|
-
# regenerated by the next build via content-identical adoption (ADR 0070,
|
|
56
|
-
# superseding ADR 0038's `:config` classification).
|
|
57
|
-
def self.sentinels(root)
|
|
58
|
-
File.join(run(root), "sentinels")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def self.indexes(root)
|
|
62
|
-
File.join(run(root), "indexes")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.raw_index(root)
|
|
66
|
-
File.join(indexes(root), "raw.yaml")
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def self.audit_log(root)
|
|
70
|
-
File.join(audit_dir(root), "audit.log")
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# The store's `.gitignore` body. Always ignores the runtime subtree
|
|
74
|
-
# (`.state/`, ADR 0038); when given untracked entry paths (entries marked
|
|
75
|
-
# `tracked: false`), it also lists those so they stay protocol-readable but
|
|
76
|
-
# uncommitted (ADR 0043, refining 0038). Generated, never hand-kept — no
|
|
77
|
-
# drift between the manifest and the ignore file.
|
|
78
|
-
def self.gitignore_body(untracked_paths: [])
|
|
79
|
-
lines = ["# textus runtime artifacts — safe to delete, never commit",
|
|
80
|
-
"#{RUN}/"]
|
|
81
|
-
unless untracked_paths.empty?
|
|
82
|
-
lines << "# tracked:false entries — protocol-readable, not committed (sensitive)"
|
|
83
|
-
lines.concat(untracked_paths)
|
|
84
|
-
end
|
|
85
|
-
"#{lines.join("\n")}\n"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Back-compat constant: the no-untracked-entries body (just the run subtree).
|
|
89
|
-
GITIGNORE = gitignore_body
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
require "digest"
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Ports
|
|
6
|
-
class JobStore
|
|
7
|
-
# A unit of deferred work. Pure data. The id is `<type>:<digest>` where the
|
|
8
|
-
# digest is over the args with sorted keys, so logically-identical enqueues
|
|
9
|
-
# collide on the same id — which is how Queue dedups (file already exists).
|
|
10
|
-
# At-least-once delivery means handlers must be idempotent.
|
|
11
|
-
class Job
|
|
12
|
-
DIGEST_BYTES = 16
|
|
13
|
-
|
|
14
|
-
attr_reader :type, :args, :enqueued_by, :max_attempts
|
|
15
|
-
attr_accessor :attempts, :last_error
|
|
16
|
-
|
|
17
|
-
def initialize(type:, args:, enqueued_by: nil, attempts: 0, max_attempts: 3, last_error: nil)
|
|
18
|
-
@type = type.to_s
|
|
19
|
-
@args = stringify(args)
|
|
20
|
-
@enqueued_by = enqueued_by
|
|
21
|
-
@attempts = attempts
|
|
22
|
-
@max_attempts = max_attempts
|
|
23
|
-
@last_error = last_error
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def id
|
|
27
|
-
"#{@type}:#{digest}"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def to_h
|
|
31
|
-
{
|
|
32
|
-
"type" => @type,
|
|
33
|
-
"args" => @args,
|
|
34
|
-
"enqueued_by" => @enqueued_by,
|
|
35
|
-
"attempts" => @attempts,
|
|
36
|
-
"max_attempts" => @max_attempts,
|
|
37
|
-
"last_error" => @last_error,
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.from_h(hash)
|
|
42
|
-
new(
|
|
43
|
-
type: hash["type"],
|
|
44
|
-
args: hash["args"] || {},
|
|
45
|
-
enqueued_by: hash["enqueued_by"],
|
|
46
|
-
attempts: hash["attempts"] || 0,
|
|
47
|
-
max_attempts: hash["max_attempts"] || 3,
|
|
48
|
-
last_error: hash["last_error"],
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def digest
|
|
55
|
-
canonical = JSON.dump(@args.sort.to_h)
|
|
56
|
-
Digest::SHA256.hexdigest(canonical)[0, DIGEST_BYTES]
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def stringify(hash)
|
|
60
|
-
hash.transform_keys(&:to_s)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "json"
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Ports
|
|
7
|
-
# File-backed durable job queue under `<root>/.run/queue/`. Each job state
|
|
8
|
-
# is a directory; a job is one `<id>.json` file. Claiming is an atomic
|
|
9
|
-
# `rename(2)` from ready/ to leased/ — the rename winner owns the job, so a
|
|
10
|
-
# worker pool needs no central lock. Dedup falls out of the id-as-filename:
|
|
11
|
-
# enqueueing an id that already exists is a no-op. ADR 0038 (runtime subtree),
|
|
12
|
-
# ADR 0108 (instantiable port).
|
|
13
|
-
class JobStore
|
|
14
|
-
STATES = %i[ready leased done failed].freeze
|
|
15
|
-
|
|
16
|
-
def initialize(root:)
|
|
17
|
-
@root = root
|
|
18
|
-
STATES.each { |s| FileUtils.mkdir_p(Textus::Layout.queue_state(root, s)) }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def enqueue(job)
|
|
22
|
-
dest = path(:ready, job.id)
|
|
23
|
-
return if File.exist?(dest)
|
|
24
|
-
|
|
25
|
-
write_atomic(dest, job.to_h)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def ready_ids
|
|
29
|
-
Dir.children(Textus::Layout.queue_state(@root, :ready)).map { |f| File.basename(f, ".json") }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
Leased = Struct.new(:job, :leased_path, keyword_init: true)
|
|
33
|
-
|
|
34
|
-
def lease(worker_id:, lease_ttl:)
|
|
35
|
-
ready_dir = Textus::Layout.queue_state(@root, :ready)
|
|
36
|
-
Dir.children(ready_dir).each do |name|
|
|
37
|
-
src = File.join(ready_dir, name)
|
|
38
|
-
dst = File.join(Textus::Layout.queue_state(@root, :leased), name)
|
|
39
|
-
begin
|
|
40
|
-
File.rename(src, dst)
|
|
41
|
-
rescue Errno::ENOENT
|
|
42
|
-
next
|
|
43
|
-
end
|
|
44
|
-
job = Job.from_h(JSON.parse(File.read(dst)))
|
|
45
|
-
stamp_lease(dst, worker_id: worker_id, expires_at: Time.now.utc + lease_ttl)
|
|
46
|
-
return Leased.new(job: job, leased_path: dst)
|
|
47
|
-
end
|
|
48
|
-
nil
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def ack(leased)
|
|
52
|
-
dest = File.join(Textus::Layout.queue_state(@root, :done), File.basename(leased.leased_path))
|
|
53
|
-
File.rename(leased.leased_path, dest)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def fail(leased, error:)
|
|
57
|
-
job = leased.job
|
|
58
|
-
job.attempts += 1
|
|
59
|
-
job.last_error = error
|
|
60
|
-
dead = job.attempts >= job.max_attempts
|
|
61
|
-
write_atomic(path(dead ? :failed : :ready, job.id), job.to_h)
|
|
62
|
-
File.delete(leased.leased_path)
|
|
63
|
-
dead ? :dead_lettered : :requeued
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def reclaim(now:)
|
|
67
|
-
leased_dir = Textus::Layout.queue_state(@root, :leased)
|
|
68
|
-
count = 0
|
|
69
|
-
Dir.children(leased_dir).each do |name|
|
|
70
|
-
src = File.join(leased_dir, name)
|
|
71
|
-
data = JSON.parse(File.read(src))
|
|
72
|
-
expires = data.dig("lease", "expires_at")
|
|
73
|
-
next if expires && Time.parse(expires) > now
|
|
74
|
-
|
|
75
|
-
dst = File.join(Textus::Layout.queue_state(@root, :ready), name)
|
|
76
|
-
data.delete("lease")
|
|
77
|
-
File.write(src, JSON.pretty_generate(data))
|
|
78
|
-
File.rename(src, dst)
|
|
79
|
-
count += 1
|
|
80
|
-
rescue Errno::ENOENT
|
|
81
|
-
next
|
|
82
|
-
end
|
|
83
|
-
count
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def list(state)
|
|
87
|
-
Dir.children(Textus::Layout.queue_state(@root, state.to_sym)).map { |f| File.basename(f, ".json") }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def retry_failed(job_id)
|
|
91
|
-
src = path(:failed, job_id)
|
|
92
|
-
data = JSON.parse(File.read(src))
|
|
93
|
-
data["attempts"] = 0
|
|
94
|
-
data["last_error"] = nil
|
|
95
|
-
write_atomic(path(:ready, job_id), data)
|
|
96
|
-
File.delete(src)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def purge(state)
|
|
100
|
-
dir = Textus::Layout.queue_state(@root, state.to_sym)
|
|
101
|
-
Dir.children(dir).each { |f| File.delete(File.join(dir, f)) }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def stamp_lease(leased_path, worker_id:, expires_at:)
|
|
107
|
-
data = JSON.parse(File.read(leased_path))
|
|
108
|
-
data["lease"] = { "worker_id" => worker_id, "expires_at" => expires_at.iso8601 }
|
|
109
|
-
File.write(leased_path, JSON.pretty_generate(data))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def path(state, job_id)
|
|
113
|
-
File.join(Textus::Layout.queue_state(@root, state), "#{job_id}.json")
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def write_atomic(dest, hash)
|
|
117
|
-
tmp = "#{dest}.#{Process.pid}.tmp"
|
|
118
|
-
File.write(tmp, JSON.pretty_generate(hash))
|
|
119
|
-
File.rename(tmp, dest)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Ports
|
|
8
|
-
# Content-addressable index for the raw lane. Maps content hashes and URLs
|
|
9
|
-
# to their current canonical key. Lives under <root>/.state/indexes/raw.yaml
|
|
10
|
-
# (gitignored, regenerable — truth is in the on-disk raw entries).
|
|
11
|
-
class RawIndex
|
|
12
|
-
def initialize(root:)
|
|
13
|
-
@root = root
|
|
14
|
-
@path = Layout.raw_index(root)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
attr_reader :path
|
|
18
|
-
|
|
19
|
-
def load
|
|
20
|
-
return empty_index unless File.exist?(@path)
|
|
21
|
-
|
|
22
|
-
YAML.safe_load_file(@path) || empty_index
|
|
23
|
-
rescue StandardError
|
|
24
|
-
empty_index
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def save(index)
|
|
28
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
29
|
-
File.write(@path, YAML.dump(index))
|
|
30
|
-
index
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def find_by_hash(content_hash)
|
|
34
|
-
index = load
|
|
35
|
-
index["hashes"]&.fetch(content_hash, nil)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def find_by_url(url)
|
|
39
|
-
return nil unless url
|
|
40
|
-
|
|
41
|
-
index = load
|
|
42
|
-
index["urls"]&.fetch(url, nil)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def upsert(content_hash:, url:, key:)
|
|
46
|
-
index = load
|
|
47
|
-
index["hashes"] ||= {}
|
|
48
|
-
index["urls"] ||= {}
|
|
49
|
-
index["hashes"][content_hash] = key
|
|
50
|
-
index["urls"][url] = key if url
|
|
51
|
-
save(index)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def empty_index
|
|
57
|
-
{ "hashes" => {}, "urls" => {} }
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Ports
|
|
5
|
-
module Storage
|
|
6
|
-
# Pure filesystem I/O port. Wraps File/FileUtils/Etag with no knowledge
|
|
7
|
-
# of envelopes, entries, schemas, or audit.
|
|
8
|
-
class FileStore
|
|
9
|
-
def read(path) = File.binread(path)
|
|
10
|
-
|
|
11
|
-
def write(path, bytes)
|
|
12
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
13
|
-
File.binwrite(path, bytes)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Raises Errno::ENOENT if absent — mirrors File.delete and matches the
|
|
17
|
-
# semantics used by Store::Writer (which guards with File.exist? first).
|
|
18
|
-
def delete(path) = File.delete(path)
|
|
19
|
-
|
|
20
|
-
def exists?(path) = File.exist?(path)
|
|
21
|
-
|
|
22
|
-
def etag(path) = Etag.for_file(path)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
data/lib/textus/role.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Role
|
|
3
|
-
# The three role archetypes, each string sourced exactly once: human curates
|
|
4
|
-
# canon, agent proposes, automation converges the machine-maintained lanes
|
|
5
|
-
# (refresh + materialize) (explanation/concepts.md).
|
|
6
|
-
# Reference these constants instead of bare literals (ADR 0044).
|
|
7
|
-
HUMAN = "human".freeze
|
|
8
|
-
AGENT = "agent".freeze
|
|
9
|
-
AUTOMATION = "automation".freeze
|
|
10
|
-
|
|
11
|
-
# The closed set of legal role names (ADR 0045), built FROM the archetypes
|
|
12
|
-
# above so it stays the single source of truth — a manifest declaring any
|
|
13
|
-
# other name is rejected at load, and DEFAULT ∈ NAMES holds structurally.
|
|
14
|
-
# Capabilities (`can:`) remain freely tunable per role.
|
|
15
|
-
NAMES = [HUMAN, AGENT, AUTOMATION].freeze
|
|
16
|
-
|
|
17
|
-
# Default acting identity (ADR 0040): a *choice* over the vocabulary, not a
|
|
18
|
-
# new name. CLI callers act as the human; an agent over stdio proposes and
|
|
19
|
-
# does not inherit the human's authority (it defaults to AGENT per transport).
|
|
20
|
-
DEFAULT = HUMAN
|
|
21
|
-
|
|
22
|
-
def self.resolve(root:, flag: nil, env: ENV, default: DEFAULT)
|
|
23
|
-
candidate = flag || env["TEXTUS_ROLE"] || read_file(root) || default
|
|
24
|
-
raise InvalidRole.new(candidate) unless NAMES.include?(candidate)
|
|
25
|
-
|
|
26
|
-
candidate
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def self.read_file(root)
|
|
30
|
-
path = File.join(root, "role")
|
|
31
|
-
return nil unless File.exist?(path)
|
|
32
|
-
|
|
33
|
-
File.read(path).strip.then { |s| s.empty? ? nil : s }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
data/lib/textus/schemas.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Eager-loading schema cache. Loads every *.yaml under +dir+ at construction.
|
|
3
|
-
# A missing directory is treated as "no schemas" (does not raise) to mirror
|
|
4
|
-
# the lazy behavior previously embedded in Store#schema_for.
|
|
5
|
-
class Schemas
|
|
6
|
-
def initialize(dir)
|
|
7
|
-
@dir = dir
|
|
8
|
-
@schemas = {}
|
|
9
|
-
load_all
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def fetch(name)
|
|
13
|
-
@schemas[name] || raise(IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Only nil short-circuits. A missing-but-named schema still raises IoError.
|
|
17
|
-
def fetch_or_nil(name)
|
|
18
|
-
return nil if name.nil?
|
|
19
|
-
|
|
20
|
-
fetch(name)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def all
|
|
24
|
-
@schemas.values
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Name-keyed view: { canonical_name => Schema }. The key is the schema's
|
|
28
|
-
# file stem, which is authoritative even when a schema file carries no
|
|
29
|
-
# top-level `name:` (Schema#name reads the body and may be nil). Symmetric
|
|
30
|
-
# with #all (values); use this when you need the names too.
|
|
31
|
-
def by_name
|
|
32
|
-
@schemas.dup
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def load_all
|
|
38
|
-
return unless File.directory?(@dir)
|
|
39
|
-
|
|
40
|
-
Dir.glob(File.join(@dir, "*.yaml")).each do |path|
|
|
41
|
-
name = File.basename(path, ".yaml")
|
|
42
|
-
begin
|
|
43
|
-
@schemas[name] = Schema.load(path)
|
|
44
|
-
rescue StandardError
|
|
45
|
-
# Tolerate broken schema files at construction time so the rest of
|
|
46
|
-
# the store remains loadable. Surfacing the failure is the job of
|
|
47
|
-
# Doctor::Check::SchemaParseError. Lookups via #fetch still raise
|
|
48
|
-
# IoError for the missing-but-named schema.
|
|
49
|
-
next
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
data/lib/textus/session.rb
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "dry-struct"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
# The agent session: per-connection (MCP), per-process (CLI), or per-loop
|
|
7
|
-
# (Ruby) orientation state — the audit cursor plus the contract etag and
|
|
8
|
-
# propose_lane captured at boot. Immutable Dry::Struct::Value; advance_cursor
|
|
9
|
-
# and with return new instances. ADR 0036; contract_etag widened in ADR 0074.
|
|
10
|
-
class Session < Dry::Struct
|
|
11
|
-
attribute :role, Types::RoleName
|
|
12
|
-
attribute :cursor, Types::Cursor
|
|
13
|
-
attribute :propose_lane, Types::String.optional
|
|
14
|
-
attribute :contract_etag, Types::String
|
|
15
|
-
|
|
16
|
-
def with(**attrs) = self.class.new(to_h.merge(attrs))
|
|
17
|
-
|
|
18
|
-
def advance_cursor(new_cursor) = with(cursor: new_cursor)
|
|
19
|
-
|
|
20
|
-
def check_etag!(observed_etag)
|
|
21
|
-
return if observed_etag == contract_etag
|
|
22
|
-
|
|
23
|
-
raise Textus::ContractDrift.new(
|
|
24
|
-
"contract changed (manifest/hooks/schemas were #{short_etag(contract_etag)}, " \
|
|
25
|
-
"now #{short_etag(observed_etag)}); re-run boot",
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
# First 8 hex chars after the "sha256:" prefix — a stable short id for
|
|
32
|
-
# the drift diagnostic.
|
|
33
|
-
def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Surfaces
|
|
3
|
-
class CLI
|
|
4
|
-
class Verb
|
|
5
|
-
class Put < Runner::Base
|
|
6
|
-
self.spec = Textus::Action::Put.contract
|
|
7
|
-
option :as_flag, "--as=ROLE"
|
|
8
|
-
option :use_stdin, "--stdin"
|
|
9
|
-
|
|
10
|
-
def invoke(store)
|
|
11
|
-
key = positional.shift or raise UsageError.new("put requires a key")
|
|
12
|
-
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
13
|
-
|
|
14
|
-
payload = JSON.parse(@stdin.read)
|
|
15
|
-
cmd = Textus::Command::Put.new(
|
|
16
|
-
key: key,
|
|
17
|
-
meta: payload["_meta"] || {},
|
|
18
|
-
body: payload["body"] || "",
|
|
19
|
-
content: nil,
|
|
20
|
-
if_etag: payload["if_etag"],
|
|
21
|
-
role: resolved_role(store),
|
|
22
|
-
)
|
|
23
|
-
result = store.gate.dispatch(cmd)
|
|
24
|
-
emit(result.to_h_for_wire)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Surfaces
|
|
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
|
-
module Catalog
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
WRITE_VERBS = %i[
|
|
12
|
-
put propose key_delete key_mv accept reject enqueue
|
|
13
|
-
].freeze
|
|
14
|
-
|
|
15
|
-
MAINTENANCE_VERBS = %i[
|
|
16
|
-
data_mv key_mv_prefix key_delete_prefix drain rule_lint
|
|
17
|
-
].freeze
|
|
18
|
-
|
|
19
|
-
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
20
|
-
def specs
|
|
21
|
-
Textus::Action::VERBS.values
|
|
22
|
-
.select { |k| mcp_surfaced?(k) }
|
|
23
|
-
.map(&:contract)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
|
|
27
|
-
def build_tools(mcp_server)
|
|
28
|
-
Textus::Action::VERBS
|
|
29
|
-
.select { |_, klass| mcp_surfaced?(klass) }
|
|
30
|
-
.map do |name, action|
|
|
31
|
-
schema = action.contract.input_schema
|
|
32
|
-
schema = schema.reject { |k, v| k == :required && Array(v).empty? }
|
|
33
|
-
::MCP::Tool.define(
|
|
34
|
-
name: name.to_s,
|
|
35
|
-
description: action.contract.summary,
|
|
36
|
-
input_schema: schema,
|
|
37
|
-
) do |server_context:, **args|
|
|
38
|
-
mcp_server.dispatch(name, args, server_context)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def names
|
|
44
|
-
Textus::Action::VERBS
|
|
45
|
-
.select { |_, klass| mcp_surfaced?(klass) }
|
|
46
|
-
.keys.map(&:to_s)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
50
|
-
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
51
|
-
# from this so it can never advertise a verb the agent cannot call, nor
|
|
52
|
-
# omit one it can (ADR 0056). Excludes write/maintenance verbs by verb
|
|
53
|
-
# identity (routing may be legacy UseCases or Dispatch::Actions).
|
|
54
|
-
def read_verbs
|
|
55
|
-
Textus::Action::VERBS
|
|
56
|
-
.reject { |verb, _klass| WRITE_VERBS.include?(verb) || MAINTENANCE_VERBS.include?(verb) }
|
|
57
|
-
.select { |_verb, klass| mcp_surfaced?(klass) }
|
|
58
|
-
.keys.map(&:to_s)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
62
|
-
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
63
|
-
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
64
|
-
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
65
|
-
# (ADR 0056, ADR 0057).
|
|
66
|
-
def write_verbs
|
|
67
|
-
Textus::Action::VERBS
|
|
68
|
-
.select { |verb, klass| WRITE_VERBS.include?(verb) && mcp_surfaced?(klass) }
|
|
69
|
-
.keys.map(&:to_s)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def mcp_surfaced?(klass)
|
|
73
|
-
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def call(name, session:, store:, args:) # rubocop:disable Metrics/AbcSize
|
|
77
|
-
klass = Textus::Action::VERBS[name.to_sym]
|
|
78
|
-
raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
|
|
79
|
-
|
|
80
|
-
spec = klass.contract
|
|
81
|
-
inputs = Textus::Contract::Binder.inputs_from_wire(spec, args)
|
|
82
|
-
|
|
83
|
-
invoke = lambda do |effective_inputs|
|
|
84
|
-
pos, kwargs = Textus::Contract::Binder.bind(spec, effective_inputs, session: session)
|
|
85
|
-
spec.args.select(&:positional).zip(pos).each { |a, v| kwargs[a.name] = v unless kwargs.key?(a.name) }
|
|
86
|
-
cmd_class = Textus::Gate::VERB_COMMAND.fetch(spec.verb) do
|
|
87
|
-
raise Textus::MCP::ToolError.new("unknown verb: #{spec.verb}")
|
|
88
|
-
end
|
|
89
|
-
merged = kwargs.merge(role: session.role)
|
|
90
|
-
filled = cmd_class.members.to_h { |m| [m, merged.key?(m) ? merged[m] : nil] }
|
|
91
|
-
cmd = cmd_class.new(**filled)
|
|
92
|
-
store.gate.dispatch(cmd)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
result = if spec.around
|
|
96
|
-
Textus::Contract::Around.with(spec.around, scope: store.as(session.role), inputs: inputs, session: session, &invoke)
|
|
97
|
-
else
|
|
98
|
-
invoke.call(inputs)
|
|
99
|
-
end
|
|
100
|
-
Textus::Contract::View.render(spec, :default, result, inputs)
|
|
101
|
-
rescue Textus::Contract::MissingArgs => e
|
|
102
|
-
raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
103
|
-
rescue Textus::ContractDrift, CursorExpired
|
|
104
|
-
raise
|
|
105
|
-
rescue Textus::Error => e
|
|
106
|
-
raise ToolError.new("#{name}: #{e.message}")
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Surfaces
|
|
5
|
-
# Role-scoped identity carrier. Holds the acting identity (role,
|
|
6
|
-
# correlation_id, dry_run) bound to a container. All verb methods
|
|
7
|
-
# (put, get, accept, ...) are injected by textus.rb's define_method
|
|
8
|
-
# loop, which dispatches directly through Gate.
|
|
9
|
-
class RoleScope
|
|
10
|
-
attr_reader :container, :role, :correlation_id
|
|
11
|
-
|
|
12
|
-
def initialize(container:, role:, dry_run: false, correlation_id: nil)
|
|
13
|
-
@container = container
|
|
14
|
-
@role = role.to_s
|
|
15
|
-
@dry_run = dry_run
|
|
16
|
-
@correlation_id = correlation_id || SecureRandom.uuid
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def dry_run? = !!@dry_run
|
|
20
|
-
|
|
21
|
-
def with_role(role)
|
|
22
|
-
self.class.new(container: @container, role:, dry_run: @dry_run, correlation_id: @correlation_id)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def with_correlation_id(cid)
|
|
26
|
-
self.class.new(container: @container, role: @role, dry_run: @dry_run, correlation_id: cid)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def with_dry_run
|
|
30
|
-
self.class.new(container: @container, role: @role, dry_run: true, correlation_id: @correlation_id)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
data/lib/textus/types.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "dry/types"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Types
|
|
7
|
-
include Dry.Types()
|
|
8
|
-
|
|
9
|
-
RoleName = Types::String.constrained(included_in: Textus::Role::NAMES)
|
|
10
|
-
Cursor = Types::Integer.constrained(gteq: 0)
|
|
11
|
-
FormatName = Types::String.constrained(
|
|
12
|
-
included_in: %w[markdown json yaml text], # must match Format::STRATEGIES.keys
|
|
13
|
-
)
|
|
14
|
-
end
|
|
15
|
-
end
|