textus 0.52.0 → 0.54.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 +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/retention/apply.rb +0 -52
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -2,12 +2,12 @@ require "digest"
|
|
|
2
2
|
require "json"
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
|
-
module
|
|
6
|
-
|
|
5
|
+
module Ports
|
|
6
|
+
class JobStore
|
|
7
7
|
# A unit of deferred work. Pure data. The id is `<type>:<digest>` where the
|
|
8
8
|
# digest is over the args with sorted keys, so logically-identical enqueues
|
|
9
|
-
# collide on the same id — which is how
|
|
10
|
-
#
|
|
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
11
|
class Job
|
|
12
12
|
DIGEST_BYTES = 16
|
|
13
13
|
|
|
@@ -15,10 +15,10 @@ module Textus
|
|
|
15
15
|
attr_accessor :attempts, :last_error
|
|
16
16
|
|
|
17
17
|
def initialize(type:, args:, enqueued_by: nil, attempts: 0, max_attempts: 3, last_error: nil)
|
|
18
|
-
@type
|
|
19
|
-
@args
|
|
18
|
+
@type = type.to_s
|
|
19
|
+
@args = stringify(args)
|
|
20
20
|
@enqueued_by = enqueued_by
|
|
21
|
-
@attempts
|
|
21
|
+
@attempts = attempts
|
|
22
22
|
@max_attempts = max_attempts
|
|
23
23
|
@last_error = last_error
|
|
24
24
|
end
|
|
@@ -29,16 +29,23 @@ module Textus
|
|
|
29
29
|
|
|
30
30
|
def to_h
|
|
31
31
|
{
|
|
32
|
-
"type" => @type,
|
|
33
|
-
"
|
|
32
|
+
"type" => @type,
|
|
33
|
+
"args" => @args,
|
|
34
|
+
"enqueued_by" => @enqueued_by,
|
|
35
|
+
"attempts" => @attempts,
|
|
36
|
+
"max_attempts" => @max_attempts,
|
|
37
|
+
"last_error" => @last_error,
|
|
34
38
|
}
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def self.from_h(hash)
|
|
38
42
|
new(
|
|
39
|
-
type: hash["type"],
|
|
40
|
-
|
|
41
|
-
|
|
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"],
|
|
42
49
|
)
|
|
43
50
|
end
|
|
44
51
|
|
|
@@ -10,7 +10,7 @@ module Textus
|
|
|
10
10
|
# worker pool needs no central lock. Dedup falls out of the id-as-filename:
|
|
11
11
|
# enqueueing an id that already exists is a no-op. ADR 0038 (runtime subtree),
|
|
12
12
|
# ADR 0108 (instantiable port).
|
|
13
|
-
class
|
|
13
|
+
class JobStore
|
|
14
14
|
STATES = %i[ready leased done failed].freeze
|
|
15
15
|
|
|
16
16
|
def initialize(root:)
|
|
@@ -20,7 +20,7 @@ module Textus
|
|
|
20
20
|
|
|
21
21
|
def enqueue(job)
|
|
22
22
|
dest = path(:ready, job.id)
|
|
23
|
-
return if File.exist?(dest)
|
|
23
|
+
return if File.exist?(dest)
|
|
24
24
|
|
|
25
25
|
write_atomic(dest, job.to_h)
|
|
26
26
|
end
|
|
@@ -29,7 +29,6 @@ module Textus
|
|
|
29
29
|
Dir.children(Textus::Layout.queue_state(@root, :ready)).map { |f| File.basename(f, ".json") }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# A claimed job plus the path it lives at, so ack/fail act on this copy.
|
|
33
32
|
Leased = Struct.new(:job, :leased_path, keyword_init: true)
|
|
34
33
|
|
|
35
34
|
def lease(worker_id:, lease_ttl:)
|
|
@@ -38,11 +37,11 @@ module Textus
|
|
|
38
37
|
src = File.join(ready_dir, name)
|
|
39
38
|
dst = File.join(Textus::Layout.queue_state(@root, :leased), name)
|
|
40
39
|
begin
|
|
41
|
-
File.rename(src, dst)
|
|
40
|
+
File.rename(src, dst)
|
|
42
41
|
rescue Errno::ENOENT
|
|
43
|
-
next
|
|
42
|
+
next
|
|
44
43
|
end
|
|
45
|
-
job =
|
|
44
|
+
job = Job.from_h(JSON.parse(File.read(dst)))
|
|
46
45
|
stamp_lease(dst, worker_id: worker_id, expires_at: Time.now.utc + lease_ttl)
|
|
47
46
|
return Leased.new(job: job, leased_path: dst)
|
|
48
47
|
end
|
|
@@ -54,9 +53,6 @@ module Textus
|
|
|
54
53
|
File.rename(leased.leased_path, dest)
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
# Increment attempts and either requeue (transient) or dead-letter (attempts
|
|
58
|
-
# exhausted). Returns :requeued or :dead_lettered so the worker can count
|
|
59
|
-
# terminal failures distinctly from retries.
|
|
60
56
|
def fail(leased, error:)
|
|
61
57
|
job = leased.job
|
|
62
58
|
job.attempts += 1
|
|
@@ -67,9 +63,6 @@ module Textus
|
|
|
67
63
|
dead ? :dead_lettered : :requeued
|
|
68
64
|
end
|
|
69
65
|
|
|
70
|
-
# Return expired leases to ready/ (the holding worker crashed). Returns the
|
|
71
|
-
# count reclaimed. At-least-once delivery: a job whose handler actually
|
|
72
|
-
# finished but whose ack was lost will re-run — handlers must be idempotent.
|
|
73
66
|
def reclaim(now:)
|
|
74
67
|
leased_dir = Textus::Layout.queue_state(@root, :leased)
|
|
75
68
|
count = 0
|
|
@@ -85,7 +78,7 @@ module Textus
|
|
|
85
78
|
File.rename(src, dst)
|
|
86
79
|
count += 1
|
|
87
80
|
rescue Errno::ENOENT
|
|
88
|
-
next
|
|
81
|
+
next
|
|
89
82
|
end
|
|
90
83
|
count
|
|
91
84
|
end
|
|
@@ -123,7 +116,7 @@ module Textus
|
|
|
123
116
|
def write_atomic(dest, hash)
|
|
124
117
|
tmp = "#{dest}.#{Process.pid}.tmp"
|
|
125
118
|
File.write(tmp, JSON.pretty_generate(hash))
|
|
126
|
-
File.rename(tmp, dest)
|
|
119
|
+
File.rename(tmp, dest)
|
|
127
120
|
end
|
|
128
121
|
end
|
|
129
122
|
end
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
# Persistence adapter for sentinel files. Owns the on-disk JSON shape, the
|
|
8
8
|
# path layout (<store_root>/.run/sentinels/<target-rel-to-repo>.textus-managed.json
|
|
9
9
|
# — runtime, git-ignored, ADR 0070), and all File/FileUtils I/O.
|
|
10
|
-
#
|
|
10
|
+
# Core::Sentinel is a pure value object that depends on this port for
|
|
11
11
|
# reads and writes.
|
|
12
12
|
class SentinelStore
|
|
13
13
|
SUFFIX = ".textus-managed.json".freeze
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
|
|
27
27
|
def load(path, repo_root)
|
|
28
28
|
raw = JSON.parse(File.read(path))
|
|
29
|
-
Textus::
|
|
29
|
+
Textus::Core::Sentinel.new(
|
|
30
30
|
target: absolutize(raw["target"], repo_root),
|
|
31
31
|
source: absolutize(raw["source"], repo_root),
|
|
32
32
|
sha256: raw["sha256"],
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Ports
|
|
7
|
+
# Flock-based watcher presence lock. Held for the watcher's lifetime.
|
|
8
|
+
# Process death releases the flock automatically.
|
|
9
|
+
class WatcherLock
|
|
10
|
+
def initialize(root)
|
|
11
|
+
@path = Textus::Layout.watcher_lock(root)
|
|
12
|
+
@file = nil
|
|
13
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.running?(root)
|
|
17
|
+
path = Textus::Layout.watcher_lock(root)
|
|
18
|
+
return false unless File.exist?(path)
|
|
19
|
+
|
|
20
|
+
File.open(path, "r+") do |file|
|
|
21
|
+
got = file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
22
|
+
file.flock(File::LOCK_UN) if got
|
|
23
|
+
return !got
|
|
24
|
+
end
|
|
25
|
+
rescue Errno::ENOENT
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def acquire
|
|
30
|
+
@file = File.open(@path, File::RDWR | File::CREAT, 0o644)
|
|
31
|
+
raise "watcher already running" unless @file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
32
|
+
|
|
33
|
+
@file.write("pid=#{Process.pid} host=#{Socket.gethostname}\n")
|
|
34
|
+
@file.flush
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def release
|
|
39
|
+
return unless @file
|
|
40
|
+
|
|
41
|
+
@file.flock(File::LOCK_UN)
|
|
42
|
+
@file.close
|
|
43
|
+
FileUtils.rm_f(@path)
|
|
44
|
+
@file = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,94 +1,49 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Produce
|
|
3
|
-
# The single convergence engine (ADR 0093/0094). "Make these machine entries
|
|
4
|
-
# current from upstream." Acquire is per-`from`; publish is one uniform
|
|
5
|
-
# `publish_via` entry point for all kinds (ADR 0094):
|
|
6
|
-
# intake (from: handler) -> re-pull (Produce::Acquire::Intake), then publish_via
|
|
7
|
-
# derived (from: project) -> build data + publish_via (ToPaths or None)
|
|
8
|
-
# derived (from: command) -> skip the build; publish_via publishes
|
|
9
|
-
# existing store bytes via mode resolution
|
|
10
|
-
# (None when no targets -> skipped)
|
|
11
|
-
# Runs as the converge build actor (self-elevating); the passed `call`
|
|
12
|
-
# supplies only correlation_id/dry_run. Callers choose the key set: the
|
|
13
|
-
# write subscriber passes rdeps ∩ derived; the converge pass passes
|
|
14
|
-
# all-derived + stale-intake.
|
|
15
3
|
class Engine
|
|
16
|
-
# Locked + failure-isolated convergence — the entry point worker handlers
|
|
17
|
-
# call to materialize a key set (ADR 0093 / job-queue model). A held lock
|
|
18
|
-
# is a soft miss (an in-flight build/converge already produces fresh
|
|
19
|
-
# output); any other error is republished as :produce_failed and never
|
|
20
|
-
# raised at the caller (ADR 0087 §5 failure isolation, preserved).
|
|
21
4
|
def self.converge(container:, call:, keys:)
|
|
22
|
-
|
|
23
|
-
new(container: container, call: call).call(keys: keys)
|
|
24
|
-
end
|
|
25
|
-
rescue Textus::BuildInProgress
|
|
26
|
-
nil
|
|
27
|
-
rescue Textus::Error => e
|
|
28
|
-
container.events.publish(
|
|
29
|
-
:produce_failed,
|
|
30
|
-
ctx: Textus::Hooks::Context.for(container: container, call: call),
|
|
31
|
-
keys: keys, error: e.message
|
|
32
|
-
)
|
|
5
|
+
new(container:, call:).run(keys)
|
|
33
6
|
end
|
|
34
7
|
|
|
35
8
|
def initialize(container:, call:)
|
|
36
9
|
@container = container
|
|
37
10
|
@call = call
|
|
38
|
-
@manifest = container.manifest
|
|
39
11
|
end
|
|
40
12
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
context = build_context(build_call)
|
|
46
|
-
out = { produced: [], skipped: [], failed: [] }
|
|
47
|
-
|
|
48
|
-
keys.each do |key|
|
|
49
|
-
produce_one(key, build_call, context, out)
|
|
50
|
-
rescue Textus::Error => e
|
|
51
|
-
out[:failed] << { "key" => key, "error" => e.message }
|
|
52
|
-
end
|
|
53
|
-
out
|
|
13
|
+
def run(keys)
|
|
14
|
+
results = { completed: [], failed: [] }
|
|
15
|
+
Array(keys).each { |key| produce_one(key, results) }
|
|
16
|
+
results
|
|
54
17
|
end
|
|
55
18
|
|
|
56
|
-
|
|
19
|
+
alias call run
|
|
57
20
|
|
|
58
|
-
|
|
59
|
-
# for every kind. The command emit-vs-skip falls out of publish-mode
|
|
60
|
-
# resolution (Publish::None when no targets), so there is no command branch.
|
|
61
|
-
def produce_one(key, build_call, context, out)
|
|
62
|
-
entry = @manifest.resolver.resolve(key).entry
|
|
21
|
+
private
|
|
63
22
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
23
|
+
def produce_one(key, results)
|
|
24
|
+
workflow = @container.workflows.for(key)
|
|
25
|
+
if workflow
|
|
26
|
+
Workflow::Runner.new(workflow, container: @container, call: @call).run(key)
|
|
68
27
|
else
|
|
69
|
-
|
|
70
|
-
result.nil? ? (out[:skipped] << key) : (out[:produced] << key)
|
|
28
|
+
publish_only(key)
|
|
71
29
|
end
|
|
30
|
+
results[:completed] << key
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
results[:failed] << { key: key, error: e.message }
|
|
72
33
|
end
|
|
73
34
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
role: build_role,
|
|
82
|
-
correlation_id: @call.correlation_id,
|
|
83
|
-
dry_run: @call.dry_run,
|
|
84
|
-
)
|
|
85
|
-
end
|
|
35
|
+
def publish_only(key)
|
|
36
|
+
entry = @container.manifest.resolver.resolve(key).entry
|
|
37
|
+
return unless entry.publish_tree || !Array(entry.publish_to).empty?
|
|
38
|
+
|
|
39
|
+
reader = Textus::Envelope::Reader.from(container: @container)
|
|
40
|
+
entry_path = @container.manifest.resolver.resolve(key).path
|
|
41
|
+
return unless entry.publish_tree || File.exist?(entry_path)
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
container: @container, call: call,
|
|
90
|
-
reader: Textus::Read::Get.new(container: @container, call: call)
|
|
43
|
+
pctx = Textus::Manifest::Entry::Base::PublishContext.new(
|
|
44
|
+
container: @container, call: @call, reader: reader.method(:read),
|
|
91
45
|
)
|
|
46
|
+
entry.publish_via(pctx)
|
|
92
47
|
end
|
|
93
48
|
end
|
|
94
49
|
end
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
require "mustache"
|
|
2
|
+
|
|
1
3
|
module Textus
|
|
2
4
|
module Produce
|
|
3
|
-
# Renders an entry's stored DATA into the bytes for one publish target
|
|
4
|
-
# (ADR 0094). Relocates the Mustache logic that used to live in the
|
|
5
|
-
# build-time Markdown renderer. Provenance is NOT added here — it lives in
|
|
6
|
-
# the data's `_meta`; a template surfaces it if the output should show it.
|
|
7
|
-
# A verbatim target (no template) is the caller's job to copy.
|
|
8
5
|
class Render
|
|
9
6
|
def initialize(template_loader:)
|
|
10
7
|
@template_loader = template_loader
|
|
11
8
|
end
|
|
12
9
|
|
|
13
|
-
# target: a rendering Policy::PublishTarget. data: parsed entry data.
|
|
14
|
-
# boot: boot context hash or nil. Returns the rendered String.
|
|
15
10
|
def bytes_for(target:, data:, boot:)
|
|
16
11
|
raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
|
|
17
12
|
|
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -84,10 +84,11 @@ module Textus
|
|
|
84
84
|
# Orchestrator-free read: schema tooling must never trigger a fetch
|
|
85
85
|
# while inspecting/migrating entries (ADR 0062).
|
|
86
86
|
def self.pure_get(store, role, key)
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
scope = store.as(role)
|
|
88
|
+
Textus::Action::Get.new(key: key).call(
|
|
89
|
+
container: scope.container,
|
|
89
90
|
call: Textus::Call.build(role: role),
|
|
90
|
-
)
|
|
91
|
+
)
|
|
91
92
|
end
|
|
92
93
|
|
|
93
94
|
def self.load_schema(store, name)
|
data/lib/textus/session.rb
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
# The agent session: per-connection (MCP), per-process (CLI), or per-loop
|
|
3
3
|
# (Ruby) orientation state — the audit cursor plus the contract etag and
|
|
4
|
-
#
|
|
4
|
+
# propose_lane captured at boot. Immutable Data value; advance_cursor
|
|
5
5
|
# returns a new instance. ADR 0036; contract_etag widened in ADR 0074.
|
|
6
|
-
Session = Data.define(:role, :cursor, :
|
|
6
|
+
Session = Data.define(:role, :cursor, :propose_lane, :contract_etag) do
|
|
7
|
+
# Back-compat reader while lane terminology migrates.
|
|
8
|
+
def propose_zone = propose_lane
|
|
9
|
+
|
|
7
10
|
def advance_cursor(new_cursor) = with(cursor: new_cursor)
|
|
8
11
|
|
|
9
12
|
def check_etag!(observed_etag)
|
|
10
13
|
return if observed_etag == contract_etag
|
|
11
14
|
|
|
12
|
-
raise Textus::
|
|
15
|
+
raise Textus::ContractDrift.new(
|
|
13
16
|
"contract changed (manifest/hooks/schemas were #{short_etag(contract_etag)}, " \
|
|
14
17
|
"now #{short_etag(observed_etag)}); re-run boot",
|
|
15
18
|
)
|
data/lib/textus/store.rb
CHANGED
|
@@ -44,8 +44,6 @@ module Textus
|
|
|
44
44
|
|
|
45
45
|
def initialize(root)
|
|
46
46
|
@container = build_container(File.expand_path(root))
|
|
47
|
-
bootstrap_hooks
|
|
48
|
-
events.publish(:store_loaded, ctx: Hooks::Context.new(scope: as(Role::DEFAULT)))
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
# Build an agent Session oriented at the current cursor/manifest — the
|
|
@@ -54,26 +52,24 @@ module Textus
|
|
|
54
52
|
Textus::Session.new(
|
|
55
53
|
role: role,
|
|
56
54
|
cursor: audit_log.latest_seq,
|
|
57
|
-
|
|
55
|
+
propose_lane: manifest.policy.propose_lane_for(role),
|
|
58
56
|
contract_etag: Textus::Etag.for_contract(root),
|
|
59
57
|
)
|
|
60
58
|
end
|
|
61
59
|
|
|
62
|
-
def
|
|
63
|
-
|
|
60
|
+
def gate
|
|
61
|
+
@container.gate
|
|
64
62
|
end
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
as(role).public_send(verb, *args, **kwargs)
|
|
69
|
-
end
|
|
64
|
+
def as(role, dry_run: false, correlation_id: nil)
|
|
65
|
+
Textus::Surfaces::RoleScope.new(container: container, role: role, dry_run: dry_run, correlation_id: correlation_id)
|
|
70
66
|
end
|
|
71
67
|
|
|
72
68
|
private
|
|
73
69
|
|
|
74
70
|
def build_container(root)
|
|
75
71
|
manifest = Manifest.load(root)
|
|
76
|
-
Container.new(
|
|
72
|
+
container = Container.new(
|
|
77
73
|
root: root,
|
|
78
74
|
manifest: manifest,
|
|
79
75
|
schemas: Schemas.new(File.join(root, "schemas")),
|
|
@@ -83,16 +79,13 @@ module Textus
|
|
|
83
79
|
max_size: manifest.data.audit_config[:max_size],
|
|
84
80
|
keep: manifest.data.audit_config[:keep],
|
|
85
81
|
),
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
workflows: Workflow::Loader.load_all(root),
|
|
83
|
+
gate: nil,
|
|
88
84
|
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Ports::ProduceOnWriteSubscriber.new(container).attach(events)
|
|
94
|
-
Hooks::Builtin.register_all(events: events, rpc: rpc)
|
|
95
|
-
Hooks::Loader.new(events: events, rpc: rpc).load_dir(File.join(root, "hooks"))
|
|
85
|
+
gate = Textus::Gate.new(container)
|
|
86
|
+
container = container.with(gate: gate)
|
|
87
|
+
gate.instance_variable_set(:@container, container)
|
|
88
|
+
container
|
|
96
89
|
end
|
|
97
90
|
end
|
|
98
91
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
class CLI
|
|
4
|
+
class Group < Verb
|
|
5
|
+
class << self
|
|
6
|
+
# Subcommands are auto-derived: any Verb descendant whose
|
|
7
|
+
# `parent_group` is this group counts as a subcommand. Sorted
|
|
8
|
+
# alphabetically by command_name for stable help output.
|
|
9
|
+
def subcommands
|
|
10
|
+
Textus::Surfaces::CLI::Runner.install!
|
|
11
|
+
Verb.descendants
|
|
12
|
+
.select { |k| k.parent_group == self && k.command_name }
|
|
13
|
+
.sort_by(&:command_name)
|
|
14
|
+
.to_h { |k| [k.command_name, k] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def needs_store?
|
|
18
|
+
# Delegate to the matched subcommand at parse time; default true.
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse(argv)
|
|
24
|
+
subs = self.class.subcommands
|
|
25
|
+
subname = argv.shift
|
|
26
|
+
if subname.nil?
|
|
27
|
+
raise UsageError.new(
|
|
28
|
+
"#{self.class.command_name} requires a subcommand: #{subs.keys.join(", ")}",
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@sub_klass = subs[subname]
|
|
33
|
+
unless @sub_klass
|
|
34
|
+
raise UsageError.new(
|
|
35
|
+
"unknown #{self.class.command_name} subcommand '#{subname}'. " \
|
|
36
|
+
"Valid: #{subs.keys.join(", ")}",
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
41
|
+
@sub.parse(argv)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(store)
|
|
45
|
+
@sub.call(@sub_klass.needs_store? ? store : nil)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|