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
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Markdown < Base
|
|
6
|
-
def self.parse(raw, path: nil)
|
|
7
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
8
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
9
|
-
return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
|
|
10
|
-
|
|
11
|
-
lines = raw.split(/\r?\n/, -1)
|
|
12
|
-
close_idx = lines[1..].index("---")
|
|
13
|
-
raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
|
|
14
|
-
|
|
15
|
-
close_idx += 1
|
|
16
|
-
fm_yaml = lines[1...close_idx].join("\n")
|
|
17
|
-
body = lines[(close_idx + 1)..].join("\n")
|
|
18
|
-
begin
|
|
19
|
-
fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
|
|
20
|
-
rescue Psych::SyntaxError => e
|
|
21
|
-
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
22
|
-
end
|
|
23
|
-
fm = {} unless fm.is_a?(Hash)
|
|
24
|
-
{ "_meta" => fm, "body" => body, "content" => nil }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.serialize(meta:, body:, content: nil)
|
|
28
|
-
_ = content
|
|
29
|
-
fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
|
|
30
|
-
body = body.to_s
|
|
31
|
-
body += "\n" unless body.empty? || body.end_with?("\n")
|
|
32
|
-
"---\n#{fm_yaml}---\n#{body}"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.extensions = [".md"]
|
|
36
|
-
|
|
37
|
-
def self.nested_glob = "**/*.md"
|
|
38
|
-
|
|
39
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
40
|
-
_ = path
|
|
41
|
-
_ = content
|
|
42
|
-
bytes = serialize(meta: meta || {}, body: body.to_s)
|
|
43
|
-
[bytes, meta, body.to_s, nil]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.validate_path_extension(path, _nested)
|
|
47
|
-
ext = File.extname(path)
|
|
48
|
-
return if ["", ".md"].include?(ext)
|
|
49
|
-
|
|
50
|
-
raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.data_to_payload(data)
|
|
54
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
55
|
-
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
data/lib/textus/format/text.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
class Text < Base
|
|
4
|
-
def self.parse(raw, path: nil)
|
|
5
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
6
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
7
|
-
|
|
8
|
-
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.serialize(meta:, body:, content: nil)
|
|
12
|
-
_ = meta
|
|
13
|
-
_ = content
|
|
14
|
-
b = body.to_s
|
|
15
|
-
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
16
|
-
b
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.extensions = [".txt"]
|
|
20
|
-
|
|
21
|
-
def self.nested_glob = "**/*.txt"
|
|
22
|
-
|
|
23
|
-
def self.enforce_name_match!(_path, _meta); end
|
|
24
|
-
|
|
25
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
26
|
-
_ = path
|
|
27
|
-
_ = content
|
|
28
|
-
bytes = serialize(meta: meta || {}, body: body.to_s)
|
|
29
|
-
[bytes, meta, body.to_s, nil]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
|
|
33
|
-
false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.validate_path_extension(path, nested)
|
|
37
|
-
ext = File.extname(path)
|
|
38
|
-
if nested
|
|
39
|
-
return if ext == ""
|
|
40
|
-
|
|
41
|
-
raise UsageError.new("nested text path must not have an extension")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
return if [".txt", ""].include?(ext)
|
|
45
|
-
|
|
46
|
-
raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.data_to_payload(data)
|
|
50
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
51
|
-
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
data/lib/textus/format/yaml.rb
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Yaml < Base
|
|
6
|
-
def self.parse(raw, path: nil)
|
|
7
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
8
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
9
|
-
|
|
10
|
-
begin
|
|
11
|
-
parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
|
|
12
|
-
rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
|
|
13
|
-
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
14
|
-
end
|
|
15
|
-
raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
meta = parsed["_meta"]
|
|
18
|
-
fm = meta.is_a?(Hash) ? meta : {}
|
|
19
|
-
content_without_meta = parsed.except("_meta")
|
|
20
|
-
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.serialize(meta:, body:, content: nil)
|
|
24
|
-
if content.is_a?(Hash)
|
|
25
|
-
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
26
|
-
::YAML.dump(on_disk).sub(/\A---\n/, "")
|
|
27
|
-
elsif body && !body.to_s.empty?
|
|
28
|
-
b = body.to_s
|
|
29
|
-
b += "\n" unless b.end_with?("\n")
|
|
30
|
-
b
|
|
31
|
-
else
|
|
32
|
-
raise UsageError.new("yaml serialize requires :content or :body")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.validate_against(schema, parsed)
|
|
37
|
-
schema.validate!(parsed["content"] || {})
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
RAW_REQUIRED = %w[ingested_at content_hash].freeze
|
|
41
|
-
RAW_SOURCE_KINDS = %w[url file asset].freeze
|
|
42
|
-
|
|
43
|
-
def self.validate_raw_entry!(parsed, lane)
|
|
44
|
-
return unless lane == "raw"
|
|
45
|
-
|
|
46
|
-
content = parsed["content"] || {}
|
|
47
|
-
return if content["superseded_by"]
|
|
48
|
-
|
|
49
|
-
missing = RAW_REQUIRED.reject { |f| content[f] }
|
|
50
|
-
raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
|
|
51
|
-
|
|
52
|
-
source = content["source"] || {}
|
|
53
|
-
kind = source["kind"]
|
|
54
|
-
unless RAW_SOURCE_KINDS.include?(kind)
|
|
55
|
-
raise Textus::BadContent.new(
|
|
56
|
-
nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return unless kind == "url" && !source["url"]
|
|
61
|
-
|
|
62
|
-
raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.extensions = [".yaml", ".yml"]
|
|
66
|
-
|
|
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
|
data/lib/textus/format.rb
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
SEP = "---".freeze
|
|
4
|
-
|
|
5
|
-
STRATEGIES = {
|
|
6
|
-
"markdown" => -> { Format::Markdown },
|
|
7
|
-
"json" => -> { Format::Json },
|
|
8
|
-
"yaml" => -> { Format::Yaml },
|
|
9
|
-
"text" => -> { Format::Text },
|
|
10
|
-
}.freeze
|
|
11
|
-
|
|
12
|
-
# Optional registry for injectable format strategies. Tests or app
|
|
13
|
-
# initializers can set Format.registry = { "custom" => -> { MyFormat }}
|
|
14
|
-
@registry = nil
|
|
15
|
-
|
|
16
|
-
def self.registry=(reg)
|
|
17
|
-
@registry = reg
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.registry
|
|
21
|
-
@registry
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
EXT_TO_FORMAT = {
|
|
25
|
-
".md" => "markdown",
|
|
26
|
-
".json" => "json",
|
|
27
|
-
".yaml" => "yaml",
|
|
28
|
-
".yml" => "yaml",
|
|
29
|
-
".txt" => "text",
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
def self.for(format)
|
|
33
|
-
key = format.to_s
|
|
34
|
-
return registry.fetch(key).call if registry&.key?(key)
|
|
35
|
-
|
|
36
|
-
STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def self.infer_from_extension(ext)
|
|
40
|
-
EXT_TO_FORMAT[ext]
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.formats
|
|
44
|
-
EXT_TO_FORMAT.values.uniq
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.parse(raw, path: nil, format: "markdown")
|
|
48
|
-
Format.for(format).parse(raw, path: path)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
|
|
52
|
-
Format.for(format).serialize(meta: meta, body: body, content: content)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.data_to_payload(data, format)
|
|
56
|
-
return { meta: {}, body: "", content: nil } if data.nil?
|
|
57
|
-
|
|
58
|
-
Format.for(format).data_to_payload(data)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class BootStore
|
|
5
|
-
def initialize(container:)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(_command, _call)
|
|
10
|
-
Value::Result.success(Textus::Boot.build(container: @container))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class DoctorStore
|
|
5
|
-
def initialize(container:)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, call)
|
|
10
|
-
Value::Result.success(Textus::Doctor.build(container: @container, checks: command.checks, role: call.role))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class DrainStore
|
|
5
|
-
def initialize(container:, job_store:)
|
|
6
|
-
@container = container
|
|
7
|
-
@job_store = job_store
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call(_command, call)
|
|
11
|
-
queue = Textus::Store::Jobs::Queue.new(store: @job_store)
|
|
12
|
-
Textus::Store::Jobs::Planner.seed(container: @container, queue: queue, role: call.role)
|
|
13
|
-
queue.reclaim(now: Textus::Port::Clock.new.now)
|
|
14
|
-
summary = Textus::Store::Jobs::Worker.for(container: @container, queue: queue).drain
|
|
15
|
-
Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => summary.failed.zero?,
|
|
16
|
-
"completed" => summary.completed, "failed" => summary.failed)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "date"
|
|
3
|
-
require "digest"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Handlers
|
|
7
|
-
module Maintenance
|
|
8
|
-
class IngestEntry
|
|
9
|
-
SOURCE_KINDS = %w[url file asset].freeze
|
|
10
|
-
CONTENT_HASH_ALGO = "sha256"
|
|
11
|
-
|
|
12
|
-
def initialize(container:)
|
|
13
|
-
@container = container
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(command, call)
|
|
17
|
-
unless SOURCE_KINDS.include?(command.kind)
|
|
18
|
-
return Value::Result.failure(:usage_error,
|
|
19
|
-
"ingest kind must be one of #{SOURCE_KINDS.join("|")}")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
case command.kind
|
|
23
|
-
when "url" then return Value::Result.failure(:usage_error, "ingest url requires url") unless command.url
|
|
24
|
-
when "file" then return Value::Result.failure(:usage_error, "ingest file requires path") unless command.path
|
|
25
|
-
when "asset"
|
|
26
|
-
return Value::Result.failure(:usage_error, "ingest asset requires path") unless command.path
|
|
27
|
-
return Value::Result.failure(:usage_error, "ingest asset requires lane") unless command.lane
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
now = Time.now.utc
|
|
31
|
-
key = derive_key(now, command.kind, command.slug)
|
|
32
|
-
content_hash = compute_content_hash(command)
|
|
33
|
-
mentry = @container.manifest.resolver.resolve(key).entry
|
|
34
|
-
ts = now.iso8601
|
|
35
|
-
|
|
36
|
-
structured = build_structured(ts, now, content_hash, command)
|
|
37
|
-
store = @container.job_store
|
|
38
|
-
index = Textus::Store::Index::Lookup.new(store:)
|
|
39
|
-
|
|
40
|
-
duplicate_key = find_duplicate(index, content_hash, command)
|
|
41
|
-
|
|
42
|
-
env = if duplicate_key && duplicate_key != key
|
|
43
|
-
supersede_entry(duplicate_key, key, structured, call, store, command)
|
|
44
|
-
else
|
|
45
|
-
write_entry(key, structured, mentry, call)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
rebuild_index(store)
|
|
49
|
-
Value::Result.success(env)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def derive_key(now, kind, slug)
|
|
55
|
-
date = now.strftime("%Y.%m.%d")
|
|
56
|
-
"raw.#{date}.#{kind}-#{slug}"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def compute_content_hash(command)
|
|
60
|
-
digest = Digest::SHA256.new
|
|
61
|
-
case command.kind
|
|
62
|
-
when "url" then digest.update(command.url)
|
|
63
|
-
when "file", "asset" then digest.file(command.path)
|
|
64
|
-
end
|
|
65
|
-
"#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def build_structured(timestamp, now, content_hash, command)
|
|
69
|
-
base = { "ingested_at" => timestamp, "content_hash" => content_hash }
|
|
70
|
-
case command.kind
|
|
71
|
-
when "url"
|
|
72
|
-
base.merge("source" => { "kind" => "url", "url" => command.url,
|
|
73
|
-
"label" => command.label || command.url }, "body" => nil)
|
|
74
|
-
when "file"
|
|
75
|
-
base.merge("source" => { "kind" => "file", "path" => command.path,
|
|
76
|
-
"label" => command.label || File.basename(command.path) },
|
|
77
|
-
"body" => File.read(command.path))
|
|
78
|
-
when "asset"
|
|
79
|
-
asset_rel = copy_asset(now, command.path, command.lane)
|
|
80
|
-
base.merge("source" => { "kind" => "asset",
|
|
81
|
-
"label" => command.label || File.basename(command.path) },
|
|
82
|
-
"asset" => asset_rel, "body" => nil)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def copy_asset(now, path, lane)
|
|
87
|
-
date_path = now.strftime("%Y/%m/%d")
|
|
88
|
-
filename = File.basename(path)
|
|
89
|
-
assets_dir = @container.layout.asset_raw_dir(date_path, lane)
|
|
90
|
-
FileUtils.mkdir_p(assets_dir)
|
|
91
|
-
FileUtils.cp(path, File.join(assets_dir, filename))
|
|
92
|
-
sentinel = @container.layout.asset_sentinel_path
|
|
93
|
-
File.write(sentinel, "*\n") unless File.exist?(sentinel)
|
|
94
|
-
"raw/#{date_path}/#{lane}/#{filename}"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def write_entry(key, structured, mentry, call)
|
|
98
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
99
|
-
writer.put(key, mentry: mentry,
|
|
100
|
-
payload: Textus::Value::Payload.new(meta: nil, body: nil, content: structured))
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def find_duplicate(index, content_hash, command)
|
|
104
|
-
dup = index.find_by_hash(content_hash)
|
|
105
|
-
return dup if dup
|
|
106
|
-
return unless command.kind == "url"
|
|
107
|
-
|
|
108
|
-
index.find_by_url(command.url)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def supersede_entry(old_key, new_key, structured, call, store, command)
|
|
112
|
-
old_mentry = @container.manifest.resolver.resolve(old_key).entry
|
|
113
|
-
reader = Store::Entry::Reader.from(container: @container)
|
|
114
|
-
old_env = reader.read(old_key)
|
|
115
|
-
old_content = old_env&.content || {}
|
|
116
|
-
tombstone = {}
|
|
117
|
-
%w[ingested_at].each { |k| tombstone[k] = old_content[k] if old_content.key?(k) }
|
|
118
|
-
source_kind = old_content.dig("source", "kind")
|
|
119
|
-
tombstone["source"] = { "kind" => source_kind } if source_kind
|
|
120
|
-
tombstone["superseded_by"] = new_key
|
|
121
|
-
|
|
122
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
123
|
-
writer.put(old_key, mentry: old_mentry,
|
|
124
|
-
payload: Textus::Value::Payload.new(meta: nil, body: nil, content: tombstone))
|
|
125
|
-
|
|
126
|
-
structured["supersedes"] = old_key
|
|
127
|
-
env = write_entry(new_key, structured,
|
|
128
|
-
@container.manifest.resolver.resolve(new_key).entry, call)
|
|
129
|
-
|
|
130
|
-
move_asset(old_content["asset"], command.lane) if command.kind == "asset" && old_content["asset"]
|
|
131
|
-
|
|
132
|
-
rebuild_index(store)
|
|
133
|
-
env
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def move_asset(old_rel, lane)
|
|
137
|
-
old_path = @container.layout.asset_resolve(old_rel)
|
|
138
|
-
return unless File.exist?(old_path)
|
|
139
|
-
|
|
140
|
-
now = Time.now.utc
|
|
141
|
-
date_path = now.strftime("%Y/%m/%d")
|
|
142
|
-
filename = File.basename(old_path)
|
|
143
|
-
new_dir = @container.layout.asset_raw_dir(date_path, lane)
|
|
144
|
-
new_path = File.join(new_dir, filename)
|
|
145
|
-
return if old_path == new_path
|
|
146
|
-
|
|
147
|
-
FileUtils.mkdir_p(new_dir)
|
|
148
|
-
FileUtils.mv(old_path, new_path)
|
|
149
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
|
150
|
-
warn "[textus ingest] could not move asset #{old_rel}: #{e.message}"
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def rebuild_index(store)
|
|
154
|
-
Textus::Store::Index::Builder.new(store:).rebuild!(resolver: @container.manifest.resolver)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class JobsAction
|
|
5
|
-
def initialize(job_store:)
|
|
6
|
-
@job_store = job_store
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, _call)
|
|
10
|
-
queue = Textus::Store::Jobs::Queue.new(store: @job_store)
|
|
11
|
-
case command.action
|
|
12
|
-
when "retry" then queue.retry_failed(command.job_id)
|
|
13
|
-
when "purge" then queue.purge(command.state)
|
|
14
|
-
end
|
|
15
|
-
Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => true,
|
|
16
|
-
"state" => command.state, "jobs" => queue.list(command.state))
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class PublishedEntries
|
|
5
|
-
def initialize(manifest:)
|
|
6
|
-
@manifest = manifest
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(_command, _call)
|
|
10
|
-
Value::Result.success(@manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
11
|
-
{ "key" => entry.key, "publish_to" => entry.publish_to }
|
|
12
|
-
end)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Maintenance
|
|
4
|
-
class RuleExplain
|
|
5
|
-
def initialize(manifest:)
|
|
6
|
-
@manifest = manifest
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, _call)
|
|
10
|
-
key = command.key
|
|
11
|
-
result = if command.detail
|
|
12
|
-
explain(key)
|
|
13
|
-
else
|
|
14
|
-
effective(key)
|
|
15
|
-
end
|
|
16
|
-
Value::Result.success(result)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
LEAN_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY
|
|
20
|
-
.select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
|
|
21
|
-
DETAIL_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY
|
|
22
|
-
.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
|
|
23
|
-
EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| Textus::Manifest::Schema::FIELD_REGISTRY[f][:policy_class] }.freeze
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def effective(key)
|
|
28
|
-
set = @manifest.rules.for(key)
|
|
29
|
-
LEAN_FIELDS.each_with_object({}) do |field, out|
|
|
30
|
-
value = set.public_send(field)
|
|
31
|
-
out[field.to_s] = lean_value(field, value) unless value.nil?
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def lean_value(field, value)
|
|
36
|
-
case field
|
|
37
|
-
when :retention then retention_hash(value, string_keys: true)
|
|
38
|
-
when :react then value.to_h
|
|
39
|
-
else value
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def explain(key)
|
|
44
|
-
matching = @manifest.rules.explain(key)
|
|
45
|
-
winners = @manifest.rules.for(key)
|
|
46
|
-
{
|
|
47
|
-
key: key,
|
|
48
|
-
matched_blocks: matching.map do |block|
|
|
49
|
-
{ match: block.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !block.public_send(f).nil?] })
|
|
50
|
-
end,
|
|
51
|
-
effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
|
|
52
|
-
guards: Textus::Manifest::Policy::Predicates::FLOOR.keys.to_h do |action|
|
|
53
|
-
floor = Textus::Manifest::Policy::Predicates::FLOOR.fetch(action, [])
|
|
54
|
-
rule = Array(@manifest.rules.for(key).guard&.dig(action.to_s))
|
|
55
|
-
[action, { floor: floor, rule: rule }]
|
|
56
|
-
end,
|
|
57
|
-
}
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def effective_value(field, value)
|
|
61
|
-
return nil if value.nil?
|
|
62
|
-
|
|
63
|
-
case field
|
|
64
|
-
when :retention then retention_hash(value, string_keys: false)
|
|
65
|
-
when :react then value.to_h
|
|
66
|
-
else value
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def retention_hash(retention, string_keys:)
|
|
71
|
-
h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
|
|
72
|
-
string_keys ? h.transform_keys(&:to_s) : h
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Handlers
|
|
5
|
-
module Maintenance
|
|
6
|
-
class RuleLint
|
|
7
|
-
def initialize(manifest:)
|
|
8
|
-
@manifest = manifest
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def call(command, _call)
|
|
12
|
-
root = @manifest.data.root
|
|
13
|
-
live_rules = current_rules(root)
|
|
14
|
-
candidate_result = parse_candidate(command.candidate_yaml)
|
|
15
|
-
return candidate_result if candidate_result.is_a?(Value::Result) && candidate_result.failure?
|
|
16
|
-
|
|
17
|
-
candidate_rules = candidate_result
|
|
18
|
-
live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
|
|
19
|
-
candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
|
|
20
|
-
|
|
21
|
-
steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
|
|
22
|
-
{ "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
|
|
23
|
-
end
|
|
24
|
-
(live_by_match.keys - candidate_by_match.keys).each do |match|
|
|
25
|
-
steps << { "op" => "remove_rule", "match" => match }
|
|
26
|
-
end
|
|
27
|
-
(live_by_match.keys & candidate_by_match.keys).each do |match|
|
|
28
|
-
next if live_by_match[match] == candidate_by_match[match]
|
|
29
|
-
|
|
30
|
-
steps << { "op" => "change_rule", "match" => match, "from" => live_by_match[match], "to" => candidate_by_match[match] }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Value::Result.success(Textus::Store::Jobs::Plan.new(steps: steps, warnings: []))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def current_rules(root)
|
|
39
|
-
raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
|
|
40
|
-
Array(raw["rules"])
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def parse_candidate(yaml_text)
|
|
44
|
-
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
45
|
-
return Value::Result.failure(:usage_error, "candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
46
|
-
|
|
47
|
-
Array(raw["rules"])
|
|
48
|
-
rescue Psych::Exception => e
|
|
49
|
-
Value::Result.failure(:usage_error, "candidate YAML parse error: #{e.message}")
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|