textus 0.55.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -2768
- data/README.md +8 -9
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +0 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{dispatch → protocol}/binder.rb +1 -7
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store → protocol}/layout.rb +16 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +22 -21
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +2 -7
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +7 -7
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +12 -12
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/server.rb +35 -24
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -53
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -16
- metadata +169 -185
- data/SPEC.md +0 -909
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -141
- data/lib/textus/boot.rb +0 -181
- data/lib/textus/dispatch/contracts.rb +0 -63
- data/lib/textus/dispatch/handler_registry.rb +0 -21
- data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
- data/lib/textus/dispatch/middleware/auth.rb +0 -40
- data/lib/textus/dispatch/middleware/base.rb +0 -26
- data/lib/textus/dispatch/middleware/binder.rb +0 -20
- data/lib/textus/dispatch/middleware/cascade.rb +0 -53
- data/lib/textus/dispatch/pipeline.rb +0 -35
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -43
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -77
- data/lib/textus/format/json.rb +0 -81
- data/lib/textus/format/markdown.rb +0 -59
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -104
- data/lib/textus/format.rb +0 -61
- data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
- data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
- data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
- data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
- data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
- data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
- data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
- data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
- data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
- data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
- data/lib/textus/handlers/read/audit_entries.rb +0 -48
- data/lib/textus/handlers/read/blame_entry.rb +0 -71
- data/lib/textus/handlers/read/deps_entry.rb +0 -17
- data/lib/textus/handlers/read/get_entry.rb +0 -68
- data/lib/textus/handlers/read/list_keys.rb +0 -36
- data/lib/textus/handlers/read/pulse_entries.rb +0 -66
- data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
- data/lib/textus/handlers/read/uid_entry.rb +0 -18
- data/lib/textus/handlers/read/where_entry.rb +0 -18
- data/lib/textus/handlers/write/accept_proposal.rb +0 -39
- data/lib/textus/handlers/write/data_mv.rb +0 -55
- data/lib/textus/handlers/write/delete_key.rb +0 -17
- data/lib/textus/handlers/write/enqueue_job.rb +0 -27
- data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
- data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
- data/lib/textus/handlers/write/move_key.rb +0 -80
- data/lib/textus/handlers/write/propose_entry.rb +0 -29
- data/lib/textus/handlers/write/put_entry.rb +0 -29
- data/lib/textus/handlers/write/reject_proposal.rb +0 -29
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
- data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
- data/lib/textus/manifest/policy/predicates.rb +0 -54
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/orchestration.rb +0 -55
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel.rb +0 -25
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -49
- data/lib/textus/port/storage/interface.rb +0 -17
- data/lib/textus/port/store.rb +0 -149
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -40
- data/lib/textus/produce/publisher.rb +0 -21
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/registry.rb +0 -42
- data/lib/textus/schema/tools.rb +0 -108
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/store/container.rb +0 -173
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/entry/reader.rb +0 -47
- data/lib/textus/store/entry/writer.rb +0 -219
- data/lib/textus/store/envelope/meta.rb +0 -61
- data/lib/textus/store/freshness/drift_detector.rb +0 -93
- data/lib/textus/store/freshness/evaluator.rb +0 -20
- data/lib/textus/store/freshness/verdict.rb +0 -24
- data/lib/textus/store/freshness.rb +0 -8
- data/lib/textus/store/index/builder.rb +0 -76
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -124
- data/lib/textus/store/jobs/queue.rb +0 -162
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention/base.rb +0 -52
- data/lib/textus/store/jobs/retention/sweep.rb +0 -55
- data/lib/textus/store/jobs/retention.rb +0 -8
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store.rb +0 -122
- data/lib/textus/surface/cli/verb/doctor.rb +0 -20
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp/projector.rb +0 -20
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -90
- data/lib/textus/value/result.rb +0 -46
- data/lib/textus/verb_registry.rb +0 -417
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
class Script < Base
|
|
5
|
+
EXTENSIONS = %w[.rb .py .sh .js .pl .rs .exs].freeze
|
|
6
|
+
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
|
|
11
|
+
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.serialize(meta:, body:, content: nil)
|
|
15
|
+
_ = meta
|
|
16
|
+
_ = content
|
|
17
|
+
b = body.to_s
|
|
18
|
+
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
19
|
+
b
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.extensions = EXTENSIONS
|
|
23
|
+
def self.nested_glob = "**/*"
|
|
24
|
+
def self.nested_ext(*) = ""
|
|
25
|
+
def self.enforce_name_match!(_path, _meta); end
|
|
26
|
+
|
|
27
|
+
def self.rewrite_name(_path, _basename)
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.validate_path_extension(path, nested)
|
|
32
|
+
ext = File.extname(path)
|
|
33
|
+
if nested
|
|
34
|
+
return if ext == ""
|
|
35
|
+
|
|
36
|
+
raise UsageError.new("nested script path must not have an extension")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return if EXTENSIONS.include?(ext) || ext == ""
|
|
40
|
+
|
|
41
|
+
raise UsageError.new("script format requires #{EXTENSIONS.join(" | ")} extension (got #{ext.inspect})")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.validate_raw_entry!(_parsed, lane)
|
|
45
|
+
nil unless lane == "scratchpad"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.data_to_payload(data)
|
|
49
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
50
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
class Text < Base
|
|
5
|
+
def self.parse(raw, path: nil)
|
|
6
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
7
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
8
|
+
|
|
9
|
+
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.serialize(meta:, body:, content: nil)
|
|
13
|
+
_ = meta
|
|
14
|
+
_ = content
|
|
15
|
+
b = body.to_s
|
|
16
|
+
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
17
|
+
b
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.extensions = [".txt"]
|
|
21
|
+
def self.nested_glob = "**/*.txt"
|
|
22
|
+
def self.enforce_name_match!(_path, _meta); end
|
|
23
|
+
|
|
24
|
+
def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.validate_path_extension(path, nested)
|
|
29
|
+
ext = File.extname(path)
|
|
30
|
+
if nested
|
|
31
|
+
return if ext == ""
|
|
32
|
+
|
|
33
|
+
raise UsageError.new("nested text path must not have an extension")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if [".txt", ""].include?(ext)
|
|
37
|
+
|
|
38
|
+
raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.data_to_payload(data)
|
|
42
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
43
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Format
|
|
6
|
+
class Yaml < Base
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
|
|
13
|
+
rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
|
|
14
|
+
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
15
|
+
end
|
|
16
|
+
raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
meta = parsed["_meta"]
|
|
19
|
+
fm = meta.is_a?(Hash) ? meta : {}
|
|
20
|
+
content_without_meta = parsed.except("_meta")
|
|
21
|
+
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.serialize(meta:, body:, content: nil)
|
|
25
|
+
if content.is_a?(Hash)
|
|
26
|
+
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
27
|
+
::YAML.dump(on_disk).sub(/\A---\n/, "")
|
|
28
|
+
elsif body && !body.to_s.empty?
|
|
29
|
+
b = body.to_s
|
|
30
|
+
b += "\n" unless b.end_with?("\n")
|
|
31
|
+
b
|
|
32
|
+
else
|
|
33
|
+
raise UsageError.new("yaml serialize requires :content or :body")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.validate_against(schema, parsed)
|
|
38
|
+
schema.validate!(parsed["content"] || {})
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
RAW_REQUIRED = %w[ingested_at content_hash].freeze
|
|
42
|
+
RAW_SOURCE_KINDS = %w[url file asset].freeze
|
|
43
|
+
|
|
44
|
+
def self.validate_raw_entry!(parsed, lane)
|
|
45
|
+
return unless lane == "raw"
|
|
46
|
+
|
|
47
|
+
content = parsed["content"] || {}
|
|
48
|
+
return if content["superseded_by"]
|
|
49
|
+
|
|
50
|
+
missing = RAW_REQUIRED.reject { |f| content[f] }
|
|
51
|
+
raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
|
|
52
|
+
|
|
53
|
+
source = content["source"] || {}
|
|
54
|
+
kind = source["kind"]
|
|
55
|
+
unless RAW_SOURCE_KINDS.include?(kind)
|
|
56
|
+
raise Textus::BadContent.new(
|
|
57
|
+
nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return unless kind == "url" && !source["url"]
|
|
62
|
+
|
|
63
|
+
raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.extensions = [".yaml", ".yml"]
|
|
67
|
+
def self.nested_glob = "**/*.{yaml,yml}"
|
|
68
|
+
|
|
69
|
+
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
70
|
+
raise UsageError.new("put for yaml requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
71
|
+
|
|
72
|
+
if content.nil?
|
|
73
|
+
begin
|
|
74
|
+
parsed = parse(body.to_s, path: path)
|
|
75
|
+
rescue BadFrontmatter => e
|
|
76
|
+
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
79
|
+
else
|
|
80
|
+
bytes = serialize(meta: meta, body: "", content: content)
|
|
81
|
+
[bytes, meta, bytes, content]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.validate_path_extension(path, nested)
|
|
86
|
+
ext = File.extname(path)
|
|
87
|
+
if nested
|
|
88
|
+
return if ext == ""
|
|
89
|
+
|
|
90
|
+
raise UsageError.new("nested yaml path must not have an extension")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
return if [".yaml", ".yml"].include?(ext)
|
|
94
|
+
|
|
95
|
+
raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.data_to_payload(data)
|
|
99
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
100
|
+
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
SEP = "---".freeze
|
|
5
|
+
|
|
6
|
+
STRATEGIES = {
|
|
7
|
+
"markdown" => -> { Protocol::Format::Markdown },
|
|
8
|
+
"json" => -> { Protocol::Format::Json },
|
|
9
|
+
"yaml" => -> { Protocol::Format::Yaml },
|
|
10
|
+
"text" => -> { Protocol::Format::Text },
|
|
11
|
+
"script" => -> { Protocol::Format::Script },
|
|
12
|
+
"bash" => -> { Protocol::Format::Bash },
|
|
13
|
+
"ruby" => -> { Protocol::Format::Ruby },
|
|
14
|
+
"python" => -> { Protocol::Format::Python },
|
|
15
|
+
"javascript" => -> { Protocol::Format::Javascript },
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
@registry = nil
|
|
19
|
+
|
|
20
|
+
def self.registry=(reg)
|
|
21
|
+
@registry = reg
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.registry
|
|
25
|
+
@registry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
EXT_TO_FORMAT = {
|
|
29
|
+
".md" => "markdown",
|
|
30
|
+
".json" => "json",
|
|
31
|
+
".yaml" => "yaml",
|
|
32
|
+
".yml" => "yaml",
|
|
33
|
+
".txt" => "text",
|
|
34
|
+
".rb" => "ruby",
|
|
35
|
+
".py" => "python",
|
|
36
|
+
".sh" => "bash",
|
|
37
|
+
".js" => "javascript",
|
|
38
|
+
".pl" => "script",
|
|
39
|
+
".rs" => "script",
|
|
40
|
+
".exs" => "script",
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def self.for(format)
|
|
44
|
+
key = format.to_s
|
|
45
|
+
return registry.fetch(key).call if registry&.key?(key)
|
|
46
|
+
|
|
47
|
+
STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.infer_from_extension(ext)
|
|
51
|
+
EXT_TO_FORMAT[ext]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.formats
|
|
55
|
+
EXT_TO_FORMAT.values.uniq
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.parse(raw, path: nil, format: "markdown")
|
|
59
|
+
Protocol::Format.for(format).parse(raw, path: path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
|
|
63
|
+
Protocol::Format.for(format).serialize(meta: meta, body: body, content: content)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.data_to_payload(data, format)
|
|
67
|
+
return { meta: {}, body: "", content: nil } if data.nil?
|
|
68
|
+
|
|
69
|
+
Protocol::Format.for(format).data_to_payload(data)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
|
|
4
|
+
module Protocol
|
|
5
5
|
module Freshness
|
|
6
6
|
class TtlEvaluator
|
|
7
7
|
def initialize(manifest:, file_stat:, clock:)
|
|
@@ -11,7 +11,7 @@ module Textus
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def verdict(mentry)
|
|
14
|
-
ttl =
|
|
14
|
+
ttl = lane_ttl(mentry.lane)
|
|
15
15
|
return fresh if ttl.nil?
|
|
16
16
|
|
|
17
17
|
stale = age_stale?(file_basis(mentry), ttl)
|
|
@@ -33,11 +33,15 @@ module Textus
|
|
|
33
33
|
@file_stat.mtime(path)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def lane_ttl(lane)
|
|
37
|
+
Textus::Protocol::V4::LANES.dig(lane, :retention_ttl)
|
|
38
|
+
end
|
|
39
|
+
|
|
36
40
|
def due?(mentry, prefix:, lane:)
|
|
37
41
|
return false if lane && mentry.lane != lane
|
|
38
42
|
return false if prefix && !mentry.key.start_with?(prefix)
|
|
39
43
|
|
|
40
|
-
ttl =
|
|
44
|
+
ttl = lane_ttl(mentry.lane)
|
|
41
45
|
return false if ttl.nil?
|
|
42
46
|
|
|
43
47
|
path = @manifest.resolver.resolve(mentry.key).path
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Gate
|
|
4
|
+
HANDLER_GROUPS = {
|
|
5
|
+
read: Handlers::Read,
|
|
6
|
+
write: Handlers::Write,
|
|
7
|
+
maintenance: Handlers::System,
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(manifest:, resolver:, container:, store_engine:, policy: nil, async: nil)
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
@resolver = resolver
|
|
13
|
+
@container = container
|
|
14
|
+
@store_engine = store_engine
|
|
15
|
+
@async = async
|
|
16
|
+
@policy = policy || manifest.policy
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dispatch(role, verb, params = {}, call: nil)
|
|
20
|
+
spec = VerbRegistry.for(verb)
|
|
21
|
+
return error(:unknown_verb, "Unknown verb: #{verb}") unless spec
|
|
22
|
+
|
|
23
|
+
key = params[:key]
|
|
24
|
+
authorize!(role, verb, key, spec)
|
|
25
|
+
|
|
26
|
+
call_obj = call || Value::Call.build(role:, correlation_id: SecureRandom.uuid)
|
|
27
|
+
bound = Protocol::Binder.bind(spec, params)
|
|
28
|
+
ctx_class = spec.read? && !spec.system? ? Handlers::QueryContext : Handlers::CommandContext
|
|
29
|
+
ctx = ctx_class.new(@store_engine, @container, call_obj)
|
|
30
|
+
|
|
31
|
+
handler = spec.lane ? Lane.handler_for(spec.lane) : handler_group(spec)
|
|
32
|
+
handler.public_send(verb, **bound, ctx:, call: call_obj)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def handler_group(spec)
|
|
38
|
+
return Handlers::System if spec.system?
|
|
39
|
+
|
|
40
|
+
HANDLER_GROUPS[spec.category] || Handlers::System
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def authorize!(role, _verb, key, spec)
|
|
44
|
+
return true unless spec&.write?
|
|
45
|
+
|
|
46
|
+
key_lane = key&.split(".")&.first
|
|
47
|
+
lane = key_lane || spec.lane&.to_s
|
|
48
|
+
return true unless lane
|
|
49
|
+
|
|
50
|
+
lane_map = Textus::Protocol::V4::LANES[lane]
|
|
51
|
+
return true unless lane_map
|
|
52
|
+
|
|
53
|
+
@policy.allowed!(role:, lane:, key:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def error(code, message)
|
|
57
|
+
{ error: { code: code.to_s, message: } }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
class CommandContext < QueryContext
|
|
5
|
+
# StoreEngine delegation (write side)
|
|
6
|
+
def put(...) = @store_engine.put(...)
|
|
7
|
+
def delete(...) = @store_engine.delete(...)
|
|
8
|
+
def move(...) = @store_engine.move(...)
|
|
9
|
+
|
|
10
|
+
# Infrastructure (ops, drain, pulse)
|
|
11
|
+
def audit_log = @container.store.audit_log
|
|
12
|
+
def workflow_queue = @container.store.workflow_queue
|
|
13
|
+
def file_system = @container.store.file_system
|
|
14
|
+
def workflows = @container.workflows
|
|
15
|
+
def workflow_registry = @container.workflow_registry
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
module Ops
|
|
5
|
+
class AuditFilter
|
|
6
|
+
FILTER_KEYS = %i[key lane role verb since seq_since correlation_id limit].freeze
|
|
7
|
+
|
|
8
|
+
def call(ctx:, filters: {})
|
|
9
|
+
seq = filters[:seq_since]
|
|
10
|
+
if seq
|
|
11
|
+
min = ctx.audit_log.min_available
|
|
12
|
+
raise Textus::CursorExpired.new(requested: seq, min_available: min) if min && seq < min - 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rows = ctx.audit_log.list(seq_since: filters[:seq_since], key: filters[:key], role: filters[:role], verb: filters[:verb],
|
|
16
|
+
correlation_id: filters[:correlation_id], limit: filters[:limit]).select do |row|
|
|
17
|
+
next false if filters[:lane] && !key_in_lane?(row["key"], filters[:lane], ctx)
|
|
18
|
+
next false if filters[:since] && row["ts"] && Time.parse(row["ts"]) < filters[:since]
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
{ "verb" => "audit", "rows" => rows }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def key_in_lane?(key, lane_name, ctx)
|
|
28
|
+
mentry = ctx.mentry_for(key:)
|
|
29
|
+
mentry && mentry.lane == lane_name
|
|
30
|
+
rescue Textus::Error
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
module Ops
|
|
5
|
+
class BulkPlanner
|
|
6
|
+
Plan = Data.define(:steps, :warnings) do
|
|
7
|
+
def to_h
|
|
8
|
+
{ "steps" => steps, "warnings" => warnings }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def plan_mv(from_prefix:, to_prefix:, ctx:)
|
|
13
|
+
raise Textus::UsageError.new("from_prefix and to_prefix required") unless from_prefix && to_prefix
|
|
14
|
+
|
|
15
|
+
leaves = ctx.list(prefix: from_prefix)
|
|
16
|
+
raise Textus::UsageError.new("from_prefix is a leaf — use mv") if leaves.any? { |r| r["key"] == from_prefix }
|
|
17
|
+
|
|
18
|
+
warnings = leaves.empty? ? ["no keys under #{from_prefix}"] : []
|
|
19
|
+
steps = leaves.map do |row|
|
|
20
|
+
tail = row["key"].delete_prefix("#{from_prefix}.")
|
|
21
|
+
{ "op" => "mv", "from" => row["key"], "to" => "#{to_prefix}.#{tail}" }
|
|
22
|
+
end
|
|
23
|
+
Plan.new(steps:, warnings:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def plan_delete(prefix:, ctx:)
|
|
27
|
+
raise Textus::UsageError.new("prefix required") unless prefix && !prefix.empty?
|
|
28
|
+
|
|
29
|
+
leaves = ctx.list(prefix:)
|
|
30
|
+
warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
|
|
31
|
+
steps = leaves.map { |row| { "op" => "delete", "key" => row["key"] } }
|
|
32
|
+
Plan.new(steps:, warnings:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Handlers
|
|
6
|
+
module Ops
|
|
7
|
+
class DataMv
|
|
8
|
+
Plan = Data.define(:steps, :warnings) do
|
|
9
|
+
def to_h
|
|
10
|
+
{ "steps" => steps, "warnings" => warnings }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(from:, to:, ctx:, dry_run: false)
|
|
15
|
+
raise Textus::UsageError.new("from and to required") unless from && to
|
|
16
|
+
raise Textus::UsageError.new("data lane '#{from}' not declared") unless ctx.manifest.data.declared_lane_kinds.key?(from)
|
|
17
|
+
|
|
18
|
+
dest_dir = ctx.layout.lane_path(to)
|
|
19
|
+
raise Textus::UsageError.new("destination data/#{to} already exists") if File.exist?(dest_dir)
|
|
20
|
+
|
|
21
|
+
affected_keys = ctx.manifest.data.entries.select { |e| e.lane == from }.map(&:key)
|
|
22
|
+
steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
|
|
23
|
+
steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
|
|
24
|
+
plan = Plan.new(steps:, warnings: [])
|
|
25
|
+
return plan.to_h if dry_run
|
|
26
|
+
|
|
27
|
+
rewrite_manifest!(from:, to:, ctx:)
|
|
28
|
+
FileUtils.mv(ctx.layout.lane_path(from), dest_dir)
|
|
29
|
+
plan.to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def rewrite_manifest!(from:, to:, ctx:)
|
|
35
|
+
path = ctx.layout.manifest_path
|
|
36
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
37
|
+
raw["lanes"].each { |lane| lane["name"] = to if lane["name"] == from }
|
|
38
|
+
raw["entries"].each do |entry|
|
|
39
|
+
entry["lane"] = to if entry["lane"] == from
|
|
40
|
+
entry["key"] = entry["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
|
|
41
|
+
entry["path"] = entry["path"].sub(%r{\A(data/)?#{Regexp.escape(from)}(/|\z)}, "\\1#{to}\\2")
|
|
42
|
+
end
|
|
43
|
+
File.write(path, YAML.dump(raw))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Handlers
|
|
6
|
+
module Ops
|
|
7
|
+
class GitBlame
|
|
8
|
+
def call(key:, ctx:)
|
|
9
|
+
root = ctx.manifest.data.root
|
|
10
|
+
path = resolve_blame_path(key, ctx)
|
|
11
|
+
return nil unless path && git_tracked?(path, root:)
|
|
12
|
+
|
|
13
|
+
audit_rows = ctx.audit_log.list(key:)
|
|
14
|
+
audit_rows.to_h { |row| [row["ts"], git_commit_at(path, timestamp: row["ts"], root:)] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_blame_path(key, ctx)
|
|
20
|
+
res = ctx.resolve(key:)
|
|
21
|
+
res.path || Textus::Protocol::Key::Path.resolve(ctx.manifest.data, res.entry)
|
|
22
|
+
rescue Textus::Error
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def git_tracked?(path, root:)
|
|
27
|
+
return false if path.nil? || !File.exist?(path) || !git_repo?(root)
|
|
28
|
+
|
|
29
|
+
_out, _err, status = Open3.capture3("git", "ls-files", "--error-unmatch", path, chdir: root)
|
|
30
|
+
status.success?
|
|
31
|
+
rescue Errno::ENOENT
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def git_repo?(root)
|
|
36
|
+
dir = root
|
|
37
|
+
loop do
|
|
38
|
+
return true if File.directory?(File.join(dir, ".git"))
|
|
39
|
+
|
|
40
|
+
parent = File.dirname(dir)
|
|
41
|
+
return false if parent == dir
|
|
42
|
+
|
|
43
|
+
dir = parent
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def git_commit_at(path, timestamp:, root:)
|
|
48
|
+
args = ["git", "log", "-1"]
|
|
49
|
+
args << "--before=#{timestamp}" if timestamp
|
|
50
|
+
args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
|
|
51
|
+
out, _err, status = Open3.capture3(*args, chdir: root)
|
|
52
|
+
return nil unless status.success?
|
|
53
|
+
|
|
54
|
+
sha, author, date, subject = out.strip.split("\t", 4)
|
|
55
|
+
return nil if sha.nil? || sha.empty?
|
|
56
|
+
|
|
57
|
+
{ "sha" => sha, "author" => author, "date" => date, "subject" => subject }
|
|
58
|
+
rescue Errno::ENOENT
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
module Ops
|
|
5
|
+
class PulseCoordinator
|
|
6
|
+
def call(role:, ctx:, since: nil)
|
|
7
|
+
cursor_val = since || ctx.cursor_store.get(role)[:seq]
|
|
8
|
+
audit_filter = AuditFilter.new
|
|
9
|
+
audit_result = audit_filter.call(ctx:, filters: { seq_since: cursor_val })
|
|
10
|
+
changed = audit_result["rows"] || []
|
|
11
|
+
queue = ctx.manifest.policy.queue_lane
|
|
12
|
+
pending = queue ? ctx.list(lane: queue) : []
|
|
13
|
+
runner = Textus::Lanes::Knowledge::Runner.new(ctx.manifest, cursor_store: ctx.cursor_store)
|
|
14
|
+
result = {
|
|
15
|
+
"cursor" => ctx.audit_log.latest[:seq],
|
|
16
|
+
"changed" => changed,
|
|
17
|
+
"pending_review" => pending.is_a?(Array) ? pending.map { |r| r["key"] } : [],
|
|
18
|
+
"contract_etag" => Textus::Value::Etag.for_contract(ctx.layout),
|
|
19
|
+
"index_etag" => pulse_index_etag(ctx),
|
|
20
|
+
"loop_state" => runner.current_position(role),
|
|
21
|
+
}
|
|
22
|
+
ctx.cursor_store.set(role, result["cursor"])
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def pulse_index_etag(ctx)
|
|
29
|
+
ctx.etag_for("artifacts.system.index")
|
|
30
|
+
rescue Textus::Error
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
module Ops
|
|
5
|
+
class SourceExpander
|
|
6
|
+
def call(env, ctx:, depth: 0)
|
|
7
|
+
return env if depth >= 5
|
|
8
|
+
|
|
9
|
+
raw_sources = Array(env.meta["sources"])
|
|
10
|
+
return env if raw_sources.empty?
|
|
11
|
+
|
|
12
|
+
expanded = raw_sources.map { |src| expand_one(src, ctx:, depth:) }
|
|
13
|
+
env.with(sources: expanded)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def expand_one(src, ctx:, depth:)
|
|
19
|
+
src = { "key" => src } if src.is_a?(String)
|
|
20
|
+
return src unless src.is_a?(Hash) && src["key"].is_a?(String)
|
|
21
|
+
|
|
22
|
+
key = src["key"]
|
|
23
|
+
stored_etag = src["etag"]
|
|
24
|
+
res = ctx.resolve(key:)
|
|
25
|
+
path = res.path
|
|
26
|
+
current_etag = File.exist?(path) ? ctx.file_system.etag(path) : nil
|
|
27
|
+
suspended = stored_etag && current_etag ? stored_etag != current_etag : false
|
|
28
|
+
result = src.merge("suspended" => suspended)
|
|
29
|
+
child_env = ctx.read(key:)
|
|
30
|
+
if child_env
|
|
31
|
+
child_expanded = call(child_env, ctx:, depth: depth + 1)
|
|
32
|
+
child_sources = Array(child_expanded.sources)
|
|
33
|
+
result = result.merge("sources" => child_sources) unless child_sources.empty?
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|