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
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
require "tempfile"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Manifest
|
|
5
|
-
class Entry
|
|
6
|
-
module Publish
|
|
7
|
-
# publish.to: render or copy the entry's stored data to each fixed repo path.
|
|
8
|
-
# The behaviour of any entry that declares `publish: [{ to: ... }, ...]`.
|
|
9
|
-
# ADR 0094: iterates publish_targets (to-targets), rendering through a
|
|
10
|
-
# template when the target declares one, or copying verbatim otherwise.
|
|
11
|
-
class ToPaths < Mode
|
|
12
|
-
def initialize(entry, publisher: Textus::Port::Publisher.new)
|
|
13
|
-
super(entry)
|
|
14
|
-
@publisher = publisher
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize
|
|
18
|
-
targets = entry.publish_targets.select(&:to_target?)
|
|
19
|
-
|
|
20
|
-
return nil if targets.empty?
|
|
21
|
-
|
|
22
|
-
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
23
|
-
envelope = pctx.reader.call(entry.key)
|
|
24
|
-
renderer = Textus::Produce::Render.new(template_loader: ->(n) { pctx.read_template(n) })
|
|
25
|
-
content = nil # parsed lazily; the data's `content` (always _meta-free)
|
|
26
|
-
|
|
27
|
-
targets.each do |t|
|
|
28
|
-
if t.renders?
|
|
29
|
-
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
30
|
-
publish_bytes(render_bytes(t, content, renderer, pctx), entry.key, t, pctx, data_path, envelope)
|
|
31
|
-
elsif strip_meta?(entry)
|
|
32
|
-
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
33
|
-
bytes = Textus::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
|
|
34
|
-
publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
|
|
35
|
-
else
|
|
36
|
-
# opaque / command / non-structured — publish the stored file as-is
|
|
37
|
-
target_abs = File.join(pctx.repo_root, t.to)
|
|
38
|
-
@publisher.publish(source: data_path, target: target_abs, store_root: pctx.root)
|
|
39
|
-
pctx.emit(:entry_published, key: entry.key, envelope: envelope, source: data_path, target: target_abs)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{ kind: :built, value: { "key" => entry.key, "path" => data_path, "published_to" => targets.map(&:to) } }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def strip_meta?(entry)
|
|
49
|
-
%w[json yaml].include?(entry.format.to_s)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def render_bytes(target, content, renderer, pctx)
|
|
53
|
-
boot = target.inject_boot ? Textus::Boot.build(container: pctx.container) : nil
|
|
54
|
-
renderer.bytes_for(target: target, data: content, boot: boot)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Write bytes to a system temp, publish (recording the persistent data
|
|
58
|
-
# file as the sentinel source), then remove the temp — the store is
|
|
59
|
-
# never polluted with render artifacts.
|
|
60
|
-
def publish_bytes(bytes, key, target, pctx, data_path, envelope)
|
|
61
|
-
target_abs = File.join(pctx.repo_root, target.to)
|
|
62
|
-
Tempfile.create(["textus-publish", File.extname(target.to)]) do |f|
|
|
63
|
-
f.binmode
|
|
64
|
-
f.write(bytes)
|
|
65
|
-
f.flush
|
|
66
|
-
@publisher.publish(
|
|
67
|
-
source: f.path, target: target_abs, store_root: pctx.root, provenance_source: data_path,
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
pctx.emit(:entry_published, key: key, envelope: envelope, source: data_path, target: target_abs)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# publish_tree (ADR 0047): mirror this entry's whole subtree to one
|
|
6
|
-
# target dir by real path. No resolver, no keys — files are opaque
|
|
7
|
-
# payload (envelope nil). The prune honors `ignore` so a derived index
|
|
8
|
-
# (e.g. a SKILL.md written by a separate entry into the same dir)
|
|
9
|
-
# survives the whole-target prune (ADR 0047 D4).
|
|
10
|
-
class Tree < Mode
|
|
11
|
-
# Mirrored files are opaque payload, never addressable keys (ADR 0047).
|
|
12
|
-
def keyless? = true
|
|
13
|
-
|
|
14
|
-
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
-
target_rel = entry.publish_tree
|
|
16
|
-
target_dir = repo_abs(pctx, target_rel)
|
|
17
|
-
unless inside_repo?(pctx, target_dir)
|
|
18
|
-
raise Textus::PublishError.new(
|
|
19
|
-
"entry '#{entry.key}': publish_tree target '#{target_rel}' escapes repo root",
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
result = SubtreeMirror.new(entry, pctx).mirror(
|
|
24
|
-
base: store_base(pctx),
|
|
25
|
-
walk_root: store_base(pctx),
|
|
26
|
-
target_dir: target_dir,
|
|
27
|
-
key: entry.key,
|
|
28
|
-
envelope: nil,
|
|
29
|
-
prune_honors_ignore: true,
|
|
30
|
-
)
|
|
31
|
-
{ kind: :leaves, value: result[:written], pruned: result[:pruned] }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def validate!
|
|
35
|
-
publish_tree = entry.publish_tree
|
|
36
|
-
raise UsageError.new("entry '#{entry.key}': publish.tree must be a string") unless publish_tree.is_a?(String)
|
|
37
|
-
|
|
38
|
-
used_vars = publish_tree.scan(Template::VAR_RE).flatten
|
|
39
|
-
return if used_vars.empty?
|
|
40
|
-
|
|
41
|
-
raise UsageError.new(
|
|
42
|
-
"entry '#{entry.key}': publish.tree names a single directory and takes no template variable(s) " \
|
|
43
|
-
"#{used_vars.map { |v| "{#{v}}" }.join(", ")} — it mirrors the whole subtree to one target dir.",
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
# ADR 0049: the publish design is a key-split concept (ADR 0047 table)
|
|
5
|
-
# realized as one resolved sum type. Each directory entry resolves, once,
|
|
6
|
-
# to one Publish::* mode that owns its publish algorithm — no nil-cascade,
|
|
7
|
-
# no pairwise exclusivity guards, one shared subtree mirror. ADR 0051
|
|
8
|
-
# removed `publish_each` (both leaf modes); ADR 0052 folded the two surviving
|
|
9
|
-
# keys into one `publish:` block (`to:` xor `tree:`). The surface is two modes:
|
|
10
|
-
#
|
|
11
|
-
# None — nothing to publish (no publish: block)
|
|
12
|
-
# ToPaths — publish: { to: [...] } — 1 stored file -> N fixed repo paths
|
|
13
|
-
# Tree — publish: { tree: "dir" } — whole entry subtree -> 1 dir, no keys
|
|
14
|
-
module Publish
|
|
15
|
-
# Resolve an entry to its single publish mode. The publish config is the
|
|
16
|
-
# ADR 0052 `publish:` block, sourced into entry.publish_to/publish_tree.
|
|
17
|
-
# Raises one UsageError if both `publish.to` and `publish.tree` are set —
|
|
18
|
-
# the block groups the two but does not make exclusivity structural, so
|
|
19
|
-
# this stays the one enforcement point (ADR 0052 D2).
|
|
20
|
-
def self.resolve(entry)
|
|
21
|
-
set = []
|
|
22
|
-
set << "publish.to" unless Array(entry.publish_to).empty?
|
|
23
|
-
set << "publish.tree" unless entry.publish_tree.nil?
|
|
24
|
-
|
|
25
|
-
if set.length > 1
|
|
26
|
-
raise Textus::UsageError.new(
|
|
27
|
-
"entry '#{entry.key}': #{set.join(" and ")} are mutually exclusive — an entry publishes exactly one way",
|
|
28
|
-
)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
mode_for(entry, set.first)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.mode_for(entry, key)
|
|
35
|
-
case key
|
|
36
|
-
when "publish.to" then ToPaths.new(entry)
|
|
37
|
-
when "publish.tree" then Tree.new(entry)
|
|
38
|
-
else None.new(entry)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
private_class_method :mode_for
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
module FormatMatrix
|
|
6
|
-
def self.call(entry, policy:) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
|
-
begin
|
|
8
|
-
Textus::Format.for(entry.format).validate_path_extension(entry.path, entry.nested?)
|
|
9
|
-
rescue UsageError => e
|
|
10
|
-
raise UsageError.new("entry '#{entry.key}': #{e.message}")
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
return unless entry.format == "text" && !entry.schema.nil?
|
|
14
|
-
|
|
15
|
-
raise UsageError.new("entry '#{entry.key}': text format must not declare a schema")
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
# Validates the per-entry `ignore:` field (ADR 0042): a list of
|
|
6
|
-
# non-empty glob strings, allowed only on nested entries.
|
|
7
|
-
module Ignore
|
|
8
|
-
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
9
|
-
patterns = entry.raw["ignore"]
|
|
10
|
-
return if patterns.nil?
|
|
11
|
-
|
|
12
|
-
raise UsageError.new("entry '#{entry.key}': ignore requires nested: true") unless entry.nested?
|
|
13
|
-
|
|
14
|
-
raise UsageError.new("entry '#{entry.key}': ignore must be a list of glob strings") unless patterns.is_a?(Array)
|
|
15
|
-
|
|
16
|
-
patterns.each do |pat|
|
|
17
|
-
next if pat.is_a?(String) && !pat.empty?
|
|
18
|
-
|
|
19
|
-
raise UsageError.new(
|
|
20
|
-
"entry '#{entry.key}': each ignore pattern must be a non-empty string (got #{pat.inspect})",
|
|
21
|
-
)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
# ADR 0049: one publish validator. Exclusivity among the publish keys is
|
|
6
|
-
# enforced structurally by Publish.resolve (reached via #publish_mode),
|
|
7
|
-
# and each mode's shape rules run *because that mode resolved* — replacing
|
|
8
|
-
# the scattered pairwise "not-both" guards of the old PublishEach +
|
|
9
|
-
# PublishTree validators. Misuse on a non-nested entry is still caught
|
|
10
|
-
# here from raw, since the typed attrs stub nil on Base. (publish_each was
|
|
11
|
-
# removed in 0.42.0 — ADR 0051; the publish_to/publish_tree keys were folded
|
|
12
|
-
# into the `publish:` block in 0.43.0 — ADR 0052; Schema rejects the retired
|
|
13
|
-
# flat keys at load.)
|
|
14
|
-
module Publish
|
|
15
|
-
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
16
|
-
unless entry.nested?
|
|
17
|
-
# ADR 0094: publish: is now a list; use publish_tree (derived reader)
|
|
18
|
-
# rather than raw.dig("publish", "tree") which breaks on an Array.
|
|
19
|
-
raise UsageError.new("entry '#{entry.key}': publish.tree requires nested: true") if entry.publish_tree
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
entry.publish_mode.validate!
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
REGISTERED = [
|
|
6
|
-
Publish,
|
|
7
|
-
Ignore,
|
|
8
|
-
FormatMatrix,
|
|
9
|
-
].freeze
|
|
10
|
-
|
|
11
|
-
def self.run_all(entry, policy:)
|
|
12
|
-
REGISTERED.each { |v| v.call(entry, policy: policy) }
|
|
13
|
-
nil
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Policy
|
|
4
|
-
module Matcher
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
|
-
def matches?(glob, key)
|
|
8
|
-
glob_segs = glob.split(".")
|
|
9
|
-
key_segs = key.split(".")
|
|
10
|
-
consume(glob_segs, key_segs)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def specificity(glob)
|
|
14
|
-
glob.split(".").reduce(0) do |s, seg|
|
|
15
|
-
s + case seg
|
|
16
|
-
when "**" then 0
|
|
17
|
-
when "*" then 1
|
|
18
|
-
else 10
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def pick_most_specific(globs, key:)
|
|
24
|
-
matching = globs.select { |g| matches?(g, key) }
|
|
25
|
-
return nil if matching.empty?
|
|
26
|
-
|
|
27
|
-
matching.max_by { |g| [specificity(g), -g.length, g] }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.consume(glob_segs, key_segs)
|
|
31
|
-
return key_segs.empty? if glob_segs.empty?
|
|
32
|
-
|
|
33
|
-
head = glob_segs.first
|
|
34
|
-
rest = glob_segs[1..]
|
|
35
|
-
|
|
36
|
-
if head == "**"
|
|
37
|
-
return true if rest.empty?
|
|
38
|
-
|
|
39
|
-
(0..key_segs.length).any? { |i| consume(rest, key_segs[i..]) }
|
|
40
|
-
elsif key_segs.empty?
|
|
41
|
-
false
|
|
42
|
-
elsif head == "*" || head == key_segs.first
|
|
43
|
-
consume(rest, key_segs[1..])
|
|
44
|
-
else
|
|
45
|
-
false
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Policy
|
|
4
|
-
# One publish destination (ADR 0094). Exactly one of:
|
|
5
|
-
# to-target { to:, template:?, inject_boot:? } — render data through a
|
|
6
|
-
# template, or copy verbatim when no template
|
|
7
|
-
# tree-target { tree: } — ADR 0052 subtree mirror
|
|
8
|
-
# Provenance is NOT a publish flag — it lives in the data's `_meta`.
|
|
9
|
-
class PublishTarget
|
|
10
|
-
attr_reader :to, :tree, :template, :inject_boot
|
|
11
|
-
|
|
12
|
-
def initialize(raw)
|
|
13
|
-
if raw.key?("provenance")
|
|
14
|
-
raise Textus::BadManifest.new("publish `provenance:` was removed (ADR 0094): provenance lives in the data's `_meta`")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
@to = raw["to"]
|
|
18
|
-
@tree = raw["tree"]
|
|
19
|
-
raise Textus::BadManifest.new("a publish target needs exactly one of `to:` or `tree:`") unless @to.nil? ^ @tree.nil?
|
|
20
|
-
|
|
21
|
-
@template = raw["template"]
|
|
22
|
-
@inject_boot = raw["inject_boot"] == true
|
|
23
|
-
return unless tree_target? && (@template || @inject_boot)
|
|
24
|
-
|
|
25
|
-
raise Textus::BadManifest.new("a tree target takes no template/inject_boot (ADR 0094)")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def to_target? = !@to.nil?
|
|
29
|
-
def tree_target? = !@tree.nil?
|
|
30
|
-
def renders? = to_target? && !@template.nil?
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Policy
|
|
4
|
-
class React
|
|
5
|
-
ALLOWED_KEYS = %w[on when do scope budget idempotency observe priority].freeze
|
|
6
|
-
|
|
7
|
-
attr_reader :raw
|
|
8
|
-
|
|
9
|
-
def initialize(raw:)
|
|
10
|
-
raise Textus::BadManifest.new("react: must be a map") unless raw.is_a?(Hash)
|
|
11
|
-
|
|
12
|
-
raw = raw.each_with_object({}) do |(key, value), out|
|
|
13
|
-
normalized = key == true ? "on" : key.to_s
|
|
14
|
-
out[normalized] = value
|
|
15
|
-
end
|
|
16
|
-
raise Textus::BadManifest.new("react.ttl is invalid; ttl belongs only to source.ttl or retention.ttl") if raw.key?("ttl")
|
|
17
|
-
|
|
18
|
-
unknown = raw.keys - ALLOWED_KEYS
|
|
19
|
-
raise Textus::BadManifest.new("react: unknown key(s): #{unknown.join(", ")}") unless unknown.empty?
|
|
20
|
-
|
|
21
|
-
@raw = raw
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to_h
|
|
25
|
-
@raw
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Policy
|
|
4
|
-
# Garbage collection (ADR 0093). A glob-matched rule slot: when an entry
|
|
5
|
-
# ages past `ttl`, retire it. Destructive only — runs on the full
|
|
6
|
-
# `converge` pass, never on a write (ADR 0079's invariant). Orthogonal to
|
|
7
|
-
# production (`source:`): an intake entry can re-pull hourly AND archive
|
|
8
|
-
# after 90 days. `warn`/`refresh` are gone (refresh is implied by an
|
|
9
|
-
# intake source; warn never fired after ADR 0089's pure-read get).
|
|
10
|
-
class Retention
|
|
11
|
-
ACTIONS = %i[drop archive].freeze
|
|
12
|
-
|
|
13
|
-
attr_reader :action
|
|
14
|
-
|
|
15
|
-
def initialize(raw)
|
|
16
|
-
@ttl = raw["ttl"] or
|
|
17
|
-
raise Textus::BadManifest.new("retention requires a 'ttl:'")
|
|
18
|
-
@action = (raw["action"] || "").to_s.to_sym
|
|
19
|
-
return if ACTIONS.include?(@action)
|
|
20
|
-
|
|
21
|
-
raise Textus::BadManifest.new("retention action must be one of #{ACTIONS.join("|")}, got #{raw["action"].inspect}")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def ttl_seconds = Textus::Core::Duration.seconds(@ttl)
|
|
25
|
-
def destructive? = true
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Policy
|
|
4
|
-
# An entry's external-generator declaration. `source:` now means ONLY
|
|
5
|
-
# `from: external` — out-of-band runner; textus never invokes it, only
|
|
6
|
-
# detects drift (doctor generator_drift check).
|
|
7
|
-
# `from: fetch` and `from: derive` are removed; those concerns now live
|
|
8
|
-
# in workflow files under .textus/workflows/.
|
|
9
|
-
class Source
|
|
10
|
-
attr_reader :command, :sources
|
|
11
|
-
|
|
12
|
-
def initialize(raw)
|
|
13
|
-
from = raw["from"].to_s
|
|
14
|
-
unless from == "external"
|
|
15
|
-
raise Textus::BadManifest.new(
|
|
16
|
-
"from: #{from} is removed — use a workflow file under .textus/workflows/ instead",
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
@command = raw["command"] or
|
|
21
|
-
raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
|
|
22
|
-
@sources = raw["sources"] || []
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def external? = true
|
|
26
|
-
def kind = :external
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
# Authority over lanes and roles derived from a Manifest::Data snapshot.
|
|
4
|
-
# Encapsulates the lookups previously living on Manifest itself
|
|
5
|
-
# (lane_writers, permission_for). Write authority is derived from
|
|
6
|
-
# capabilities x lane-kind (ADR 0030): each lane-kind requires one verb
|
|
7
|
-
# (Schema::KIND_REQUIRES_VERB) and a role may write a lane iff its caps
|
|
8
|
-
# include that verb (verb_for_lane, roles_with_capability). Derived /
|
|
9
|
-
# proposal-queue status is authoritative via the declared-kind family
|
|
10
|
-
# (declared_kind, derived_entry?, queue_lane?, queue_lane).
|
|
11
|
-
class Policy
|
|
12
|
-
def initialize(data)
|
|
13
|
-
@data = data
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# The capability a lane's kind requires to be written, or nil if the
|
|
17
|
-
# lane declares no kind. declared_kind returns a Symbol; the table is
|
|
18
|
-
# keyed by String.
|
|
19
|
-
def verb_for_lane(lane_name)
|
|
20
|
-
kind = declared_kind(lane_name)
|
|
21
|
-
kind && Schema::KIND_REQUIRES_VERB[kind.to_s]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Names of roles whose declared caps include `verb`.
|
|
25
|
-
def roles_with_capability(verb)
|
|
26
|
-
@data.role_caps.select { |_name, caps| caps.include?(verb) }.keys
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# The conventional automated proposer: a role that can propose but is not
|
|
30
|
-
# the author-anchor (so it resolves to `agent`, not `human`, under the
|
|
31
|
-
# default mapping). Falls back to the first proposer, then nil.
|
|
32
|
-
def proposer_role
|
|
33
|
-
proposers = roles_with_capability("propose")
|
|
34
|
-
(proposers - roles_with_capability("author")).first || proposers.first
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# The role textus acts AS for a system-initiated operation requiring
|
|
38
|
-
# `verb` (no human passed --as). Capability-derived — a role name that
|
|
39
|
-
# exists in the manifest, or nil. Never a hardcoded literal (ADR 0044).
|
|
40
|
-
def actor_for(verb)
|
|
41
|
-
roles_with_capability(verb).first
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# The kind declared on a lane in the manifest, or nil if undeclared.
|
|
45
|
-
def declared_kind(lane_name)
|
|
46
|
-
@data.declared_lane_kinds[lane_name]
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Lane names declaring `kind` (a Symbol), in manifest order. Lets callers
|
|
50
|
-
# (boot) name a kind's live lane instance(s) instead of hardcoding names.
|
|
51
|
-
def lanes_of_kind(kind)
|
|
52
|
-
@data.declared_lane_kinds.select { |_name, k| k == kind }.keys
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# The single lane declaring `kind: queue`, or nil. Schema guarantees <=1.
|
|
56
|
-
def queue_lane
|
|
57
|
-
@data.declared_lane_kinds.key(:queue)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# ADR 0091: derived-ness is a property of the ENTRY, not its lane (one
|
|
61
|
-
# machine lane holds both intake and derived entries). Resolve the entry
|
|
62
|
-
# and ask it directly. Returns false if entries are not yet built
|
|
63
|
-
# (validator phase during Data#initialize) — validators must not rely on
|
|
64
|
-
# cross-entry state during construction.
|
|
65
|
-
def derived_entry?(_key)
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# The single lane declaring kind: machine, or nil.
|
|
70
|
-
def machine_lane
|
|
71
|
-
@data.declared_lane_kinds.key(:machine)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# A lane is a proposal queue iff it declares kind: queue.
|
|
75
|
-
def queue_lane?(lane_name)
|
|
76
|
-
declared_kind(lane_name) == :queue
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# The lane a proposer role writes proposals into: the single lane that
|
|
80
|
-
# declares kind: queue, when the role can write it. Returns nil if there
|
|
81
|
-
# is no queue lane or the role cannot write it.
|
|
82
|
-
def propose_lane_for(role)
|
|
83
|
-
return nil if role.nil?
|
|
84
|
-
|
|
85
|
-
q = queue_lane
|
|
86
|
-
return nil unless q
|
|
87
|
-
|
|
88
|
-
q_verb = verb_for_lane(q)
|
|
89
|
-
return nil unless roles_with_capability(q_verb).include?(role)
|
|
90
|
-
|
|
91
|
-
q
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Resolver
|
|
4
|
-
Resolution = ::Data.define(:entry, :path, :remaining)
|
|
5
|
-
|
|
6
|
-
def initialize(data)
|
|
7
|
-
@data = data
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def resolve(key)
|
|
11
|
-
@data.validate_key!(key)
|
|
12
|
-
segments = key.split(".")
|
|
13
|
-
candidates = @data.entries
|
|
14
|
-
.map { |e| [e, e.key.split(".")] }
|
|
15
|
-
.select { |(_, esegs)| esegs == segments[0, esegs.length] }
|
|
16
|
-
.sort_by { |(_, esegs)| -esegs.length }
|
|
17
|
-
raise UnknownKey.new(key, suggestions: suggestions_for(key)) if candidates.empty?
|
|
18
|
-
|
|
19
|
-
entry, esegs = candidates.first
|
|
20
|
-
remaining = segments[esegs.length..]
|
|
21
|
-
build_resolution(entry, remaining, key)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def suggestions_for(key)
|
|
25
|
-
candidates = enumerate.map { |r| r[:key] }
|
|
26
|
-
candidates.concat(@data.entries.reject { |e| nested_entry?(e) }.map(&:key))
|
|
27
|
-
candidates.uniq!
|
|
28
|
-
Key::Distance.suggest(key, candidates, limit: 5)
|
|
29
|
-
rescue StandardError
|
|
30
|
-
[]
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def enumerate(prefix: nil, include_keyless: false)
|
|
34
|
-
out = @data.entries.flat_map do |entry|
|
|
35
|
-
nested_entry?(entry) ? enumerate_nested(entry, include_keyless: include_keyless) : enumerate_leaf(entry)
|
|
36
|
-
end
|
|
37
|
-
out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
|
|
38
|
-
out.sort_by { |row| row[:key] }
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
# Returns true for entries that behave as nested (Nested subclass, or any
|
|
44
|
-
# entry with nested: true in the raw YAML — e.g. Intake entries covering
|
|
45
|
-
# a directory of leaf files).
|
|
46
|
-
def nested_entry?(entry)
|
|
47
|
-
entry.nested?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def build_resolution(entry, remaining, key)
|
|
51
|
-
if remaining.empty?
|
|
52
|
-
Resolution.new(entry: entry, path: resolve_leaf_path(entry), remaining: [])
|
|
53
|
-
else
|
|
54
|
-
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless nested_entry?(entry)
|
|
55
|
-
|
|
56
|
-
primary_ext = Textus::Format.for(entry.format).extensions.first
|
|
57
|
-
base = Textus::Key::Path.normalize_relative_path(entry.path)
|
|
58
|
-
path = File.join(@data.root, base, *remaining) + primary_ext
|
|
59
|
-
Resolution.new(entry: entry, path: path, remaining: remaining)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def enumerate_leaf(entry)
|
|
64
|
-
fp = resolve_leaf_path(entry)
|
|
65
|
-
File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def enumerate_nested(entry, include_keyless: false)
|
|
69
|
-
# publish_tree mirrors opaque payload by path — its files are never
|
|
70
|
-
# enumerated as keys (ADR 0047). Ask the resolved mode, not the path.
|
|
71
|
-
# The `include_keyless:` override is used only by the projection lister
|
|
72
|
-
# so that `from: project` selects can read source data from keyless
|
|
73
|
-
# nested entries (e.g. knowledge.decisions) without exposing them as
|
|
74
|
-
# addressable store keys in the public `list` surface.
|
|
75
|
-
return [] if entry.publish_mode.keyless? && !include_keyless
|
|
76
|
-
|
|
77
|
-
base = File.join(@data.root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
78
|
-
return [] unless File.directory?(base)
|
|
79
|
-
|
|
80
|
-
Dir.glob(File.join(base, nested_glob(entry.format)))
|
|
81
|
-
.filter_map { |path| nested_row_for(entry, base, path) }
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def nested_row_for(entry, base, path)
|
|
85
|
-
rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
86
|
-
return nil if entry.ignored?(rel)
|
|
87
|
-
|
|
88
|
-
stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
|
|
89
|
-
segs = stripped.split("/").reject { |s| s.empty? || s == "." }
|
|
90
|
-
return nil if segs.empty?
|
|
91
|
-
|
|
92
|
-
illegal = segs.find { |s| !valid_segment?(s) }
|
|
93
|
-
if illegal
|
|
94
|
-
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — " \
|
|
95
|
-
"rename to match [a-z0-9][a-z0-9-]* (run 'textus doctor' for the full list)")
|
|
96
|
-
return nil
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
{ key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def valid_segment?(seg)
|
|
103
|
-
return false if seg.nil? || seg.empty?
|
|
104
|
-
return false if seg.length > Key::Grammar::MAX_SEGMENT_LEN
|
|
105
|
-
|
|
106
|
-
seg.match?(Key::Grammar::SEGMENT)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def resolve_leaf_path(entry)
|
|
110
|
-
Textus::Key::Path.resolve(@data, entry)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def nested_glob(format)
|
|
114
|
-
Textus::Format.for(format).nested_glob
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|