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,35 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Key
|
|
4
|
+
module Grammar
|
|
5
|
+
SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
|
|
6
|
+
MAX_SEGMENTS = 8
|
|
7
|
+
MAX_SEGMENT_LEN = 64
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def validate!(key)
|
|
12
|
+
raise UsageError.new("key must be a String") unless key.is_a?(String)
|
|
13
|
+
raise UsageError.new("empty key") if key.empty?
|
|
14
|
+
|
|
15
|
+
segs = key.split(".")
|
|
16
|
+
raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
|
|
17
|
+
|
|
18
|
+
segs.each do |seg|
|
|
19
|
+
if seg.empty?
|
|
20
|
+
raise UsageError.new("empty segment in key '#{key}'")
|
|
21
|
+
elsif seg.length > MAX_SEGMENT_LEN
|
|
22
|
+
raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
|
|
23
|
+
elsif !seg.match?(SEGMENT)
|
|
24
|
+
raise UsageError.new(
|
|
25
|
+
"invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
|
|
26
|
+
"(lowercase, digits, hyphens; no underscores or uppercase)",
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Key
|
|
4
|
+
module Matching
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def matches_prefix?(key, prefix, nested: false)
|
|
8
|
+
return true if key == prefix || key.start_with?("#{prefix}.")
|
|
9
|
+
|
|
10
|
+
nested && prefix.start_with?("#{key}.")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def match?(pattern, key)
|
|
14
|
+
segs = pattern.split(".")
|
|
15
|
+
key_segs = key.split(".")
|
|
16
|
+
return false if segs.length != key_segs.length
|
|
17
|
+
|
|
18
|
+
segs.zip(key_segs).all? { |p, k| p == "*" || p == k }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Key
|
|
4
|
+
module Path
|
|
5
|
+
def self.resolve(data, mentry)
|
|
6
|
+
primary_ext = Textus::Protocol::Format.for(mentry.format).extensions.first
|
|
7
|
+
rel_path = normalize_relative_path(mentry.path)
|
|
8
|
+
if File.extname(mentry.path) == ""
|
|
9
|
+
File.join(data.root, rel_path + primary_ext)
|
|
10
|
+
else
|
|
11
|
+
File.join(data.root, rel_path)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.normalize_relative_path(path)
|
|
16
|
+
return path if path.start_with?("data/")
|
|
17
|
+
|
|
18
|
+
File.join("data", path)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Key
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
Resolution = ::Data.define(:entry, :remaining)
|
|
7
|
+
|
|
8
|
+
def resolve(key, entries)
|
|
9
|
+
entry = entries.find { |e| e.key == key }
|
|
10
|
+
return Resolution.new(entry:, remaining: []) if entry
|
|
11
|
+
|
|
12
|
+
candidates = entries.select { |e| e.nested? && key.start_with?("#{e.key}.") }
|
|
13
|
+
return nil if candidates.empty?
|
|
14
|
+
|
|
15
|
+
best = candidates.max_by { |e| e.key.length }
|
|
16
|
+
tail = key.delete_prefix("#{best.key}.")
|
|
17
|
+
Resolution.new(entry: best, remaining: tail.split("."))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def match?(pattern, key)
|
|
21
|
+
Matching.match?(pattern, key)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Lane
|
|
4
|
+
def self.handler_for(lane_name)
|
|
5
|
+
ns = lane_name.to_s.split("_").map(&:capitalize).join
|
|
6
|
+
Textus::Lanes.const_get(ns)::Handlers
|
|
7
|
+
rescue NameError
|
|
8
|
+
raise ArgumentError.new("no handler for lane: #{lane_name}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
class
|
|
4
|
-
RUN = ".state"
|
|
2
|
+
module Protocol
|
|
3
|
+
class Layout
|
|
5
4
|
DATA = "data"
|
|
6
5
|
ASSETS = "assets"
|
|
7
6
|
|
|
@@ -12,12 +11,11 @@ module Textus
|
|
|
12
11
|
|
|
13
12
|
attr_reader :root
|
|
14
13
|
|
|
15
|
-
# -- data paths --
|
|
16
14
|
def data_root = File.join(@root, DATA)
|
|
17
15
|
def lane_path(lane_name) = File.join(data_root, lane_name.to_s)
|
|
18
16
|
|
|
19
17
|
def entry_path(mentry)
|
|
20
|
-
primary_ext = Format.for(mentry.format).extensions.first
|
|
18
|
+
primary_ext = Textus::Protocol::Format.for(mentry.format).extensions.first
|
|
21
19
|
rel = normalize_relative_path(mentry.path)
|
|
22
20
|
if File.extname(mentry.path) == ""
|
|
23
21
|
File.join(@root, rel + primary_ext)
|
|
@@ -26,21 +24,32 @@ module Textus
|
|
|
26
24
|
end
|
|
27
25
|
end
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
def
|
|
31
|
-
def
|
|
32
|
-
def
|
|
33
|
-
def
|
|
34
|
-
def
|
|
35
|
-
def sentinels_root = File.join(
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
def db_root = File.join(@root, "db")
|
|
28
|
+
def db_path = File.join(db_root, "store.db")
|
|
29
|
+
def log_root = File.join(@root, "log")
|
|
30
|
+
def run_root = File.join(@root, "run")
|
|
31
|
+
def lock_path(name) = File.join(run_root, "locks", "#{name}.lock")
|
|
32
|
+
def track_root = File.join(@root, "track")
|
|
33
|
+
def sentinels_root = File.join(track_root, "sentinels")
|
|
34
|
+
def cursor_path(role) = File.join(track_root, "cursors", role.to_s)
|
|
35
|
+
|
|
36
|
+
def asset_raw_dir(date_path, zone)
|
|
37
|
+
File.join(@root, ASSETS, "raw", date_path, zone.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def asset_sentinel_path
|
|
41
|
+
File.join(@root, ASSETS, ".gitignore")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def asset_resolve(rel_path)
|
|
45
|
+
File.join(@root, ASSETS, rel_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def archive_path(source_path)
|
|
49
|
+
rel = source_path.delete_prefix("#{@root}/")
|
|
50
|
+
File.join(@root, "archive", rel)
|
|
41
51
|
end
|
|
42
52
|
|
|
43
|
-
# -- config paths --
|
|
44
53
|
def manifest_path = File.join(@root, "manifest.yaml")
|
|
45
54
|
def schemas_dir = File.join(@root, "schemas")
|
|
46
55
|
def schema_path(name) = File.join(schemas_dir, "#{name}.yaml")
|
|
@@ -49,10 +58,8 @@ module Textus
|
|
|
49
58
|
def hooks_dir = File.join(@root, "hooks")
|
|
50
59
|
def schemas_glob = File.join(schemas_dir, "**", "*")
|
|
51
60
|
|
|
52
|
-
# -- gitignore --
|
|
53
61
|
def gitignore_body(untracked_entries: [])
|
|
54
|
-
lines = ["# textus runtime artifacts — safe to delete, never commit",
|
|
55
|
-
"#{RUN}/"]
|
|
62
|
+
lines = ["# textus runtime artifacts — safe to delete, never commit", *runtime_ignore_dirs]
|
|
56
63
|
unless untracked_entries.empty?
|
|
57
64
|
lines << "# tracked:false entries — protocol-readable, not committed"
|
|
58
65
|
lines.concat(untracked_entries)
|
|
@@ -60,7 +67,6 @@ module Textus
|
|
|
60
67
|
"#{lines.join("\n")}\n"
|
|
61
68
|
end
|
|
62
69
|
|
|
63
|
-
# -- lane boundary (replaces Writer#zone_floor) --
|
|
64
70
|
def lane_floor(path)
|
|
65
71
|
prefix = "#{data_root}/"
|
|
66
72
|
return nil unless path.start_with?(prefix)
|
|
@@ -71,6 +77,10 @@ module Textus
|
|
|
71
77
|
|
|
72
78
|
private
|
|
73
79
|
|
|
80
|
+
def runtime_ignore_dirs
|
|
81
|
+
["db/", "log/", "run/", "track/", ".state/"]
|
|
82
|
+
end
|
|
83
|
+
|
|
74
84
|
def normalize_relative_path(path)
|
|
75
85
|
return path if path.start_with?("data/")
|
|
76
86
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Links
|
|
6
|
+
class Resolver
|
|
7
|
+
class UnknownKeyError < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(manifest:)
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve(key:, from_path:)
|
|
15
|
+
entry = @manifest.data.entries.find { |e| e.key == key }
|
|
16
|
+
raise UnknownKeyError.new("unknown key: #{key}") unless entry
|
|
17
|
+
|
|
18
|
+
to = Array(entry.publish_to).first
|
|
19
|
+
return "`textus get #{key}`" if to.nil?
|
|
20
|
+
|
|
21
|
+
relative_path(from: from_path, to: to)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def relative_path(from:, to:)
|
|
27
|
+
from_dir = Pathname.new(from).dirname
|
|
28
|
+
Pathname.new(to).relative_path_from(from_dir).to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Links
|
|
4
|
+
class UriRewriter
|
|
5
|
+
TEXTUS_URI = /\[([^\]]*)\]\(textus:([^#)\s]+)(#[^)\s]*)?\)/
|
|
6
|
+
|
|
7
|
+
def initialize(resolver:, from_path:, from_key: nil, edge_store: nil)
|
|
8
|
+
@resolver = resolver
|
|
9
|
+
@from_path = from_path
|
|
10
|
+
@from_key = from_key
|
|
11
|
+
@edge_store = edge_store
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rewrite(content)
|
|
15
|
+
content.gsub(TEXTUS_URI) do
|
|
16
|
+
text = Regexp.last_match(1)
|
|
17
|
+
key = Regexp.last_match(2)
|
|
18
|
+
anchor = Regexp.last_match(3).to_s
|
|
19
|
+
|
|
20
|
+
resolved = @resolver.resolve(key: key, from_path: @from_path)
|
|
21
|
+
@edge_store&.record(from_key: @from_key, to_key: key)
|
|
22
|
+
"[#{text}](#{resolved}#{anchor})"
|
|
23
|
+
rescue Resolver::UnknownKeyError
|
|
24
|
+
"[#{text}](`textus get #{key}`)"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -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
|