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,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Surfaces YAML parse failures for files in <store>/schemas/. Without
|
|
5
|
-
# this check, malformed schemas are silently skipped by other doctor
|
|
6
|
-
# checks (UnownedSchemaFields rescues, Schemas only checks filenames),
|
|
7
|
-
# leaving the operator with no signal that a schema is broken.
|
|
8
|
-
class SchemaParseError < Check
|
|
9
|
-
def call
|
|
10
|
-
dir = geometry.schemas_dir
|
|
11
|
-
return [] unless File.directory?(dir)
|
|
12
|
-
|
|
13
|
-
Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
|
|
14
|
-
Schema.load(path)
|
|
15
|
-
rescue StandardError => e
|
|
16
|
-
out << {
|
|
17
|
-
"code" => "schema.parse_error",
|
|
18
|
-
"level" => "error",
|
|
19
|
-
"subject" => path,
|
|
20
|
-
"message" => "schema failed to parse: #{e.class}: #{e.message}",
|
|
21
|
-
"fix" => "fix the YAML at #{path} (check indentation, quoted scalars, and aliases)",
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class SchemaViolations < Check
|
|
5
|
-
def call
|
|
6
|
-
result = Textus::Doctor::Validator.new(
|
|
7
|
-
reader: ->(key, ctnr, _c) { Textus::Store::Entry::Reader.from(container: ctnr).read(key) },
|
|
8
|
-
manifest: @container.manifest,
|
|
9
|
-
audit_log: @container.audit_log,
|
|
10
|
-
schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
|
|
11
|
-
).call(container: @container, call: Textus::Value::Call.build(role: Textus::Value::Role::DEFAULT))
|
|
12
|
-
|
|
13
|
-
result["violations"].map do |v|
|
|
14
|
-
fix = v["expected"] &&
|
|
15
|
-
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
16
|
-
{
|
|
17
|
-
"code" => v["code"],
|
|
18
|
-
"level" => "error",
|
|
19
|
-
"subject" => v["key"],
|
|
20
|
-
"message" => v["message"] || "#{v["code"]} on #{v["key"]}",
|
|
21
|
-
"fix" => fix,
|
|
22
|
-
}.compact
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Schemas < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
manifest.data.entries.each do |entry|
|
|
8
|
-
next if entry.schema.nil?
|
|
9
|
-
|
|
10
|
-
sp = geometry.schema_path(entry.schema)
|
|
11
|
-
next if File.exist?(sp)
|
|
12
|
-
|
|
13
|
-
out << {
|
|
14
|
-
"code" => "schema.missing",
|
|
15
|
-
"level" => "error",
|
|
16
|
-
"subject" => entry.key,
|
|
17
|
-
"message" => "schema '#{entry.schema}' not found at #{sp}",
|
|
18
|
-
"fix" => "create the schema file or run 'textus schema init #{entry.schema} --from=<key>'",
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
out
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class ScratchpadSources < Check
|
|
5
|
-
def call
|
|
6
|
-
issues = []
|
|
7
|
-
manifest.resolver.enumerate.each do |row|
|
|
8
|
-
next unless row[:key].start_with?("scratchpad.notes.")
|
|
9
|
-
next unless row[:path] && File.exist?(row[:path])
|
|
10
|
-
|
|
11
|
-
sources = parse_sources(row[:path])
|
|
12
|
-
sources.each do |raw_key|
|
|
13
|
-
next if raw_entry_exists?(raw_key)
|
|
14
|
-
|
|
15
|
-
issues << {
|
|
16
|
-
"code" => "scratchpad.source_missing",
|
|
17
|
-
"level" => "warning",
|
|
18
|
-
"subject" => row[:key],
|
|
19
|
-
"message" => "scratchpad entry '#{row[:key]}' references raw key '#{raw_key}' " \
|
|
20
|
-
"which does not exist in the store",
|
|
21
|
-
"fix" => "re-ingest the source: textus ingest ..., or remove the stale sources: entry",
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
issues
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def parse_sources(path)
|
|
31
|
-
content = File.read(path)
|
|
32
|
-
match = content.match(/\A---\n(.*?)\n---/m)
|
|
33
|
-
return [] unless match
|
|
34
|
-
|
|
35
|
-
front = YAML.safe_load(match[1])
|
|
36
|
-
Array(front&.dig("sources")).filter_map do |s|
|
|
37
|
-
case s
|
|
38
|
-
when String then s
|
|
39
|
-
when Hash then s["key"]
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
rescue StandardError
|
|
43
|
-
[]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def raw_entry_exists?(key)
|
|
47
|
-
path = manifest.resolver.resolve(key).path
|
|
48
|
-
File.exist?(path)
|
|
49
|
-
rescue Textus::UnknownKey, Textus::Error
|
|
50
|
-
false
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Sentinels < Check
|
|
5
|
-
def call
|
|
6
|
-
store = Textus::Port::SentinelStore.new
|
|
7
|
-
file_stat = Textus::Port::Storage::FileStat.new
|
|
8
|
-
dir = Textus::Store::Layout.new(root).sentinels_root
|
|
9
|
-
return [] unless file_stat.directory?(dir)
|
|
10
|
-
|
|
11
|
-
repo_root = File.dirname(root)
|
|
12
|
-
file_stat.glob(File.join(dir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
|
|
13
|
-
inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
20
|
-
sentinel = store.load(sentinel_path, repo_root)
|
|
21
|
-
return [parse_error_issue(sentinel_path)] if sentinel.nil?
|
|
22
|
-
return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?(file_stat)
|
|
23
|
-
return [drift_issue(sentinel)] if sentinel.drift?(file_stat)
|
|
24
|
-
|
|
25
|
-
[]
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def parse_error_issue(sentinel_path)
|
|
29
|
-
{
|
|
30
|
-
"code" => "sentinel.parse_error",
|
|
31
|
-
"level" => "warning",
|
|
32
|
-
"subject" => sentinel_path,
|
|
33
|
-
"message" => "sentinel is not valid JSON",
|
|
34
|
-
"fix" => "delete #{sentinel_path} and re-run 'textus drain' to regenerate",
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def orphan_issue(sentinel_path, sentinel)
|
|
39
|
-
{
|
|
40
|
-
"code" => "sentinel.orphan",
|
|
41
|
-
"level" => "warning",
|
|
42
|
-
"subject" => sentinel_path,
|
|
43
|
-
"message" => "sentinel target #{sentinel.target.inspect} no longer exists",
|
|
44
|
-
"fix" => "delete #{sentinel_path} (the published file is gone) or restore the target",
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def drift_issue(sentinel)
|
|
49
|
-
{
|
|
50
|
-
"code" => "sentinel.drift",
|
|
51
|
-
"level" => "warning",
|
|
52
|
-
"subject" => sentinel.target,
|
|
53
|
-
"message" => "published file at #{sentinel.target} was modified out-of-band",
|
|
54
|
-
"fix" => "re-run 'textus drain' to overwrite, or copy the manual edit back into the store source",
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
# Warns when a knowledge-lane doc's `**reviewed** YYYY-MM (vX.Y)` stamp
|
|
5
|
-
# is more than MINOR_THRESHOLD minor versions behind Textus::VERSION.
|
|
6
|
-
# The stamp is the human staleness signal declared in docs conventions
|
|
7
|
-
# (knowledge.docs-index); this check makes it machine-enforced.
|
|
8
|
-
class StaleReviewedStamp < Check
|
|
9
|
-
STAMP_RE = /\*\*reviewed\*\*\s+\d{4}-\d{2}\s+\(v(\d+\.\d+(?:\.\d+)?)\)/
|
|
10
|
-
MINOR_THRESHOLD = 5
|
|
11
|
-
|
|
12
|
-
def call
|
|
13
|
-
current_minor = parse_minor(Textus::VERSION)
|
|
14
|
-
issues = []
|
|
15
|
-
|
|
16
|
-
manifest.resolver.enumerate.each do |row|
|
|
17
|
-
next unless row[:key].to_s.start_with?("knowledge.")
|
|
18
|
-
next unless row[:path] && File.file?(row[:path])
|
|
19
|
-
|
|
20
|
-
body = File.read(row[:path])
|
|
21
|
-
m = body.match(STAMP_RE)
|
|
22
|
-
next unless m
|
|
23
|
-
|
|
24
|
-
reviewed_minor = parse_minor(m[1])
|
|
25
|
-
behind = current_minor - reviewed_minor
|
|
26
|
-
next unless behind > MINOR_THRESHOLD
|
|
27
|
-
|
|
28
|
-
issues << stale_issue(row[:key], m[1], behind)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
issues
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def parse_minor(version_str)
|
|
37
|
-
version_str.sub(/\Av/, "").split(".").map(&:to_i)[1] || 0
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def stale_issue(key, stamp_version, behind)
|
|
41
|
-
current_short = Textus::VERSION[/\A\d+\.\d+/]
|
|
42
|
-
{
|
|
43
|
-
"code" => "stale_reviewed_stamp",
|
|
44
|
-
"level" => "warning",
|
|
45
|
-
"subject" => key.to_s,
|
|
46
|
-
"message" => "reviewed at v#{stamp_version}; current is v#{Textus::VERSION} " \
|
|
47
|
-
"(#{behind} minor versions behind, threshold is #{MINOR_THRESHOLD})",
|
|
48
|
-
"fix" => "review the doc and update the stamp to: **reviewed** YYYY-MM (v#{current_short})",
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class Templates < Check
|
|
5
|
-
def call
|
|
6
|
-
out = []
|
|
7
|
-
manifest.data.entries.each do |entry|
|
|
8
|
-
entry.publish_targets.each do |target|
|
|
9
|
-
template = target.template
|
|
10
|
-
next if template.nil?
|
|
11
|
-
|
|
12
|
-
tp = geometry.template_path(template)
|
|
13
|
-
next if File.exist?(tp)
|
|
14
|
-
|
|
15
|
-
out << {
|
|
16
|
-
"code" => "template.missing",
|
|
17
|
-
"level" => "error",
|
|
18
|
-
"subject" => entry.key,
|
|
19
|
-
"message" => "template '#{template}' not found at #{tp}",
|
|
20
|
-
"fix" => "create the file at #{tp} or update the publish target's template: field",
|
|
21
|
-
}
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
out
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class UnownedSchemaFields < Check
|
|
5
|
-
def call
|
|
6
|
-
dir = geometry.schemas_dir
|
|
7
|
-
return [] unless File.directory?(dir)
|
|
8
|
-
|
|
9
|
-
Dir.glob(File.join(dir, "*.yaml")).flat_map do |path|
|
|
10
|
-
issues_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def issues_for(path)
|
|
17
|
-
schema = safe_load(path)
|
|
18
|
-
return [] if schema.nil?
|
|
19
|
-
|
|
20
|
-
unowned = schema.unowned_fields
|
|
21
|
-
return [] if unowned.empty?
|
|
22
|
-
|
|
23
|
-
[{
|
|
24
|
-
"code" => "schema.unowned_fields",
|
|
25
|
-
"level" => "info",
|
|
26
|
-
"subject" => schema.name || File.basename(path, ".yaml"),
|
|
27
|
-
"message" => "schema has fields without maintained_by: #{unowned.join(", ")}",
|
|
28
|
-
"fix" => "add 'maintained_by: <role>' to each field in #{path} (optional but recommended)",
|
|
29
|
-
}]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def safe_load(path)
|
|
33
|
-
Schema.load(path)
|
|
34
|
-
rescue StandardError
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
data/lib/textus/doctor/check.rb
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
# Abstract base for a single doctor check. Each concrete check inspects
|
|
4
|
-
# one slice of store health and returns an array of issue hashes:
|
|
5
|
-
# { "code" => String, "level" => "error"|"warning"|"info",
|
|
6
|
-
# "subject" => String, "message" => String, "fix" => String (optional) }
|
|
7
|
-
class Check
|
|
8
|
-
# Snake-case name used in --checks flag and ALL_CHECKS list. Default
|
|
9
|
-
# derives from the class name; override if the SPEC name diverges.
|
|
10
|
-
def self.name_key
|
|
11
|
-
@name_key ||= name.split("::").last
|
|
12
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
13
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
14
|
-
.downcase
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def initialize(container, role: Textus::Value::Role::DEFAULT)
|
|
18
|
-
@container = container
|
|
19
|
-
@role = role
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call
|
|
23
|
-
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
protected
|
|
27
|
-
|
|
28
|
-
def root = @container.root
|
|
29
|
-
def geometry = @container.layout
|
|
30
|
-
def manifest = @container.manifest
|
|
31
|
-
|
|
32
|
-
def dispatch(verb, *args, **kwargs)
|
|
33
|
-
contract_class = Textus::VerbRegistry.contract_class_for(verb)
|
|
34
|
-
members = contract_class.members
|
|
35
|
-
command_kwargs = members.each_with_index.to_h { |m, i| [m, args[i] || kwargs[m]] }
|
|
36
|
-
command = contract_class.new(**command_kwargs)
|
|
37
|
-
call = Textus::Value::Call.build(role: @role)
|
|
38
|
-
result = @container.pipeline.dispatch(command, call: call)
|
|
39
|
-
Textus::Value::Result.extract(result)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Doctor
|
|
5
|
-
class Validator
|
|
6
|
-
def initialize(reader:, manifest:, audit_log:, schema_for:)
|
|
7
|
-
@reader = reader
|
|
8
|
-
@manifest = manifest
|
|
9
|
-
@audit_log = audit_log
|
|
10
|
-
@schema_for = schema_for
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def call(container:, call:)
|
|
14
|
-
@container = container
|
|
15
|
-
@call = call
|
|
16
|
-
violations = []
|
|
17
|
-
check_content_violations(violations)
|
|
18
|
-
check_role_authority_violations(violations)
|
|
19
|
-
{ "protocol" => Textus::PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def check_content_violations(violations)
|
|
25
|
-
@manifest.resolver.enumerate.each do |row|
|
|
26
|
-
key = row[:key]
|
|
27
|
-
mentry = row[:manifest_entry]
|
|
28
|
-
env = fetch_envelope(key, violations) or next
|
|
29
|
-
schema = mentry.schema && @schema_for.call(mentry.schema)
|
|
30
|
-
next unless schema
|
|
31
|
-
|
|
32
|
-
begin
|
|
33
|
-
validate_schema!(schema, env, mentry.format)
|
|
34
|
-
rescue Textus::Error => e
|
|
35
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def check_role_authority_violations(violations)
|
|
41
|
-
@manifest.resolver.enumerate.each do |row|
|
|
42
|
-
mentry = row[:manifest_entry]
|
|
43
|
-
next unless mentry.schema
|
|
44
|
-
|
|
45
|
-
schema = @schema_for.call(mentry.schema)
|
|
46
|
-
next unless schema
|
|
47
|
-
|
|
48
|
-
env = begin
|
|
49
|
-
@reader.call(row[:key], @container, @call)
|
|
50
|
-
rescue StandardError
|
|
51
|
-
next
|
|
52
|
-
end
|
|
53
|
-
append_authority_violations(violations, row[:key], env, schema)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def append_authority_violations(violations, key, env, schema)
|
|
58
|
-
last_writer = @audit_log.last_writer_for(key)
|
|
59
|
-
return if last_writer.nil?
|
|
60
|
-
|
|
61
|
-
last_writer_is_authority =
|
|
62
|
-
@manifest.policy.roles_with_capability("author").include?(last_writer)
|
|
63
|
-
env.meta.each_key do |field|
|
|
64
|
-
owner = schema.maintained_by(field)
|
|
65
|
-
next if owner.nil? || last_writer == owner || last_writer_is_authority
|
|
66
|
-
|
|
67
|
-
violations << {
|
|
68
|
-
"key" => key,
|
|
69
|
-
"code" => "role_authority",
|
|
70
|
-
"field" => field,
|
|
71
|
-
"expected" => owner,
|
|
72
|
-
"last_writer" => last_writer,
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def fetch_envelope(key, violations)
|
|
78
|
-
@reader.call(key, @container, @call)
|
|
79
|
-
rescue Textus::Error => e
|
|
80
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
81
|
-
nil
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def validate_schema!(schema, envelope, format)
|
|
85
|
-
payload = case format
|
|
86
|
-
when "json", "yaml" then envelope.content || {}
|
|
87
|
-
else envelope.meta || {}
|
|
88
|
-
end
|
|
89
|
-
schema.validate!(payload)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
data/lib/textus/doctor.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Health check for a Textus store. Returns a JSON-friendly Hash envelope
|
|
3
|
-
# with an `issues` array and a summary. Each issue is a Hash with
|
|
4
|
-
# `code`, `level`, `subject`, `message`, and optionally `fix`.
|
|
5
|
-
module Doctor
|
|
6
|
-
LEVELS = %w[error warning info].freeze
|
|
7
|
-
|
|
8
|
-
CHECKS = [
|
|
9
|
-
Check::ProtocolVersion,
|
|
10
|
-
Check::ManifestFiles,
|
|
11
|
-
Check::Schemas,
|
|
12
|
-
Check::SchemaParseError,
|
|
13
|
-
Check::Templates,
|
|
14
|
-
Check::IllegalKeys,
|
|
15
|
-
Check::Sentinels,
|
|
16
|
-
Check::AuditLog,
|
|
17
|
-
Check::UnownedSchemaFields,
|
|
18
|
-
Check::SchemaViolations,
|
|
19
|
-
Check::RuleAmbiguity,
|
|
20
|
-
Check::OrphanedPublishTargets,
|
|
21
|
-
Check::PublishTreeIndexOverlap,
|
|
22
|
-
Check::ProposalTargets,
|
|
23
|
-
Check::GeneratorDrift,
|
|
24
|
-
Check::RawAssetPaths,
|
|
25
|
-
Check::ScratchpadSources,
|
|
26
|
-
Check::StaleReviewedStamp,
|
|
27
|
-
].freeze
|
|
28
|
-
|
|
29
|
-
ALL_CHECKS = CHECKS.map(&:name_key).freeze
|
|
30
|
-
|
|
31
|
-
module_function
|
|
32
|
-
|
|
33
|
-
def build(container:, checks: nil, role: Textus::Value::Role::DEFAULT)
|
|
34
|
-
selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
|
|
35
|
-
unknown = selected_keys - ALL_CHECKS
|
|
36
|
-
unless unknown.empty?
|
|
37
|
-
raise UsageError.new(
|
|
38
|
-
"unknown doctor check: #{unknown.first}. Valid checks: #{ALL_CHECKS.join(", ")}",
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
|
|
43
|
-
issues = selected.flat_map { |c| c.new(container, role:).call }
|
|
44
|
-
|
|
45
|
-
summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
|
|
46
|
-
{
|
|
47
|
-
"protocol" => Textus::PROTOCOL,
|
|
48
|
-
"ok" => summary["error"].zero?,
|
|
49
|
-
"issues" => issues,
|
|
50
|
-
"summary" => summary,
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
data/lib/textus/format/base.rb
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
class Base
|
|
4
|
-
def self.parse(_raw, path: nil)
|
|
5
|
-
_ = path
|
|
6
|
-
raise NotImplementedError.new("#{name}.parse not implemented")
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def self.serialize(meta: {}, body: "", content: nil)
|
|
10
|
-
_ = meta
|
|
11
|
-
_ = body
|
|
12
|
-
_ = content
|
|
13
|
-
raise NotImplementedError.new("#{name}.serialize not implemented")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.extensions
|
|
17
|
-
raise NotImplementedError.new("#{name}.extensions not implemented")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.validate_against(schema, parsed)
|
|
21
|
-
schema.validate!(parsed["_meta"] || {})
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.nested_glob
|
|
25
|
-
raise NotImplementedError.new("#{name}.nested_glob not implemented")
|
|
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("#{format_name} nested path must not have an extension")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
return if extensions.include?(ext)
|
|
37
|
-
|
|
38
|
-
raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.enforce_name_match!(path, meta)
|
|
42
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
43
|
-
|
|
44
|
-
ext = extensions.first
|
|
45
|
-
basename = File.basename(path, ext)
|
|
46
|
-
return if meta["name"] == basename
|
|
47
|
-
|
|
48
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
52
|
-
raw = File.binread(path)
|
|
53
|
-
parsed = parse(raw, path: path)
|
|
54
|
-
meta = parsed["_meta"] || {}
|
|
55
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
56
|
-
|
|
57
|
-
new_meta = meta.merge("name" => basename)
|
|
58
|
-
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
|
|
59
|
-
true
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def self.format_name
|
|
63
|
-
name.split("::").last.downcase
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.validate_raw_entry!(_parsed, _lane); end
|
|
67
|
-
|
|
68
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
69
|
-
_ = meta
|
|
70
|
-
_ = body
|
|
71
|
-
_ = content
|
|
72
|
-
_ = path
|
|
73
|
-
raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
data/lib/textus/format/json.rb
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Json < 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 = ::JSON.parse(raw)
|
|
12
|
-
rescue ::JSON::ParserError => e
|
|
13
|
-
raise BadFrontmatter.new(path, "JSON parse failed: #{e.message}")
|
|
14
|
-
end
|
|
15
|
-
raise BadFrontmatter.new(path, "JSON top-level must be an object") 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
|
-
out = ::JSON.pretty_generate(on_disk)
|
|
27
|
-
out += "\n" unless out.end_with?("\n")
|
|
28
|
-
out
|
|
29
|
-
elsif body && !body.to_s.empty?
|
|
30
|
-
b = body.to_s
|
|
31
|
-
b += "\n" unless b.end_with?("\n")
|
|
32
|
-
b
|
|
33
|
-
else
|
|
34
|
-
raise UsageError.new("json serialize requires :content or :body")
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.validate_against(schema, parsed)
|
|
39
|
-
schema.validate!(parsed["content"] || {})
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.extensions = [".json"]
|
|
43
|
-
|
|
44
|
-
def self.nested_glob = "**/*.json"
|
|
45
|
-
|
|
46
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
47
|
-
raise UsageError.new("put for json requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
48
|
-
|
|
49
|
-
if content.nil?
|
|
50
|
-
begin
|
|
51
|
-
parsed = parse(body.to_s, path: path)
|
|
52
|
-
rescue BadFrontmatter => e
|
|
53
|
-
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
54
|
-
end
|
|
55
|
-
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
56
|
-
else
|
|
57
|
-
bytes = serialize(meta: meta, body: "", content: content)
|
|
58
|
-
[bytes, meta, bytes, content]
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def self.validate_path_extension(path, nested)
|
|
63
|
-
ext = File.extname(path)
|
|
64
|
-
if nested
|
|
65
|
-
return if ext == ""
|
|
66
|
-
|
|
67
|
-
raise UsageError.new("nested json path must not have an extension")
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
return if ext == ".json"
|
|
71
|
-
|
|
72
|
-
raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.data_to_payload(data)
|
|
76
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
77
|
-
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|