textus 0.55.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -2768
- data/README.md +8 -9
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +0 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{dispatch → protocol}/binder.rb +1 -7
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store → protocol}/layout.rb +16 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +22 -21
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +2 -7
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +7 -7
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +12 -12
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/server.rb +35 -24
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -53
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -16
- metadata +169 -185
- data/SPEC.md +0 -909
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -141
- data/lib/textus/boot.rb +0 -181
- data/lib/textus/dispatch/contracts.rb +0 -63
- data/lib/textus/dispatch/handler_registry.rb +0 -21
- data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
- data/lib/textus/dispatch/middleware/auth.rb +0 -40
- data/lib/textus/dispatch/middleware/base.rb +0 -26
- data/lib/textus/dispatch/middleware/binder.rb +0 -20
- data/lib/textus/dispatch/middleware/cascade.rb +0 -53
- data/lib/textus/dispatch/pipeline.rb +0 -35
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -43
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -77
- data/lib/textus/format/json.rb +0 -81
- data/lib/textus/format/markdown.rb +0 -59
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -104
- data/lib/textus/format.rb +0 -61
- data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
- data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
- data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
- data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
- data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
- data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
- data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
- data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
- data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
- data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
- data/lib/textus/handlers/read/audit_entries.rb +0 -48
- data/lib/textus/handlers/read/blame_entry.rb +0 -71
- data/lib/textus/handlers/read/deps_entry.rb +0 -17
- data/lib/textus/handlers/read/get_entry.rb +0 -68
- data/lib/textus/handlers/read/list_keys.rb +0 -36
- data/lib/textus/handlers/read/pulse_entries.rb +0 -66
- data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
- data/lib/textus/handlers/read/uid_entry.rb +0 -18
- data/lib/textus/handlers/read/where_entry.rb +0 -18
- data/lib/textus/handlers/write/accept_proposal.rb +0 -39
- data/lib/textus/handlers/write/data_mv.rb +0 -55
- data/lib/textus/handlers/write/delete_key.rb +0 -17
- data/lib/textus/handlers/write/enqueue_job.rb +0 -27
- data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
- data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
- data/lib/textus/handlers/write/move_key.rb +0 -80
- data/lib/textus/handlers/write/propose_entry.rb +0 -29
- data/lib/textus/handlers/write/put_entry.rb +0 -29
- data/lib/textus/handlers/write/reject_proposal.rb +0 -29
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
- data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
- data/lib/textus/manifest/policy/predicates.rb +0 -54
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/orchestration.rb +0 -55
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel.rb +0 -25
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -49
- data/lib/textus/port/storage/interface.rb +0 -17
- data/lib/textus/port/store.rb +0 -149
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -40
- data/lib/textus/produce/publisher.rb +0 -21
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/registry.rb +0 -42
- data/lib/textus/schema/tools.rb +0 -108
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/store/container.rb +0 -173
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/entry/reader.rb +0 -47
- data/lib/textus/store/entry/writer.rb +0 -219
- data/lib/textus/store/envelope/meta.rb +0 -61
- data/lib/textus/store/freshness/drift_detector.rb +0 -93
- data/lib/textus/store/freshness/evaluator.rb +0 -20
- data/lib/textus/store/freshness/verdict.rb +0 -24
- data/lib/textus/store/freshness.rb +0 -8
- data/lib/textus/store/index/builder.rb +0 -76
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -124
- data/lib/textus/store/jobs/queue.rb +0 -162
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention/base.rb +0 -52
- data/lib/textus/store/jobs/retention/sweep.rb +0 -55
- data/lib/textus/store/jobs/retention.rb +0 -8
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store.rb +0 -122
- data/lib/textus/surface/cli/verb/doctor.rb +0 -20
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp/projector.rb +0 -20
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -90
- data/lib/textus/value/result.rb +0 -46
- data/lib/textus/verb_registry.rb +0 -417
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require_relative "schema"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
class Data
|
|
7
|
+
AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 1000 }.freeze
|
|
8
|
+
WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :raw, :root, :entries, :declared_lane_kinds,
|
|
11
|
+
:lane_descs,
|
|
12
|
+
:audit_config, :worker_config, :role_caps
|
|
13
|
+
|
|
14
|
+
def self.validate_key!(key)
|
|
15
|
+
raise UsageError.new("empty key") if key.nil? || key.empty?
|
|
16
|
+
|
|
17
|
+
Protocol::Key::Grammar.validate!(key)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_key!(key) = self.class.validate_key!(key)
|
|
21
|
+
|
|
22
|
+
def self.parse(raw, root:)
|
|
23
|
+
%w[roles lanes owner].each do |stale_key|
|
|
24
|
+
next unless raw.key?(stale_key)
|
|
25
|
+
|
|
26
|
+
raise BadManifest.new(
|
|
27
|
+
"unknown top-level key '#{stale_key}' — protocol textus/4 fixes roles/lanes in " \
|
|
28
|
+
"code; remove this section from manifest.yaml (see CHANGELOG)",
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
raise BadManifest.new("manifest must declare data:") if Array(raw["data"]).empty? && raw["data"] != {}
|
|
32
|
+
|
|
33
|
+
Schema.validate!(raw)
|
|
34
|
+
new(raw: raw, root: root)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(raw:, root:)
|
|
38
|
+
@raw = raw
|
|
39
|
+
@root = root
|
|
40
|
+
@declared_lane_kinds = Textus::Protocol::V4::LANES.keys.to_h { |name| [name, name] }
|
|
41
|
+
@lane_descs = Textus::Protocol::V4::LANES.transform_values { |row| row[:desc] }
|
|
42
|
+
@audit_config = build_audit_config(raw)
|
|
43
|
+
@worker_config = build_worker_config(raw)
|
|
44
|
+
@role_caps = {}
|
|
45
|
+
@entries = build_entries(raw)
|
|
46
|
+
validate_declared_keys!
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_audit_config(raw)
|
|
53
|
+
a = raw["audit"] || {}
|
|
54
|
+
{
|
|
55
|
+
max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
|
|
56
|
+
keep: a["keep"] || AUDIT_DEFAULTS[:keep],
|
|
57
|
+
}.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_worker_config(raw)
|
|
61
|
+
w = raw["worker"] || {}
|
|
62
|
+
{
|
|
63
|
+
pool: w["pool"] || WORKER_DEFAULTS[:pool],
|
|
64
|
+
poll: w["poll"] || WORKER_DEFAULTS[:poll],
|
|
65
|
+
lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
|
|
66
|
+
max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
|
|
67
|
+
}.freeze
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_entries(raw)
|
|
71
|
+
(raw["data"] || {}).flat_map do |lane_name, lane_entries|
|
|
72
|
+
unless Textus::Protocol::V4::LANES.key?(lane_name)
|
|
73
|
+
raise BadManifest.new("unknown lane '#{lane_name}' (known: #{Textus::Protocol::V4::LANES.keys.join(", ")})")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Array(lane_entries).map { |e| Manifest::Entry::Parser.call(e.merge("lane" => lane_name), root: @root) }
|
|
77
|
+
end.freeze
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_declared_keys!
|
|
81
|
+
@entries.each do |e|
|
|
82
|
+
raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
|
|
83
|
+
|
|
84
|
+
Protocol::Key::Grammar.validate!(e.key)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Entry
|
|
5
|
+
class Base < Entry
|
|
6
|
+
attr_reader :raw, :key, :path, :lane, :format, :publish_targets, :schema, :naming
|
|
7
|
+
|
|
8
|
+
# rubocop:disable Lint/MissingSuper
|
|
9
|
+
def initialize(raw:, key:, path:, lane:, format:, schema: nil, naming: nil, publish_targets: [])
|
|
10
|
+
@raw = raw
|
|
11
|
+
@key = key
|
|
12
|
+
@path = path
|
|
13
|
+
@lane = lane
|
|
14
|
+
@schema = schema
|
|
15
|
+
@naming = naming
|
|
16
|
+
@format = format
|
|
17
|
+
@publish_targets = Array(publish_targets)
|
|
18
|
+
end
|
|
19
|
+
# rubocop:enable Lint/MissingSuper
|
|
20
|
+
|
|
21
|
+
def lane_writers(policy)
|
|
22
|
+
policy.writers_for(@lane)
|
|
23
|
+
rescue UsageError => e
|
|
24
|
+
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
|
|
28
|
+
|
|
29
|
+
def derived? = false
|
|
30
|
+
def nested? = false
|
|
31
|
+
def leaf? = false
|
|
32
|
+
|
|
33
|
+
# Production traits. Default false on all non-produced entries.
|
|
34
|
+
# Lets publish modes call these without a `respond_to?` guard.
|
|
35
|
+
def external? = false
|
|
36
|
+
|
|
37
|
+
# Whether git should track this entry's file. Default true; an entry
|
|
38
|
+
# marked `tracked: false` in the manifest stays protocol-readable but is
|
|
39
|
+
# listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
|
|
40
|
+
# reads from raw here rather than threading through every constructor.
|
|
41
|
+
def tracked? = @raw["tracked"] != false
|
|
42
|
+
|
|
43
|
+
# Single source of truth is @publish_targets (ADR 0094). These
|
|
44
|
+
# derive the ADR-0049/0052 views the publish modes consume.
|
|
45
|
+
def publish_to = @publish_targets.select(&:to_target?).map(&:to)
|
|
46
|
+
def publish_tree = @publish_targets.find(&:tree_target?)&.tree
|
|
47
|
+
|
|
48
|
+
def events = {}
|
|
49
|
+
|
|
50
|
+
# Minimal context object passed into entry `publish_via` hooks.
|
|
51
|
+
# Everything beyond the three primitives is derived.
|
|
52
|
+
PublishContext = ::Data.define(:container, :reader) do
|
|
53
|
+
def manifest = container.manifest
|
|
54
|
+
def root = container.root
|
|
55
|
+
def repo_root = File.dirname(container.root)
|
|
56
|
+
def store_root = container.root
|
|
57
|
+
|
|
58
|
+
# No-op: event bus removed in workflow redesign; callers that fire
|
|
59
|
+
# :entry_published / :entry_produced remain unchanged in the source.
|
|
60
|
+
def emit(_event, **_payload) = nil
|
|
61
|
+
|
|
62
|
+
# Read a named template from the store's templates/ directory.
|
|
63
|
+
# Raises TemplateError when the file doesn't exist.
|
|
64
|
+
def read_template(name)
|
|
65
|
+
path = container.layout.template_path(name)
|
|
66
|
+
unless File.exist?(path)
|
|
67
|
+
raise Textus::TemplateError.new(
|
|
68
|
+
"template '#{name}' not found",
|
|
69
|
+
template_name: name,
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
File.read(path)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
|
|
77
|
+
# publish algorithm. A plain entry publishes via ToPaths (publish_to) or
|
|
78
|
+
# None; Nested resolves among the key/path-driven modes. Derived
|
|
79
|
+
# overrides publish_via to materialize first.
|
|
80
|
+
def publish_mode
|
|
81
|
+
@publish_mode ||= Publish.resolve(self)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def publish_via(pctx, prefix: nil)
|
|
85
|
+
publish_mode.publish(pctx, prefix: prefix)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_publish_targets(pts)
|
|
89
|
+
dup.tap { |e| e.instance_variable_set(:@publish_targets, pts) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Entry
|
|
5
|
+
class Leaf < Base
|
|
6
|
+
KIND = :leaf
|
|
7
|
+
|
|
8
|
+
def leaf? = true
|
|
9
|
+
|
|
10
|
+
def self.from_raw(common, _raw)
|
|
11
|
+
new(**common)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Entry::REGISTRY[KIND] = self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Entry
|
|
5
|
+
class Nested < Base
|
|
6
|
+
def nested? = true
|
|
7
|
+
|
|
8
|
+
KIND = :nested
|
|
9
|
+
|
|
10
|
+
def self.from_raw(common, _raw)
|
|
11
|
+
new(**common)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Entry::REGISTRY[KIND] = self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Entry
|
|
5
|
+
module Parser
|
|
6
|
+
def self.call(raw, root: nil)
|
|
7
|
+
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
8
|
+
lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
|
|
9
|
+
|
|
10
|
+
naming = raw["naming"]
|
|
11
|
+
if naming && !Textus::Protocol::V4::NAMING_PATTERNS.key?(naming)
|
|
12
|
+
known_aliases = Textus::Protocol::V4::NAMING_PATTERNS.keys.join(", ")
|
|
13
|
+
raise BadManifest.new(
|
|
14
|
+
"entry '#{key}': unknown naming alias '#{naming}' (known: #{known_aliases})",
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
kind = infer_kind(key, raw, root:)
|
|
19
|
+
format = resolve_format(key, kind, root, raw["format"])
|
|
20
|
+
path = derive_path(key, kind, format)
|
|
21
|
+
|
|
22
|
+
common = {
|
|
23
|
+
raw: raw,
|
|
24
|
+
key: key, path: path, lane: lane,
|
|
25
|
+
format: format, naming: naming,
|
|
26
|
+
schema: raw["schema"]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
klass = Entry::REGISTRY[kind]
|
|
30
|
+
raise BadManifest.new("entry '#{key}': unknown inferred kind: #{kind.inspect}") unless klass
|
|
31
|
+
|
|
32
|
+
klass.from_raw(common, raw)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.infer_kind(key, _raw, root:)
|
|
36
|
+
base = key.split(".").join("/")
|
|
37
|
+
dir = root ? File.join(root, "data", base) : nil
|
|
38
|
+
|
|
39
|
+
return :nested if dir && File.directory?(dir)
|
|
40
|
+
return :leaf if dir && Dir.glob("#{dir}.*").any? { |p| File.file?(p) }
|
|
41
|
+
|
|
42
|
+
:leaf
|
|
43
|
+
end
|
|
44
|
+
private_class_method :infer_kind
|
|
45
|
+
|
|
46
|
+
def self.resolve_format(key, kind, root, declared)
|
|
47
|
+
return declared || "markdown" if kind == :nested
|
|
48
|
+
|
|
49
|
+
dir = root ? File.join(root, "data", key.split(".").join("/")) : nil
|
|
50
|
+
if dir
|
|
51
|
+
existing = Dir.glob("#{dir}.*").find { |p| File.file?(p) }
|
|
52
|
+
return Textus::Protocol::Format.infer_from_extension(File.extname(existing)) if existing
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
declared || "markdown"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.derive_path(key, kind_sym, format)
|
|
59
|
+
dir_path = key.split(".").join("/")
|
|
60
|
+
return dir_path if kind_sym == :nested
|
|
61
|
+
|
|
62
|
+
ext = Textus::Protocol::Format.for(format).extensions.first
|
|
63
|
+
"#{dir_path}#{ext}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_class_method :derive_path
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
class Entry
|
|
7
|
+
module Publish
|
|
8
|
+
def self.resolve(entry)
|
|
9
|
+
set = []
|
|
10
|
+
set << "publish.to" unless Array(entry.publish_to).empty?
|
|
11
|
+
set << "publish.tree" unless entry.publish_tree.nil?
|
|
12
|
+
|
|
13
|
+
if set.length > 1
|
|
14
|
+
raise Textus::UsageError.new(
|
|
15
|
+
"entry '#{entry.key}': #{set.join(" and ")} are mutually exclusive — an entry publishes exactly one way",
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
mode_for(entry, set.first)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.mode_for(entry, key)
|
|
23
|
+
case key
|
|
24
|
+
when "publish.to" then ToPaths.new(entry)
|
|
25
|
+
when "publish.tree" then Tree.new(entry)
|
|
26
|
+
else None.new(entry)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
private_class_method :mode_for
|
|
30
|
+
|
|
31
|
+
class Mode
|
|
32
|
+
def initialize(entry)
|
|
33
|
+
@entry = entry
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :entry
|
|
37
|
+
|
|
38
|
+
def validate!; end
|
|
39
|
+
|
|
40
|
+
def keyless? = false
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def repo_abs(pctx, rel)
|
|
45
|
+
File.expand_path(File.join(pctx.repo_root, rel))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inside_repo?(pctx, abs)
|
|
49
|
+
abs.start_with?(File.expand_path(pctx.repo_root) + File::SEPARATOR)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def store_base(pctx)
|
|
53
|
+
File.join(pctx.root, Textus::Protocol::Key::Path.normalize_relative_path(entry.path))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class None < Mode
|
|
58
|
+
def publish(_pctx, prefix: nil) = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# publish.to: render or copy the entry's stored data to each fixed repo path.
|
|
62
|
+
class ToPaths < Mode
|
|
63
|
+
def initialize(entry, publisher: Textus::Infra::Port::Publisher.new)
|
|
64
|
+
super(entry)
|
|
65
|
+
@publisher = publisher
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def publish(pctx, prefix: nil)
|
|
69
|
+
targets = entry.publish_targets.select(&:to_target?)
|
|
70
|
+
return nil if targets.empty?
|
|
71
|
+
|
|
72
|
+
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
73
|
+
envelope = pctx.reader.call(key: entry.key)
|
|
74
|
+
content = nil
|
|
75
|
+
|
|
76
|
+
targets.each do |t|
|
|
77
|
+
if t.renders?
|
|
78
|
+
content ||= Textus::Protocol::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
79
|
+
publish_bytes(render_bytes(t, content, pctx), entry.key, t, pctx, data_path, envelope)
|
|
80
|
+
elsif strip_meta?(entry)
|
|
81
|
+
content ||= Textus::Protocol::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
82
|
+
bytes = Textus::Protocol::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
|
|
83
|
+
publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
|
|
84
|
+
else
|
|
85
|
+
target_abs = File.join(pctx.repo_root, t.to)
|
|
86
|
+
@publisher.publish(source: data_path, target: target_abs, store_root: pctx.root)
|
|
87
|
+
pctx.emit(:entry_published, key: entry.key, envelope: envelope, source: data_path, target: target_abs)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
{ kind: :built, value: { "key" => entry.key, "path" => data_path, "published_to" => targets.map(&:to) } }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def strip_meta?(entry)
|
|
97
|
+
%w[json yaml].include?(entry.format.to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_bytes(target, content, pctx)
|
|
101
|
+
boot = target.inject_boot ? Textus::Protocol::Boot.build(container: pctx.container) : nil
|
|
102
|
+
renderer = Textus::Protocol::Produce::Render.new(
|
|
103
|
+
template_loader: ->(n) { pctx.read_template(n) },
|
|
104
|
+
manifest: pctx.manifest,
|
|
105
|
+
source_publish_path: target.to,
|
|
106
|
+
entry_key: entry.key,
|
|
107
|
+
edge_store: pctx.container.store.link_store,
|
|
108
|
+
)
|
|
109
|
+
renderer.bytes_for(target: target, data: content, boot: boot)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def publish_bytes(bytes, key, target, pctx, data_path, envelope)
|
|
113
|
+
target_abs = File.join(pctx.repo_root, target.to)
|
|
114
|
+
Tempfile.create(["textus-publish", File.extname(target.to)]) do |f|
|
|
115
|
+
f.binmode
|
|
116
|
+
f.write(bytes)
|
|
117
|
+
f.flush
|
|
118
|
+
@publisher.publish(
|
|
119
|
+
source: f.path, target: target_abs, store_root: pctx.root, provenance_source: data_path,
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
pctx.emit(:entry_published, key: key, envelope: envelope, source: data_path, target: target_abs)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# publish.tree: mirror this entry's whole subtree to one target dir.
|
|
127
|
+
class Tree < Mode
|
|
128
|
+
def keyless? = true
|
|
129
|
+
|
|
130
|
+
def publish(pctx, prefix: nil)
|
|
131
|
+
target_rel = entry.publish_tree
|
|
132
|
+
target_dir = repo_abs(pctx, target_rel)
|
|
133
|
+
unless inside_repo?(pctx, target_dir)
|
|
134
|
+
raise Textus::PublishError.new(
|
|
135
|
+
"entry '#{entry.key}': publish_tree target '#{target_rel}' escapes repo root",
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
result = SubtreeMirror.new(entry, pctx).mirror(
|
|
140
|
+
walk_root: store_base(pctx),
|
|
141
|
+
target_dir: target_dir,
|
|
142
|
+
key: entry.key,
|
|
143
|
+
envelope: nil,
|
|
144
|
+
)
|
|
145
|
+
{ kind: :leaves, value: result[:written], pruned: result[:pruned] }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate!
|
|
149
|
+
publish_tree = entry.publish_tree
|
|
150
|
+
raise UsageError.new("entry '#{entry.key}': publish.tree must be a string") unless publish_tree.is_a?(String)
|
|
151
|
+
|
|
152
|
+
used_vars = publish_tree.scan(/\{([a-z]+)\}/).flatten
|
|
153
|
+
return if used_vars.empty?
|
|
154
|
+
|
|
155
|
+
raise UsageError.new(
|
|
156
|
+
"entry '#{entry.key}': publish.tree names a single directory and takes no template variable(s) " \
|
|
157
|
+
"#{used_vars.map { |v| "{#{v}}" }.join(", ")} — it mirrors the whole subtree to one target dir.",
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
class SubtreeMirror
|
|
163
|
+
def initialize(entry, pctx, publisher: Textus::Infra::Port::Publisher.new)
|
|
164
|
+
@entry = entry
|
|
165
|
+
@pctx = pctx
|
|
166
|
+
@publisher = publisher
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def mirror(walk_root:, target_dir:, key:, envelope:)
|
|
170
|
+
return { written: [], pruned: [] } unless File.directory?(walk_root)
|
|
171
|
+
|
|
172
|
+
written = publish_files(walk_root: walk_root, target_dir: target_dir, key: key, envelope: envelope)
|
|
173
|
+
{ written: written, pruned: prune(target_dir, written) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def publish_files(walk_root:, target_dir:, key:, envelope:)
|
|
179
|
+
Dir.glob(File.join(walk_root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |src|
|
|
180
|
+
next nil unless File.file?(src)
|
|
181
|
+
|
|
182
|
+
dst = File.join(target_dir, relative(src, walk_root))
|
|
183
|
+
@publisher.publish(source: src, target: dst, store_root: @pctx.root)
|
|
184
|
+
@pctx.emit(:entry_published, key: key, envelope: envelope, source: src, target: dst)
|
|
185
|
+
{ "key" => key, "source" => src, "target" => dst }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def prune(target_dir, written)
|
|
190
|
+
kept = written.map { |w| File.expand_path(w["target"]) }
|
|
191
|
+
store = Textus::Infra::Port::SentinelStore.new
|
|
192
|
+
store.targets_under(target_dir, @pctx.root).filter_map do |managed|
|
|
193
|
+
abs = File.expand_path(managed)
|
|
194
|
+
next nil if kept.include?(abs)
|
|
195
|
+
|
|
196
|
+
@publisher.unpublish(target: managed, store_root: @pctx.root)
|
|
197
|
+
managed
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def relative(path, root)
|
|
202
|
+
path.sub(%r{\A#{Regexp.escape(root)}/}, "")
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Policy
|
|
5
|
+
# One publish destination (ADR 0094). Exactly one of:
|
|
6
|
+
# to-target { to:, template:?, inject_boot:? } — render data through a
|
|
7
|
+
# template, or copy verbatim when no template
|
|
8
|
+
# tree-target { tree: } — ADR 0052 subtree mirror
|
|
9
|
+
# Provenance is NOT a publish flag — it lives in the data's `_meta`.
|
|
10
|
+
class PublishTarget
|
|
11
|
+
attr_reader :to, :tree, :template, :inject_boot
|
|
12
|
+
|
|
13
|
+
def initialize(raw)
|
|
14
|
+
if raw.key?("provenance")
|
|
15
|
+
raise Textus::BadManifest.new("publish `provenance:` was removed (ADR 0094): provenance lives in the data's `_meta`")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@to = raw["to"]
|
|
19
|
+
@tree = raw["tree"]
|
|
20
|
+
raise Textus::BadManifest.new("a publish target needs exactly one of `to:` or `tree:`") unless @to.nil? ^ @tree.nil?
|
|
21
|
+
|
|
22
|
+
@template = raw["template"]
|
|
23
|
+
@inject_boot = raw["inject_boot"] == true
|
|
24
|
+
return unless tree_target? && (@template || @inject_boot)
|
|
25
|
+
|
|
26
|
+
raise Textus::BadManifest.new("a tree target takes no template/inject_boot (ADR 0094)")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_target? = !@to.nil?
|
|
30
|
+
def tree_target? = !@tree.nil?
|
|
31
|
+
def renders? = to_target? && !@template.nil?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Policy
|
|
5
|
+
PROPOSALS_WRITERS = %w[human agent].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(data)
|
|
8
|
+
@data = data
|
|
9
|
+
@entries = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_writer :entries
|
|
13
|
+
|
|
14
|
+
# ── Authorization ──
|
|
15
|
+
|
|
16
|
+
def allowed?(role:, lane:)
|
|
17
|
+
writers_for(lane).map(&:to_sym).include?(role.to_sym)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def allowed!(role:, lane:, key: nil)
|
|
21
|
+
return true if allowed?(role:, lane:)
|
|
22
|
+
|
|
23
|
+
raise Textus::WriteForbidden.new(key, lane, holders: writers_for(lane))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def writers_for(lane_name)
|
|
27
|
+
return PROPOSALS_WRITERS if lane_name == "proposals"
|
|
28
|
+
|
|
29
|
+
lane_map = Textus::Protocol::V4::LANES[lane_name]
|
|
30
|
+
lane_map ? Array(lane_map[:writers]) : []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def actor_for(lane_name)
|
|
34
|
+
writers_for(lane_name).first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def proposer_role
|
|
38
|
+
proposers = writers_for("proposals")
|
|
39
|
+
authors = writers_for("knowledge")
|
|
40
|
+
(proposers - authors).first || proposers.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ── Existing methods (unchanged) ──
|
|
44
|
+
|
|
45
|
+
def derived_entry?(key)
|
|
46
|
+
entry = Array(@entries).find { |e| e.key == key }
|
|
47
|
+
entry && entry.lane == "artifacts"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def queue_lane
|
|
51
|
+
"scratchpad.proposals"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def machine_lane
|
|
55
|
+
"artifacts"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def queue_lane?(lane_name)
|
|
59
|
+
lane_name == "scratchpad"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def propose_lane_for(role)
|
|
63
|
+
proposers = writers_for("proposals")
|
|
64
|
+
proposers.include?(role) ? "scratchpad.proposals" : nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|