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,27 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
class CursorStore < Base
|
|
5
|
+
def get(role)
|
|
6
|
+
result(table(:cursors).where(role:).first) || { role:, seq: 0 }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def set(role, seq)
|
|
10
|
+
table(:cursors).insert_conflict(
|
|
11
|
+
update: { seq:, updated_at: Time.now.utc },
|
|
12
|
+
).insert(role:, seq:, updated_at: Time.now.utc)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get_loop(role)
|
|
16
|
+
result(table(:loop_positions).where(role:).first) || { role:, index: 0 }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_loop(role, index)
|
|
20
|
+
table(:loop_positions).insert_conflict(
|
|
21
|
+
update: { index:, updated_at: Time.now.utc },
|
|
22
|
+
).insert(role:, index:, updated_at: Time.now.utc)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
class EntryIndex < Base
|
|
5
|
+
def record(key:, lane:, format:, schema:, content:)
|
|
6
|
+
table(:entries).insert_conflict(:replace).insert(
|
|
7
|
+
key:, lane:, format:, schema:, content:,
|
|
8
|
+
indexed_at: Time.now.utc
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove(key:)
|
|
13
|
+
table(:entries).where(key:).delete
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def search(query: nil, schema: nil, lane: nil, prefix: nil)
|
|
17
|
+
dataset = table(:entries).select(:key, :lane, :format, :schema, :content, :indexed_at)
|
|
18
|
+
if query
|
|
19
|
+
match_query = query.to_s.gsub("-", " ")
|
|
20
|
+
matching_rowids = @db[:entries_fts].where(Sequel.lit("entries_fts MATCH ?", match_query)).select_map(:rowid)
|
|
21
|
+
dataset = dataset.where(Sequel[:entries][:rowid] => matching_rowids)
|
|
22
|
+
end
|
|
23
|
+
dataset = dataset.where(schema:) if schema
|
|
24
|
+
dataset = dataset.where(lane:) if lane
|
|
25
|
+
dataset = dataset.where(Sequel.like(:key, "#{prefix}%")) if prefix
|
|
26
|
+
results(dataset.all)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Infra
|
|
5
|
+
class Store
|
|
6
|
+
module Envelope
|
|
7
|
+
module Meta
|
|
8
|
+
NO_META_FORMATS = %w[text].freeze
|
|
9
|
+
|
|
10
|
+
FIELDS = {
|
|
11
|
+
"uid" => {
|
|
12
|
+
inject: lambda { |meta, content, existing_meta, **_opts|
|
|
13
|
+
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
14
|
+
existing = existing_meta.is_a?(Hash) ? existing_meta["uid"] : nil
|
|
15
|
+
m["uid"] = existing || Textus::Value::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
16
|
+
[m, content]
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"sources" => {
|
|
20
|
+
inject: lambda { |meta, content, existing_meta, etag_for: nil|
|
|
21
|
+
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
22
|
+
existing = existing_meta.is_a?(Hash) ? existing_meta["sources"] : nil
|
|
23
|
+
|
|
24
|
+
if m.key?("sources")
|
|
25
|
+
raise Textus::BadContent.new(nil, "_meta.sources must be an array") unless m["sources"].is_a?(Array)
|
|
26
|
+
|
|
27
|
+
m["sources"] = m["sources"].map { |s| Meta.normalize_source!(s, etag_for) }
|
|
28
|
+
elsif existing.is_a?(Array) && !existing.empty?
|
|
29
|
+
m["sources"] = existing
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
[m, content]
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def self.inject_all(meta, content, existing_meta = {}, format: nil, etag_for: nil)
|
|
38
|
+
return [meta, content] if NO_META_FORMATS.include?(format)
|
|
39
|
+
|
|
40
|
+
FIELDS.each_value do |field|
|
|
41
|
+
meta, content = field[:inject].call(meta, content, existing_meta, etag_for: etag_for)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
[meta, content]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.normalize_source!(src, etag_for)
|
|
48
|
+
key = case src
|
|
49
|
+
when String then src
|
|
50
|
+
when Hash then src["key"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
raise Textus::BadContent.new(nil, "each source must be a string key or { key: } object") unless key.is_a?(String)
|
|
54
|
+
raise Textus::BadContent.new(nil, "each source key must be a non-empty string") if key.empty?
|
|
55
|
+
|
|
56
|
+
etag = etag_for&.call(key)
|
|
57
|
+
etag ? { "key" => key, "etag" => etag } : { "key" => key }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
class FileStore
|
|
5
|
+
def initialize(file_system)
|
|
6
|
+
@fs = file_system
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def read(path) = @fs.read(path)
|
|
10
|
+
|
|
11
|
+
def write(path, bytes)
|
|
12
|
+
@fs.mkdir_p(File.dirname(path))
|
|
13
|
+
@fs.write(path, bytes)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(path) = @fs.delete(path)
|
|
17
|
+
def move(from, to) = @fs.mv(from, to)
|
|
18
|
+
def exists?(path) = @fs.exists?(path)
|
|
19
|
+
def etag(path) = Textus::Value::Etag.for_file(path)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
module Index
|
|
5
|
+
class Builder
|
|
6
|
+
def initialize(db)
|
|
7
|
+
@db = db
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def rebuild!(resolver:)
|
|
11
|
+
@db[:entries].each do |row|
|
|
12
|
+
key = row[:key]
|
|
13
|
+
mentry = resolver.resolve(key)
|
|
14
|
+
next unless mentry
|
|
15
|
+
|
|
16
|
+
entry = mentry.entry
|
|
17
|
+
@db[:entries].insert_conflict(:replace).insert(
|
|
18
|
+
key:, lane: entry.lane, format: entry.format,
|
|
19
|
+
schema: entry.schema, content: row[:content],
|
|
20
|
+
indexed_at: Time.now.utc
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
module Index
|
|
5
|
+
class Lookup
|
|
6
|
+
def initialize(db)
|
|
7
|
+
@db = db
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find_by_hash(hash)
|
|
11
|
+
row = @db[:entries].where(Sequel.lit("json_extract(content, '$.content_hash') = ?", hash)).first
|
|
12
|
+
row&.dig(:key)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find_by_url(url)
|
|
16
|
+
row = @db[:entries].where(Sequel.lit("json_extract(content, '$.source.url') = ?", url)).first
|
|
17
|
+
row&.dig(:key)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
class LinkStore
|
|
5
|
+
def initialize(database)
|
|
6
|
+
@db = database
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def add_edge(from_key:, to_key:)
|
|
10
|
+
@db[:link_edges].insert_ignore.insert(from_key:, to_key:)
|
|
11
|
+
end
|
|
12
|
+
alias record add_edge
|
|
13
|
+
|
|
14
|
+
def remove_edge(from_key:, to_key:)
|
|
15
|
+
@db[:link_edges].where(from_key:, to_key:).delete
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def links_from(key)
|
|
19
|
+
@db[:link_edges].where(from_key: key).select_map(:to_key)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def links_to(key)
|
|
23
|
+
@db[:link_edges].where(to_key: key).select_map(:from_key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reachable(from:, depth: nil)
|
|
27
|
+
visited = Set.new
|
|
28
|
+
queue = [[from, 0]]
|
|
29
|
+
while queue.any?
|
|
30
|
+
current, d = queue.shift
|
|
31
|
+
next if visited.include?(current)
|
|
32
|
+
next if depth && d > depth
|
|
33
|
+
|
|
34
|
+
visited.add(current)
|
|
35
|
+
links_from(current).each { |to| queue << [to, d + 1] }
|
|
36
|
+
end
|
|
37
|
+
visited.delete(from)
|
|
38
|
+
visited.to_a
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
class WorkflowQueue < Base
|
|
5
|
+
def push(event_type:, workflow_type:, key:, lane:, payload: nil,
|
|
6
|
+
created_at: Time.now.utc, scheduled_at: nil,
|
|
7
|
+
state: "pending", retry_count: 0, max_attempts: 3)
|
|
8
|
+
table(:workflow_events).insert(
|
|
9
|
+
event_type: event_type.to_s, workflow_type:, key:, lane:,
|
|
10
|
+
payload: payload&.to_json, created_at:, scheduled_at:,
|
|
11
|
+
state:, retry_count:, max_attempts:
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def pop
|
|
16
|
+
row = table(:workflow_events)
|
|
17
|
+
.where(state: "pending")
|
|
18
|
+
.where(Sequel.|({ scheduled_at: nil }, Sequel.lit("scheduled_at <= datetime('now')")))
|
|
19
|
+
.order(:created_at, :id)
|
|
20
|
+
.limit(1)
|
|
21
|
+
.for_update
|
|
22
|
+
.first
|
|
23
|
+
result(row)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pending?(event_type:, key:)
|
|
27
|
+
table(:workflow_events)
|
|
28
|
+
.where(event_type: event_type.to_s, key: key)
|
|
29
|
+
.where(state: %w[pending processing])
|
|
30
|
+
.any?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mark_processing(event_id)
|
|
34
|
+
table(:workflow_events).where(id: event_id).update(state: "processing")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def complete(event_id)
|
|
38
|
+
table(:workflow_events).where(id: event_id).update(state: "done")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fail(event_id, retry_count:, error:)
|
|
42
|
+
table(:workflow_events).where(id: event_id).update(state: "failed", retry_count:, error:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reschedule(event_id, retry_count:, delay:, error:)
|
|
46
|
+
table(:workflow_events).where(id: event_id).update(
|
|
47
|
+
state: "pending", retry_count:, error:,
|
|
48
|
+
scheduled_at: Sequel.lit("datetime('now', '+#{delay.to_i} seconds')")
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Infra
|
|
3
|
+
class Store
|
|
4
|
+
def initialize(database:, file_system:)
|
|
5
|
+
@db = database.connection
|
|
6
|
+
@file_system = file_system
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :file_system, :db
|
|
10
|
+
|
|
11
|
+
def cursor_store = @cursor_store ||= CursorStore.new(@db)
|
|
12
|
+
def link_store = @link_store ||= LinkStore.new(@db)
|
|
13
|
+
def entry_index = @entry_index ||= EntryIndex.new(@db)
|
|
14
|
+
def audit_log = @audit_log ||= AuditLog.new(@db)
|
|
15
|
+
def workflow_queue = @workflow_queue ||= WorkflowQueue.new(@db)
|
|
16
|
+
def file_store = @file_store ||= FileStore.new(@file_system)
|
|
17
|
+
|
|
18
|
+
def transaction(&) = @db.transaction(&)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
require_relative "store/base"
|
|
24
|
+
require_relative "store/cursor_store"
|
|
25
|
+
require_relative "store/audit_log"
|
|
26
|
+
require_relative "store/workflow_queue"
|
|
27
|
+
require_relative "store/entry_index"
|
|
28
|
+
require_relative "store/file_store"
|
|
29
|
+
require_relative "store/link_store"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Artifact
|
|
4
|
+
class Cache
|
|
5
|
+
def initialize(store, ttl_default: 3600 * 24 * 30)
|
|
6
|
+
@store = store
|
|
7
|
+
@ttl_default = ttl_default
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def ttl(key)
|
|
11
|
+
entry = @store.get(key:)
|
|
12
|
+
return @ttl_default unless entry
|
|
13
|
+
|
|
14
|
+
entry["ttl"] || @ttl_default
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stale?(key)
|
|
18
|
+
entry = @store.get(key:)
|
|
19
|
+
return true unless entry
|
|
20
|
+
return true if entry["stale"] || entry["fetching"]
|
|
21
|
+
|
|
22
|
+
generated_at = entry["generated_at"]
|
|
23
|
+
return true unless generated_at
|
|
24
|
+
|
|
25
|
+
age = Time.now - Time.parse(generated_at)
|
|
26
|
+
age > ttl(key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def expire(key)
|
|
30
|
+
@store.put(key:, meta: { "stale" => true })
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Artifact
|
|
4
|
+
module Handlers
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def get(key:, store:, ctx:, call:)
|
|
8
|
+
store.get(key:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(store:, ctx:, call:, prefix: nil)
|
|
12
|
+
store.list(prefix: prefix || "artifact")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def status(store:, ctx:, call:)
|
|
16
|
+
store.list(prefix: "artifact").map do |row|
|
|
17
|
+
cache = Cache.new(store)
|
|
18
|
+
{ key: row[:key], stale: cache.stale?(row[:key]) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def refresh(key:, store:, ctx:, call:)
|
|
23
|
+
cache = Cache.new(store)
|
|
24
|
+
cache.expire(key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Artifact
|
|
4
|
+
class Lifecycle
|
|
5
|
+
def initialize(cache)
|
|
6
|
+
@cache = cache
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def on_stale(key)
|
|
10
|
+
@cache.expire(key)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_expire(key)
|
|
14
|
+
@cache.expire(key)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def retention(key)
|
|
18
|
+
@cache.ttl(key)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Artifact
|
|
4
|
+
module Verbs
|
|
5
|
+
Textus::Protocol::VerbRegistry.register(
|
|
6
|
+
Textus::Protocol::VerbRegistry::VerbSpec.new(
|
|
7
|
+
:workflow_spec, "Return the full definition of a named workflow — match, steps, publish targets.",
|
|
8
|
+
[Textus::Protocol::VerbRegistry::ArgSpec.arg(
|
|
9
|
+
name: :name, required: true, positional: true,
|
|
10
|
+
description: "workflow type name (e.g. 'boot', 'decisions-log', 'materialize')"
|
|
11
|
+
)],
|
|
12
|
+
%i[cli mcp], :read
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module Dedup
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def find(lookup, content_hash, url: nil)
|
|
8
|
+
dup = lookup.find_by_hash(content_hash)
|
|
9
|
+
return dup if dup
|
|
10
|
+
return nil unless url
|
|
11
|
+
|
|
12
|
+
lookup.find_by_url(url)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module EntryTypes
|
|
5
|
+
module Asset
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build(path:, lane:, time:, layout:, label: nil, **)
|
|
9
|
+
asset_rel = copy(path, lane:, time:, layout:)
|
|
10
|
+
{ "source" => { "kind" => "asset", "label" => label || File.basename(path) },
|
|
11
|
+
"asset" => asset_rel, "ingested_at" => time.utc.iso8601, "body" => nil }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy(path, lane:, time:, layout:)
|
|
15
|
+
date_path = time.strftime("%Y/%m/%d")
|
|
16
|
+
filename = File.basename(path)
|
|
17
|
+
assets_dir = layout.asset_raw_dir(date_path, lane)
|
|
18
|
+
FileUtils.mkdir_p(assets_dir)
|
|
19
|
+
FileUtils.cp(path, File.join(assets_dir, filename))
|
|
20
|
+
sentinel = layout.asset_sentinel_path
|
|
21
|
+
File.write(sentinel, "*\n") unless File.exist?(sentinel)
|
|
22
|
+
"raw/#{date_path}/#{lane}/#{filename}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def move(old_rel, lane, layout)
|
|
26
|
+
old_path = layout.asset_resolve(old_rel)
|
|
27
|
+
return unless File.exist?(old_path)
|
|
28
|
+
|
|
29
|
+
now = Time.now.utc
|
|
30
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
31
|
+
filename = File.basename(old_path)
|
|
32
|
+
new_dir = layout.asset_raw_dir(date_path, lane)
|
|
33
|
+
new_path = File.join(new_dir, filename)
|
|
34
|
+
return if old_path == new_path
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(new_dir)
|
|
37
|
+
FileUtils.mv(old_path, new_path)
|
|
38
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
39
|
+
warn "[textus ingest] could not move asset #{old_rel}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module EntryTypes
|
|
5
|
+
module Link
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build(url:, time:, label: nil, **)
|
|
9
|
+
{ "source" => { "kind" => "url", "url" => url, "label" => label || url },
|
|
10
|
+
"ingested_at" => time.utc.iso8601, "body" => nil }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module EntryTypes
|
|
5
|
+
module Text
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build(path:, time:, label: nil, **)
|
|
9
|
+
{ "source" => { "kind" => "file", "path" => path,
|
|
10
|
+
"label" => label || File.basename(path) },
|
|
11
|
+
"ingested_at" => time.utc.iso8601, "body" => File.read(path) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module EntryTypes
|
|
5
|
+
require_relative "entry_types/link"
|
|
6
|
+
require_relative "entry_types/asset"
|
|
7
|
+
require_relative "entry_types/text"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def for(kind)
|
|
12
|
+
registry[kind] or raise(ArgumentError.new("unknown ingest kind: #{kind}"))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def registry
|
|
16
|
+
@registry ||= {
|
|
17
|
+
"link" => Link,
|
|
18
|
+
"asset" => Asset,
|
|
19
|
+
"text" => Text,
|
|
20
|
+
}.freeze
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module Handlers
|
|
5
|
+
SOURCE_KINDS = %w[url file asset].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def ingest(kind:, slug:, ctx:, call:, url: nil, path: nil, lane: nil, label: nil)
|
|
10
|
+
raise Textus::UsageError.new("kind must be one of #{SOURCE_KINDS.join("|")}") unless SOURCE_KINDS.include?(kind)
|
|
11
|
+
|
|
12
|
+
case kind
|
|
13
|
+
when "url" then raise Textus::UsageError.new("url required") unless url
|
|
14
|
+
when "file" then raise Textus::UsageError.new("path required") unless path
|
|
15
|
+
when "asset"
|
|
16
|
+
raise Textus::UsageError.new("path required") unless path
|
|
17
|
+
raise Textus::UsageError.new("lane required") unless lane
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
now = Time.now.utc
|
|
21
|
+
key = KeyBuilder.derive_key(now, kind, slug)
|
|
22
|
+
content_hash = KeyBuilder.compute_hash(kind:, url:, path:)
|
|
23
|
+
|
|
24
|
+
entry_type = EntryTypes.for(kind)
|
|
25
|
+
time = now
|
|
26
|
+
structured = entry_type.build(url:, path:, lane:, label:, time:, layout: ctx.layout)
|
|
27
|
+
|
|
28
|
+
lookup = Textus::Infra::Store::Index::Lookup.new(ctx.container.store.db)
|
|
29
|
+
duplicate_key = Dedup.find(lookup, content_hash, url:)
|
|
30
|
+
|
|
31
|
+
if duplicate_key && duplicate_key != key
|
|
32
|
+
old_content = ctx.read(key: duplicate_key)&.content || {}
|
|
33
|
+
Resolver.supersede(duplicate_key, key, ctx:, call:)
|
|
34
|
+
|
|
35
|
+
EntryTypes::Asset.move(old_content["asset"], lane, ctx.layout) if (kind == "asset") && old_content["asset"]
|
|
36
|
+
|
|
37
|
+
old_count = old_content["ingest_count"] || 1
|
|
38
|
+
structured["ingest_count"] = old_count + 1
|
|
39
|
+
structured["duplicated_at"] = now.utc.iso8601
|
|
40
|
+
structured["supersedes"] = duplicate_key
|
|
41
|
+
else
|
|
42
|
+
structured["ingest_count"] = 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
env = ctx.put(key:, meta: {}, body: nil, content: structured, call:)
|
|
46
|
+
|
|
47
|
+
IndexRebuilder.rebuild!(ctx.container.store.db, ctx.manifest.resolver)
|
|
48
|
+
|
|
49
|
+
{ "key" => key, "uid" => env.uid, "etag" => env.etag }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Lanes
|
|
3
|
+
module Ingest
|
|
4
|
+
module KeyBuilder
|
|
5
|
+
CONTENT_HASH_ALGO = "sha256"
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def derive_key(now, kind, slug)
|
|
10
|
+
date = now.strftime("%Y.%m.%d")
|
|
11
|
+
"raw.#{date}.#{kind}-#{slug}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compute_hash(kind:, url: nil, path: nil)
|
|
15
|
+
digest = Digest::SHA256.new
|
|
16
|
+
case kind
|
|
17
|
+
when "url"
|
|
18
|
+
raise ArgumentError.new("url required") unless url
|
|
19
|
+
|
|
20
|
+
digest.update(url)
|
|
21
|
+
when "file", "asset"
|
|
22
|
+
raise ArgumentError.new("path required") unless path
|
|
23
|
+
|
|
24
|
+
digest.file(path)
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError.new("unknown kind: #{kind}")
|
|
27
|
+
end
|
|
28
|
+
"#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|