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
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Domain
|
|
7
|
-
module Policy
|
|
8
|
-
module Predicates
|
|
9
|
-
# Parameterized predicate: the entry must have been written within
|
|
10
|
-
# `duration` of now. Duration strings ("1h", "30m", "7d") parse via
|
|
11
|
-
# Domain::Duration.seconds. Passes when no envelope exists yet.
|
|
12
|
-
class FreshWithin
|
|
13
|
-
attr_reader :reason
|
|
14
|
-
|
|
15
|
-
def initialize(duration:, now: nil)
|
|
16
|
-
@seconds = Textus::Domain::Duration.seconds(duration)
|
|
17
|
-
@now = now
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def name = "fresh_within"
|
|
21
|
-
|
|
22
|
-
def call(eval)
|
|
23
|
-
return true if eval.envelope.nil? || @seconds.nil?
|
|
24
|
-
|
|
25
|
-
written = written_at(eval.envelope)
|
|
26
|
-
return true if written.nil?
|
|
27
|
-
|
|
28
|
-
now = @now || Textus::Ports::Clock.new.now
|
|
29
|
-
return true if now - written <= @seconds
|
|
30
|
-
|
|
31
|
-
@reason = "entry older than #{@seconds}s (written #{written.iso8601})"
|
|
32
|
-
false
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
# Domain-pure: reads the stored write timestamp from the envelope's
|
|
38
|
-
# freshness (checked_at) or meta (last_fetched_at) and parses the
|
|
39
|
-
# stored ISO-8601 string. Parsing a stored string is not I/O (allowed
|
|
40
|
-
# in domain, ADR 0024). `generated_at` is intentionally NOT consulted:
|
|
41
|
-
# build-generation time is no longer carried in the artifact (ADR
|
|
42
|
-
# 0070), and fetch-freshness is a fetch concept, not a build one.
|
|
43
|
-
def written_at(envelope)
|
|
44
|
-
raw = envelope.freshness&.checked_at ||
|
|
45
|
-
envelope.meta&.dig("last_fetched_at")
|
|
46
|
-
return raw if raw.is_a?(Time)
|
|
47
|
-
return nil if raw.nil?
|
|
48
|
-
|
|
49
|
-
begin
|
|
50
|
-
Time.parse(raw.to_s)
|
|
51
|
-
rescue StandardError
|
|
52
|
-
nil
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# The single source of truth for the predicate vocabulary
|
|
8
|
-
# (ADR 0031 §3). Replaces both Promote::KNOWN and Promotion::REGISTRY.
|
|
9
|
-
# Each entry is name => ->(params:, schemas:) { predicate }.
|
|
10
|
-
module Registry
|
|
11
|
-
ENTRIES = {
|
|
12
|
-
"zone_writable_by" => ->(**) { ZoneWritableBy.new },
|
|
13
|
-
"author_held" => ->(**) { AuthorHeld.new },
|
|
14
|
-
"target_is_canon" => ->(**) { TargetIsCanon.new },
|
|
15
|
-
"schema_valid" => ->(schemas:, **) { SchemaValid.new(schemas: schemas) },
|
|
16
|
-
"etag_match" => ->(params:, **) { EtagMatch.new(if_etag: params) },
|
|
17
|
-
"fresh_within" => ->(params:, **) { FreshWithin.new(duration: params) },
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
# Accepts either "name" or { "name" => params }.
|
|
21
|
-
def self.build(spec, schemas:)
|
|
22
|
-
name, params =
|
|
23
|
-
if spec.is_a?(Hash)
|
|
24
|
-
spec.first
|
|
25
|
-
else
|
|
26
|
-
[spec.to_s, nil]
|
|
27
|
-
end
|
|
28
|
-
ctor = ENTRIES[name.to_s] or raise Textus::UsageError.new(
|
|
29
|
-
"unknown guard predicate: '#{name}' (known: #{ENTRIES.keys.join(", ")})",
|
|
30
|
-
)
|
|
31
|
-
ctor.call(params: params, schemas: schemas)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.known = ENTRIES.keys
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# Predicate: the entry's effective frontmatter satisfies the schema
|
|
8
|
-
# bound to the target key. For accept, the frontmatter lives under
|
|
9
|
-
# envelope.meta["frontmatter"]; for a direct put it is envelope.meta.
|
|
10
|
-
class SchemaValid
|
|
11
|
-
attr_reader :reason
|
|
12
|
-
|
|
13
|
-
def initialize(schemas:)
|
|
14
|
-
@schemas = schemas
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def name = "schema_valid"
|
|
18
|
-
|
|
19
|
-
def call(eval)
|
|
20
|
-
manifest = eval.manifest
|
|
21
|
-
return true if eval.envelope.nil? || manifest.nil? || @schemas.nil?
|
|
22
|
-
|
|
23
|
-
target_key = eval.target
|
|
24
|
-
return true unless target_key
|
|
25
|
-
|
|
26
|
-
mentry = manifest.resolver.resolve(target_key).entry
|
|
27
|
-
schema_ref = mentry&.schema
|
|
28
|
-
return true unless schema_ref
|
|
29
|
-
|
|
30
|
-
schema = @schemas.fetch_or_nil(schema_ref)
|
|
31
|
-
return true unless schema
|
|
32
|
-
|
|
33
|
-
frontmatter =
|
|
34
|
-
eval.envelope.meta&.dig("frontmatter") || eval.envelope.meta || {}
|
|
35
|
-
begin
|
|
36
|
-
schema.validate!(frontmatter)
|
|
37
|
-
true
|
|
38
|
-
rescue Textus::SchemaViolation => e
|
|
39
|
-
@reason = humanize(e)
|
|
40
|
-
false
|
|
41
|
-
end
|
|
42
|
-
rescue StandardError => e
|
|
43
|
-
@reason = "schema validation error: #{e.message}"
|
|
44
|
-
false
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def humanize(err)
|
|
50
|
-
d = err.details
|
|
51
|
-
return err.message.dup unless d.is_a?(Hash)
|
|
52
|
-
return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
|
|
53
|
-
return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
|
|
54
|
-
|
|
55
|
-
err.message.dup
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# Predicate: a proposal may only target a `canon` zone (ADR 0035). Runs
|
|
8
|
-
# on the `accept` floor, where Evaluation#target is the proposal's
|
|
9
|
-
# resolved target_key. Refuses promotion into workspace/derived/
|
|
10
|
-
# quarantine/queue — the queue→canon path is the only coherent one.
|
|
11
|
-
# No bespoke #error; failures accumulate into GuardFailed (ADR 0031).
|
|
12
|
-
class TargetIsCanon
|
|
13
|
-
attr_reader :reason
|
|
14
|
-
|
|
15
|
-
def name = "target_is_canon"
|
|
16
|
-
|
|
17
|
-
def call(eval)
|
|
18
|
-
zone = eval.manifest.resolver.resolve(eval.target).entry.zone
|
|
19
|
-
kind = eval.manifest.policy.declared_kind(zone.to_s)
|
|
20
|
-
return true if kind == :canon
|
|
21
|
-
|
|
22
|
-
@reason = "proposal target '#{eval.target}' is in zone '#{zone}' " \
|
|
23
|
-
"(kind: #{kind || "none"}); proposals may only target a canon zone"
|
|
24
|
-
false
|
|
25
|
-
rescue Textus::UnknownKey
|
|
26
|
-
@reason = "proposal target '#{eval.target}' resolves to no declared entry"
|
|
27
|
-
false
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
module Policy
|
|
6
|
-
module Predicates
|
|
7
|
-
# Predicate #0 of every write guard. Wraps the post-0.31.0 capability
|
|
8
|
-
# topology gate (role.can ⊇ verb_for(zone.kind)). On failure, #error
|
|
9
|
-
# raises the capability-shaped WriteForbidden so the topology refusal
|
|
10
|
-
# — textus's signature product feature — is unchanged.
|
|
11
|
-
class ZoneWritableBy
|
|
12
|
-
attr_reader :reason
|
|
13
|
-
|
|
14
|
-
def name = "zone_writable_by"
|
|
15
|
-
|
|
16
|
-
def call(eval)
|
|
17
|
-
manifest = eval.manifest
|
|
18
|
-
@mentry = manifest.resolver.resolve(eval.target).entry
|
|
19
|
-
return true if manifest.policy.permission_for(@mentry.zone.to_s).allows_write?(eval.actor)
|
|
20
|
-
|
|
21
|
-
@verb = manifest.policy.verb_for_zone(@mentry.zone) # capability the kind requires
|
|
22
|
-
@holders = manifest.policy.roles_with_capability(@verb)
|
|
23
|
-
@reason = "zone '#{@mentry.zone}' needs capability '#{@verb}'; '#{eval.actor}' lacks it"
|
|
24
|
-
false
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Matches the capability-shaped WriteForbidden landed by ADR 0030
|
|
28
|
-
# Task 3:
|
|
29
|
-
# WriteForbidden.new(key, zone, verb:, holders:)
|
|
30
|
-
# → "writing '<k>' (zone '<z>') needs capability '<verb>'",
|
|
31
|
-
# hint: "held by: <holders>; pass --as=<role>".
|
|
32
|
-
def error(_eval)
|
|
33
|
-
Textus::WriteForbidden.new(@mentry.key, @mentry.zone, verb: @verb, holders: @holders)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
# An entry's data-acquisition declaration (ADR 0094). `source:` says HOW the
|
|
5
|
-
# entry's data is acquired; rendering is a publish concern, so there are no
|
|
6
|
-
# template/render fields here. `from` is the acquire + staleness axis:
|
|
7
|
-
# from: project -> derived (internal projection; observable -> rdeps staleness)
|
|
8
|
-
# from: handler -> intake (external fetch; unobservable -> ttl staleness)
|
|
9
|
-
# from: command -> external (out-of-band runner; staleness only, textus never runs it)
|
|
10
|
-
# Materialization is async-only (job-queue model): a write enqueues a
|
|
11
|
-
# `materialize` job, converged by a worker. There is no per-entry write
|
|
12
|
-
# trigger knob.
|
|
13
|
-
class Source
|
|
14
|
-
FROMS = %w[project handler command].freeze
|
|
15
|
-
|
|
16
|
-
attr_reader :from, :handler, :config, :command, :sources
|
|
17
|
-
|
|
18
|
-
def initialize(raw)
|
|
19
|
-
@from = raw["from"].to_s
|
|
20
|
-
unless FROMS.include?(@from)
|
|
21
|
-
raise Textus::BadManifest.new("source.from must be one of #{FROMS.join("|")}, got #{raw["from"].inspect}")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
@ttl = raw["ttl"]
|
|
25
|
-
@projection = {}
|
|
26
|
-
|
|
27
|
-
case @from
|
|
28
|
-
when "project" then init_project(raw)
|
|
29
|
-
when "handler" then init_handler(raw)
|
|
30
|
-
when "command" then init_command(raw)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def kind = @from == "handler" ? :intake : :derived
|
|
35
|
-
def external? = @from == "command"
|
|
36
|
-
def projection? = @from == "project"
|
|
37
|
-
def ttl_seconds = @ttl.nil? ? nil : Textus::Domain::Duration.seconds(@ttl)
|
|
38
|
-
|
|
39
|
-
# Flattened projection accessors (ADR 0094) — read directly off the source
|
|
40
|
-
# block; nil when absent or not a projection source.
|
|
41
|
-
def select = @projection["select"]
|
|
42
|
-
def pluck = @projection["pluck"]
|
|
43
|
-
def sort_by = @projection["sort_by"]
|
|
44
|
-
def transform = @projection["transform"]
|
|
45
|
-
|
|
46
|
-
# The projection spec hash fed to Textus::Projection (string keys, only the
|
|
47
|
-
# present fields). {} when not a projection.
|
|
48
|
-
def projection_spec = @projection.dup
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def init_project(raw)
|
|
53
|
-
%w[select pluck sort_by transform].each { |f| @projection[f] = raw[f] if raw.key?(f) }
|
|
54
|
-
return unless @projection["select"].nil? && @projection["transform"].nil?
|
|
55
|
-
|
|
56
|
-
raise Textus::BadManifest.new("source (from: project) requires `select:` and/or `transform:`")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def init_handler(raw)
|
|
60
|
-
@handler = raw["handler"] or
|
|
61
|
-
raise Textus::BadManifest.new("source (from: handler) requires a `handler:` field")
|
|
62
|
-
@config = raw["config"] || {}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def init_command(raw)
|
|
66
|
-
@command = raw["command"] or
|
|
67
|
-
raise Textus::BadManifest.new("source (from: command) requires a `command:` field")
|
|
68
|
-
@sources = raw["sources"] || []
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Envelope
|
|
3
|
-
module IO
|
|
4
|
-
# Read-only counterpart to EnvelopeWriter. Resolves a key, reads the
|
|
5
|
-
# bytes, parses them via the format strategy, and hands back an
|
|
6
|
-
# Envelope. Used by Mv (pre-move inspection) and by EnvelopeWriter
|
|
7
|
-
# (existing-uid lookup for the uid-preservation step in #put).
|
|
8
|
-
#
|
|
9
|
-
# No audit, no events, no permission checks — those live one layer up.
|
|
10
|
-
class Reader
|
|
11
|
-
def self.from(container:)
|
|
12
|
-
new(file_store: container.file_store, manifest: container.manifest)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(file_store:, manifest:)
|
|
16
|
-
@file_store = file_store
|
|
17
|
-
@manifest = manifest
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def read(key)
|
|
21
|
-
res = @manifest.resolver.resolve(key)
|
|
22
|
-
path = res.path
|
|
23
|
-
return nil unless @file_store.exists?(path)
|
|
24
|
-
|
|
25
|
-
mentry = res.entry
|
|
26
|
-
raw = @file_store.read(path)
|
|
27
|
-
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
28
|
-
Textus::Envelope.build(
|
|
29
|
-
key: key, mentry: mentry, path: path,
|
|
30
|
-
meta: parsed["_meta"], body: parsed["body"],
|
|
31
|
-
etag: Etag.for_bytes(raw), content: parsed["content"]
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def existing_uid(key)
|
|
36
|
-
env = read(key)
|
|
37
|
-
env&.uid
|
|
38
|
-
rescue StandardError
|
|
39
|
-
nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def exists?(key)
|
|
43
|
-
@file_store.exists?(@manifest.resolver.resolve(key).path)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Envelope
|
|
5
|
-
module IO
|
|
6
|
-
# Owns the write pipeline (validate, serialize, etag-check, write, audit).
|
|
7
|
-
# Talks to ports (FileStore, Schemas, AuditLog, Manifest) and an
|
|
8
|
-
# Reader for the existing-uid lookup.
|
|
9
|
-
#
|
|
10
|
-
# Invariant: every public method's final action is @audit_log.append(...).
|
|
11
|
-
#
|
|
12
|
-
# No permission check, no event firing — those belong to the caller
|
|
13
|
-
# (Write::Put / ::Delete / ::Mv).
|
|
14
|
-
class Writer
|
|
15
|
-
Payload = Data.define(:meta, :body, :content)
|
|
16
|
-
|
|
17
|
-
def self.from(container:, call:)
|
|
18
|
-
new(
|
|
19
|
-
file_store: container.file_store, manifest: container.manifest,
|
|
20
|
-
schemas: container.schemas, audit_log: container.audit_log,
|
|
21
|
-
call: call, reader: Reader.from(container: container)
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def initialize(file_store:, manifest:, schemas:, audit_log:, call:, reader:)
|
|
26
|
-
@file_store = file_store
|
|
27
|
-
@manifest = manifest
|
|
28
|
-
@schemas = schemas
|
|
29
|
-
@audit_log = audit_log
|
|
30
|
-
@call = call
|
|
31
|
-
@reader = reader
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def put(key, mentry:, payload:, if_etag: nil)
|
|
35
|
-
path = @manifest.resolver.resolve(key).path
|
|
36
|
-
|
|
37
|
-
meta = payload.meta || {}
|
|
38
|
-
|
|
39
|
-
existing_uid = @reader.existing_uid(key)
|
|
40
|
-
meta, content = ensure_uid(mentry.format, meta, payload.content, existing_uid)
|
|
41
|
-
|
|
42
|
-
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
43
|
-
mentry: mentry, path: path,
|
|
44
|
-
meta: meta, body: payload.body, content: content
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
enforce_name_match!(path, eff_meta, mentry.format)
|
|
48
|
-
|
|
49
|
-
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
50
|
-
if schema
|
|
51
|
-
Entry.for_format(mentry.format).validate_against(
|
|
52
|
-
schema,
|
|
53
|
-
{ "_meta" => eff_meta, "content" => eff_content },
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
|
|
58
|
-
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
59
|
-
|
|
60
|
-
@file_store.write(path, bytes)
|
|
61
|
-
etag_after = Etag.for_bytes(bytes)
|
|
62
|
-
envelope = Textus::Envelope.build(
|
|
63
|
-
key: key, mentry: mentry, path: path,
|
|
64
|
-
meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
|
|
65
|
-
)
|
|
66
|
-
@audit_log.append(
|
|
67
|
-
role: @call.role, verb: "put", key: key,
|
|
68
|
-
etag_before: etag_before, etag_after: etag_after,
|
|
69
|
-
extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
70
|
-
)
|
|
71
|
-
envelope
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def delete(key, mentry: nil, if_etag: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
75
|
-
# `mentry:` is accepted for symmetry with `put` / `move` and to
|
|
76
|
-
# leave room for future format-specific delete hooks; no field
|
|
77
|
-
# on it is needed today.
|
|
78
|
-
path = @manifest.resolver.resolve(key).path
|
|
79
|
-
raise UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)) unless @file_store.exists?(path)
|
|
80
|
-
|
|
81
|
-
etag_before = @file_store.etag(path)
|
|
82
|
-
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
83
|
-
|
|
84
|
-
@file_store.delete(path)
|
|
85
|
-
prune_empty_parents(path)
|
|
86
|
-
@audit_log.append(
|
|
87
|
-
role: @call.role, verb: "key_delete", key: key,
|
|
88
|
-
etag_before: etag_before, etag_after: nil,
|
|
89
|
-
extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def move(from_key:, to_key:, new_mentry:, if_etag: nil)
|
|
94
|
-
from_path = @manifest.resolver.resolve(from_key).path
|
|
95
|
-
to_path = @manifest.resolver.resolve(to_key).path
|
|
96
|
-
raise UnknownKey.new(from_key, suggestions: @manifest.resolver.suggestions_for(from_key)) unless @file_store.exists?(from_path)
|
|
97
|
-
|
|
98
|
-
etag_before = @file_store.etag(from_path)
|
|
99
|
-
raise EtagMismatch.new(from_key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
100
|
-
|
|
101
|
-
FileUtils.mkdir_p(File.dirname(to_path))
|
|
102
|
-
FileUtils.mv(from_path, to_path)
|
|
103
|
-
prune_empty_parents(from_path)
|
|
104
|
-
basename = to_key.split(".").last
|
|
105
|
-
Entry.for_format(new_mentry.format).rewrite_name(to_path, basename)
|
|
106
|
-
etag_after = Etag.for_file(to_path)
|
|
107
|
-
|
|
108
|
-
raw = @file_store.read(to_path)
|
|
109
|
-
parsed = Entry.for_format(new_mentry.format).parse(raw, path: to_path)
|
|
110
|
-
envelope = Textus::Envelope.build(
|
|
111
|
-
key: to_key, mentry: new_mentry, path: to_path,
|
|
112
|
-
meta: parsed["_meta"], body: parsed["body"],
|
|
113
|
-
etag: etag_after, content: parsed["content"]
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
extras = {
|
|
117
|
-
"from_key" => from_key, "to_key" => to_key,
|
|
118
|
-
"from_path" => from_path, "to_path" => to_path,
|
|
119
|
-
"uid" => envelope.uid
|
|
120
|
-
}
|
|
121
|
-
extras["correlation_id"] = @call.correlation_id if @call.correlation_id
|
|
122
|
-
|
|
123
|
-
@audit_log.append(
|
|
124
|
-
role: @call.role, verb: "key_mv", key: to_key,
|
|
125
|
-
etag_before: etag_before, etag_after: etag_after,
|
|
126
|
-
extras: extras
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
envelope
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
# After a file leaves a directory (delete or move-source), remove any
|
|
135
|
-
# now-empty parent dirs so bulk move/delete doesn't accrue orphan dirs
|
|
136
|
-
# (F3 of #161). Floored at the entry's *zone directory* — a zone is a
|
|
137
|
-
# declared, first-class container, so its own dir is preserved even when
|
|
138
|
-
# momentarily empty; only the sub-dirs the bulk op carved out are
|
|
139
|
-
# pruned. Stops at the first non-empty ancestor, so a dir holding a
|
|
140
|
-
# `.gitkeep` or sibling entries survives. Best-effort: a lost race or a
|
|
141
|
-
# non-empty dir is silently fine, never fatal to the write.
|
|
142
|
-
def prune_empty_parents(path)
|
|
143
|
-
floor = zone_floor(path)
|
|
144
|
-
return unless floor
|
|
145
|
-
|
|
146
|
-
dir = File.dirname(path)
|
|
147
|
-
while dir.start_with?("#{floor}/") && Dir.empty?(dir)
|
|
148
|
-
Dir.rmdir(dir)
|
|
149
|
-
dir = File.dirname(dir)
|
|
150
|
-
end
|
|
151
|
-
rescue SystemCallError
|
|
152
|
-
nil
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# The zone directory under which `path` lives (`<root>/zones/<zone>`),
|
|
156
|
-
# or nil if `path` is not under the store's zones tree.
|
|
157
|
-
def zone_floor(path)
|
|
158
|
-
zones_root = File.join(@manifest.data.root, "zones")
|
|
159
|
-
prefix = "#{zones_root}/"
|
|
160
|
-
return nil unless path.start_with?(prefix)
|
|
161
|
-
|
|
162
|
-
zone_seg = path.delete_prefix(prefix).split("/").first
|
|
163
|
-
zone_seg && File.join(zones_root, zone_seg)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def ensure_uid(format, meta, content, existing_uid)
|
|
167
|
-
Textus::Entry.for_format(format).inject_uid(meta, content, existing_uid)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def enforce_name_match!(path, meta, format)
|
|
171
|
-
Textus::Entry.for_format(format).enforce_name_match!(path, meta)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def serialize_for_put(mentry:, path:, meta:, body:, content:)
|
|
175
|
-
Textus::Entry.for_format(mentry.format).serialize_for_put(
|
|
176
|
-
meta: meta, body: body, content: content, path: path,
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
data/lib/textus/hooks/builtin.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "csv"
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "rexml/document"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Hooks
|
|
8
|
-
module Builtin
|
|
9
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
10
|
-
def self.register_all(events:, rpc:) # rubocop:disable Lint/UnusedMethodArgument
|
|
11
|
-
rpc.register(:resolve_handler, :json) do |caps:, config:, args:|
|
|
12
|
-
_ = caps
|
|
13
|
-
_ = args
|
|
14
|
-
data = JSON.parse(config["bytes"].to_s)
|
|
15
|
-
{ _meta: {}, body: YAML.dump(data) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
rpc.register(:resolve_handler, :csv) do |caps:, config:, args:|
|
|
19
|
-
_ = caps
|
|
20
|
-
_ = args
|
|
21
|
-
rows = CSV.parse(config["bytes"].to_s, headers: true).map(&:to_h)
|
|
22
|
-
{ _meta: {}, body: YAML.dump(rows) }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
rpc.register(:resolve_handler, :"markdown-links") do |caps:, config:, args:|
|
|
26
|
-
_ = caps
|
|
27
|
-
_ = args
|
|
28
|
-
links = config["bytes"].to_s.scan(%r{\[([^\]]+)\]\((https?://[^)\s]+)\)}).map do |text, href|
|
|
29
|
-
{ "text" => text, "href" => href }
|
|
30
|
-
end
|
|
31
|
-
{ _meta: {}, body: YAML.dump(links) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
rpc.register(:resolve_handler, :"ical-events") do |caps:, config:, args:|
|
|
35
|
-
_ = caps
|
|
36
|
-
_ = args
|
|
37
|
-
events_list = []
|
|
38
|
-
current = nil
|
|
39
|
-
config["bytes"].to_s.each_line do |line|
|
|
40
|
-
line = line.strip
|
|
41
|
-
case line
|
|
42
|
-
when "BEGIN:VEVENT" then current = {}
|
|
43
|
-
when "END:VEVENT"
|
|
44
|
-
events_list << current if current
|
|
45
|
-
current = nil
|
|
46
|
-
when /\A(SUMMARY|DTSTART|DTEND|UID|LOCATION|DESCRIPTION):(.*)\z/
|
|
47
|
-
current[Regexp.last_match(1).downcase] = Regexp.last_match(2) if current
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
{ _meta: {}, body: YAML.dump(events_list) }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
rpc.register(:resolve_handler, :rss) do |caps:, config:, args:|
|
|
54
|
-
_ = caps
|
|
55
|
-
_ = args
|
|
56
|
-
doc = REXML::Document.new(config["bytes"].to_s)
|
|
57
|
-
items = doc.elements.to_a("//item").map do |item|
|
|
58
|
-
{
|
|
59
|
-
"title" => item.elements["title"]&.text,
|
|
60
|
-
"link" => item.elements["link"]&.text,
|
|
61
|
-
"pubDate" => item.elements["pubDate"]&.text,
|
|
62
|
-
}
|
|
63
|
-
end
|
|
64
|
-
{ _meta: {}, body: YAML.dump(items) }
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
data/lib/textus/hooks/catalog.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
# The single source of truth for hook event names and their required
|
|
6
|
-
# kwargs. EventBus, RpcRegistry, and the Loader DSL router all read these
|
|
7
|
-
# tables directly — the registries do not keep their own copies. Catalog
|
|
8
|
-
# references no other constant, so it has no load-order cycle, which is
|
|
9
|
-
# what removed the previous drift hazard (EventBus held a hard-coded
|
|
10
|
-
# `RPC_EVENTS` list that could fall out of sync with RpcRegistry's table).
|
|
11
|
-
module Catalog
|
|
12
|
-
# Pub-sub events: 0..N handlers, fire-and-forget, receive `ctx:`.
|
|
13
|
-
PUBSUB = {
|
|
14
|
-
entry_written: %i[ctx key envelope],
|
|
15
|
-
entry_deleted: %i[ctx key],
|
|
16
|
-
entry_fetched: %i[ctx key envelope change],
|
|
17
|
-
entry_renamed: %i[ctx key from_key to_key envelope],
|
|
18
|
-
entry_produced: %i[ctx key envelope sources],
|
|
19
|
-
produce_failed: %i[ctx keys error],
|
|
20
|
-
proposal_accepted: %i[ctx key target_key],
|
|
21
|
-
proposal_rejected: %i[ctx key target_key],
|
|
22
|
-
entry_published: %i[ctx key envelope source target],
|
|
23
|
-
store_loaded: %i[ctx],
|
|
24
|
-
session_opened: %i[ctx role cursor],
|
|
25
|
-
entry_fetch_started: %i[ctx key mode],
|
|
26
|
-
entry_fetch_failed: %i[ctx key error_class error_message],
|
|
27
|
-
}.freeze
|
|
28
|
-
|
|
29
|
-
# RPC events: single handler, return value matters, receive `caps:`.
|
|
30
|
-
RPC = {
|
|
31
|
-
resolve_handler: %i[caps config args],
|
|
32
|
-
transform_rows: %i[caps rows config],
|
|
33
|
-
validate: %i[caps],
|
|
34
|
-
}.freeze
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|