textus 0.55.1 → 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 +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -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/{gate → protocol}/binder.rb +2 -10
- 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/protocol/freshness/ttl_evaluator.rb +61 -0
- 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/geometry.rb → protocol/layout.rb} +32 -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 +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- 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 +26 -62
- 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/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- 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/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- 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 -61
- 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 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/core/sentinel.rb +0 -30
- 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/notebook_sources.rb +0 -50
- 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/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 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- 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/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/meta.rb +0 -54
- 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_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- 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 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- 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.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- 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,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
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Resolver
|
|
5
|
+
Resolution = ::Data.define(:entry, :path, :remaining)
|
|
6
|
+
|
|
7
|
+
MAX_DISTANCE_LEN = 200
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@data = data
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(key)
|
|
14
|
+
@data.validate_key!(key)
|
|
15
|
+
dr = Protocol::Key.resolve(key, @data.entries)
|
|
16
|
+
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr
|
|
17
|
+
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr.entry
|
|
18
|
+
|
|
19
|
+
Resolution.new(entry: dr.entry, path: build_path(dr.entry, dr.remaining), remaining: dr.remaining)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def suggestions_for(key)
|
|
23
|
+
candidates = enumerate.map { |r| r[:key] }
|
|
24
|
+
candidates.concat(@data.entries.reject { |e| nested_entry?(e) }.map(&:key))
|
|
25
|
+
candidates.uniq!
|
|
26
|
+
suggest(key, candidates, limit: 5)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def enumerate(prefix: nil, include_keyless: false)
|
|
32
|
+
out = @data.entries.flat_map do |entry|
|
|
33
|
+
nested_entry?(entry) ? enumerate_nested(entry, include_keyless: include_keyless) : enumerate_leaf(entry)
|
|
34
|
+
end
|
|
35
|
+
out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
|
|
36
|
+
out.sort_by { |row| row[:key] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def suggest(requested, candidates, limit: 5)
|
|
42
|
+
return [] if requested.nil? || requested.empty?
|
|
43
|
+
|
|
44
|
+
scored = candidates.first(200).map do |k|
|
|
45
|
+
prefix = self.class.shared_prefix_segments(requested, k)
|
|
46
|
+
dist = self.class.levenshtein(requested, k) || Float::INFINITY
|
|
47
|
+
[k, prefix, dist]
|
|
48
|
+
end
|
|
49
|
+
scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def nested_entry?(entry)
|
|
53
|
+
entry.nested?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_path(entry, remaining)
|
|
57
|
+
if remaining.empty?
|
|
58
|
+
resolve_leaf_path(entry)
|
|
59
|
+
else
|
|
60
|
+
fmt = Textus::Protocol::Format.for(entry.format)
|
|
61
|
+
base = Textus::Protocol::Key::Path.normalize_relative_path(entry.path)
|
|
62
|
+
path = File.join(@data.root, base, *remaining)
|
|
63
|
+
path + fmt.nested_ext(remaining.last)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def enumerate_leaf(entry)
|
|
68
|
+
fp = resolve_leaf_path(entry)
|
|
69
|
+
File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enumerate_nested(entry, include_keyless: false)
|
|
73
|
+
# publish_tree mirrors opaque payload by path — its files are never
|
|
74
|
+
# enumerated as keys (ADR 0047). Ask the resolved mode, not the path.
|
|
75
|
+
# The `include_keyless:` override is used only by the projection lister
|
|
76
|
+
# so that `from: project` selects can read source data from keyless
|
|
77
|
+
# nested entries (e.g. knowledge.evidence) without exposing them as
|
|
78
|
+
# addressable store keys in the public `list` surface.
|
|
79
|
+
return [] if entry.publish_mode.keyless? && !include_keyless
|
|
80
|
+
|
|
81
|
+
base = File.join(@data.root, Textus::Protocol::Key::Path.normalize_relative_path(entry.path))
|
|
82
|
+
return [] unless File.directory?(base)
|
|
83
|
+
|
|
84
|
+
Dir.glob(File.join(base, nested_glob(entry.format)))
|
|
85
|
+
.filter_map { |path| nested_row_for(entry, base, path) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def nested_row_for(entry, base, path)
|
|
89
|
+
rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
90
|
+
|
|
91
|
+
stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
|
|
92
|
+
segs = stripped.split("/").reject { |s| s.empty? || s == "." }
|
|
93
|
+
return nil if segs.empty?
|
|
94
|
+
|
|
95
|
+
illegal = segs.find { |s| !valid_segment?(s) }
|
|
96
|
+
if illegal
|
|
97
|
+
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — " \
|
|
98
|
+
"rename to match [a-z0-9][a-z0-9-]* (run 'textus doctor' for the full list)")
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
{ key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def valid_segment?(seg)
|
|
106
|
+
return false if seg.nil? || seg.empty?
|
|
107
|
+
return false if seg.length > Protocol::Key::Grammar::MAX_SEGMENT_LEN
|
|
108
|
+
|
|
109
|
+
seg.match?(Protocol::Key::Grammar::SEGMENT)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def resolve_leaf_path(entry)
|
|
113
|
+
Textus::Protocol::Key::Path.resolve(@data, entry)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def nested_glob(format)
|
|
117
|
+
Textus::Protocol::Format.for(format).nested_glob
|
|
118
|
+
end
|
|
119
|
+
class << self
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def shared_prefix_segments(left, right)
|
|
123
|
+
asegs = left.split(".")
|
|
124
|
+
bsegs = right.split(".")
|
|
125
|
+
n = [asegs.length, bsegs.length].min
|
|
126
|
+
i = 0
|
|
127
|
+
i += 1 while i < n && asegs[i] == bsegs[i]
|
|
128
|
+
i
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def levenshtein(left, right)
|
|
132
|
+
return nil if left.length > MAX_DISTANCE_LEN || right.length > MAX_DISTANCE_LEN
|
|
133
|
+
return right.length if left.empty?
|
|
134
|
+
return left.length if right.empty?
|
|
135
|
+
|
|
136
|
+
prev = (0..right.length).to_a
|
|
137
|
+
curr = Array.new(right.length + 1, 0)
|
|
138
|
+
(1..left.length).each do |i|
|
|
139
|
+
curr[0] = i
|
|
140
|
+
(1..right.length).each do |j|
|
|
141
|
+
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
142
|
+
curr[j] = [
|
|
143
|
+
curr[j - 1] + 1,
|
|
144
|
+
prev[j] + 1,
|
|
145
|
+
prev[j - 1] + cost,
|
|
146
|
+
].min
|
|
147
|
+
end
|
|
148
|
+
prev, curr = curr, prev
|
|
149
|
+
end
|
|
150
|
+
prev[right.length]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
module Schema
|
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
|
8
|
+
Contract = Dry::Schema.JSON do
|
|
9
|
+
optional(:lanes).value(:array).each do
|
|
10
|
+
hash do
|
|
11
|
+
required(:name).value(:string)
|
|
12
|
+
required(:kind).value(included_in?: LANE_KINDS)
|
|
13
|
+
optional(:owner).value(:string)
|
|
14
|
+
optional(:desc).value(:string)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
optional(:roles).value(:array).each do
|
|
19
|
+
hash do
|
|
20
|
+
required(:name).value(:string)
|
|
21
|
+
optional(:can).value(:array).each(:string)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
optional(:entries).value(:array).each do
|
|
26
|
+
hash do
|
|
27
|
+
required(:key).value(:string)
|
|
28
|
+
optional(:format).value(:string)
|
|
29
|
+
optional(:schema).maybe(:string)
|
|
30
|
+
optional(:tracked).value(:bool)
|
|
31
|
+
optional(:naming).value(:string)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
optional(:data).value(:hash)
|
|
36
|
+
|
|
37
|
+
optional(:audit).hash do
|
|
38
|
+
optional(:max_size).value(:integer)
|
|
39
|
+
optional(:keep).value(:integer)
|
|
40
|
+
end
|
|
41
|
+
optional(:worker).hash do
|
|
42
|
+
optional(:pool).value(:integer)
|
|
43
|
+
optional(:poll).value(:integer)
|
|
44
|
+
optional(:lease_ttl).value(:integer)
|
|
45
|
+
optional(:max_attempts).value(:integer)
|
|
46
|
+
end
|
|
47
|
+
optional(:version).value(:string)
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/BlockLength
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
module Schema
|
|
7
|
+
# Orchestrates structural validation (dry-schema Contract) then cross-field
|
|
8
|
+
# semantic checks (Semantics). Public interface unchanged: Validator.validate!(raw).
|
|
9
|
+
module Validator
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def validate!(raw)
|
|
13
|
+
raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
# Root unknown-key check before Contract so it fires even when lanes: is empty.
|
|
16
|
+
Schema.walk(raw, ROOT_KEYS, "$")
|
|
17
|
+
|
|
18
|
+
result = Contract.call(raw)
|
|
19
|
+
raise BadManifest.new(format_first_error(result.errors.messages)) unless result.success?
|
|
20
|
+
|
|
21
|
+
if raw.key?("data")
|
|
22
|
+
raise BadManifest.new("manifest must declare data:") if raw["data"].nil?
|
|
23
|
+
elsif Array(raw["lanes"]).empty?
|
|
24
|
+
raise BadManifest.new("manifest must declare lanes:")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Format the first dry-schema error to match the legacy path-prefixed style:
|
|
29
|
+
# "unknown key 'x' at '$.lanes[0]'" for extra-key errors;
|
|
30
|
+
# "manifest structure error at <path>: <msg>" for type/value errors.
|
|
31
|
+
def format_first_error(messages)
|
|
32
|
+
msg = messages.first
|
|
33
|
+
return "manifest structure error: unknown" unless msg
|
|
34
|
+
|
|
35
|
+
parent = format_path(msg.path[0..-2])
|
|
36
|
+
key = msg.path.last
|
|
37
|
+
|
|
38
|
+
if msg.text == "is not allowed"
|
|
39
|
+
"unknown key '#{key}' at '#{parent}'"
|
|
40
|
+
else
|
|
41
|
+
"manifest structure error at #{format_path(msg.path)}: #{msg.text}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_path(parts)
|
|
46
|
+
"$" + Array(parts).map { |p| p.is_a?(Integer) ? "[#{p}]" : ".#{p}" }.join
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
module Schema
|
|
5
|
+
# The closed coordination vocabulary (ADR 0028; five in 0033; unified in
|
|
6
|
+
# 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
|
|
7
|
+
# in ADR 0091). Each kind pairs with the capability that authorizes
|
|
8
|
+
# originating bytes in it.
|
|
9
|
+
LANES = {
|
|
10
|
+
"canon" => "author",
|
|
11
|
+
"workspace" => "keep",
|
|
12
|
+
"machine" => "converge",
|
|
13
|
+
"queue" => "propose",
|
|
14
|
+
"raw" => "ingest",
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
LANE_KINDS = LANES.keys.freeze
|
|
18
|
+
CAPABILITIES = LANES.values.uniq.freeze
|
|
19
|
+
KIND_REQUIRES_VERB = LANES
|
|
20
|
+
|
|
21
|
+
# Key whitelists and rule-field registry (ADR 0109).
|
|
22
|
+
ROOT_KEYS = %w[version roles owner lanes entries data audit worker].freeze
|
|
23
|
+
LANE_KEYS = %w[name kind owner desc].freeze
|
|
24
|
+
ENTRY_KEYS = %w[key schema format naming tracked].freeze
|
|
25
|
+
AUDIT_KEYS = %w[max_size keep].freeze
|
|
26
|
+
|
|
27
|
+
OWNER_SUBJECT_PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
28
|
+
|
|
29
|
+
# Public entry point — structural validation then semantic checks.
|
|
30
|
+
def self.validate!(raw) = Validator.validate!(raw)
|
|
31
|
+
|
|
32
|
+
def self.walk(raw, allowed, path)
|
|
33
|
+
raw.each_key do |k|
|
|
34
|
+
raise BadManifest.new("unknown key '#{k}' at '#{path}'") unless allowed.include?(k)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
Manifest = Data.define(:data, :resolver, :policy)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "manifest/schema"
|
|
10
|
+
require_relative "manifest/data"
|
|
11
|
+
require_relative "manifest/policy"
|
|
12
|
+
require_relative "manifest/resolver"
|
|
13
|
+
|
|
14
|
+
module Textus
|
|
15
|
+
module Protocol
|
|
16
|
+
class Manifest
|
|
17
|
+
class << self
|
|
18
|
+
def parse(yaml_text, root: ".")
|
|
19
|
+
raw = YAML.safe_load(yaml_text, aliases: true)
|
|
20
|
+
check_version!(raw, "<string>")
|
|
21
|
+
build(raw, root)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(root)
|
|
25
|
+
manifest_path = File.join(root, "manifest.yaml")
|
|
26
|
+
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
27
|
+
|
|
28
|
+
raw = YAML.safe_load_file(manifest_path, aliases: true)
|
|
29
|
+
check_version!(raw, manifest_path)
|
|
30
|
+
build(raw, root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build(raw, root)
|
|
36
|
+
# Phase 1: structural data + authority policy (no entry validation)
|
|
37
|
+
data = Manifest::Data.parse(raw, root: root)
|
|
38
|
+
policy = Manifest::Policy.new(data)
|
|
39
|
+
|
|
40
|
+
policy.entries = data.entries
|
|
41
|
+
|
|
42
|
+
new(
|
|
43
|
+
data:,
|
|
44
|
+
resolver: Manifest::Resolver.new(data),
|
|
45
|
+
policy:,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_version!(raw, source)
|
|
50
|
+
return if raw["version"] == PROTOCOL
|
|
51
|
+
|
|
52
|
+
raise BadFrontmatter.new(
|
|
53
|
+
source,
|
|
54
|
+
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Textus::Manifest = Textus::Protocol::Manifest
|