textus 0.55.1 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -2768
- data/README.md +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{gate → protocol}/binder.rb +2 -10
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -61
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/core/sentinel.rb +0 -30
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/notebook_sources.rb +0 -50
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Boot
|
|
4
|
+
module Context
|
|
5
|
+
def self.build(manifest:, event_store:, file_system:, layout:)
|
|
6
|
+
etag = Textus::Value::Etag.for_contract(layout)
|
|
7
|
+
latest_seq = event_store.latest[:seq]
|
|
8
|
+
artifact = read_artifact_content(manifest, file_system, layout)
|
|
9
|
+
context = read_boot_context(manifest, file_system, layout)
|
|
10
|
+
|
|
11
|
+
stable = artifact || inline_boot_content(manifest, latest_seq)
|
|
12
|
+
if stable["agent_quickstart"]
|
|
13
|
+
stable = stable.merge(
|
|
14
|
+
"agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
payload = {
|
|
19
|
+
"protocol" => Textus::PROTOCOL,
|
|
20
|
+
"store_root" => layout.root,
|
|
21
|
+
"contract_etag" => etag,
|
|
22
|
+
}.merge(stable)
|
|
23
|
+
payload["context"] = context if context
|
|
24
|
+
payload
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.read_artifact_content(manifest, file_system, _layout)
|
|
28
|
+
key = "artifacts.boot"
|
|
29
|
+
res = manifest.resolver.resolve(key)
|
|
30
|
+
return nil unless res.path && file_system.exists?(res.path)
|
|
31
|
+
|
|
32
|
+
mentry = res.entry
|
|
33
|
+
raw = file_system.read(res.path)
|
|
34
|
+
parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
|
|
35
|
+
parsed["content"]
|
|
36
|
+
rescue Textus::Error
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.read_boot_context(manifest, file_system, _layout)
|
|
41
|
+
res = manifest.resolver.resolve("knowledge.boot")
|
|
42
|
+
return nil unless res.path && file_system.exists?(res.path)
|
|
43
|
+
|
|
44
|
+
mentry = res.entry
|
|
45
|
+
raw = file_system.read(res.path)
|
|
46
|
+
parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
|
|
47
|
+
body = parsed["body"]&.strip
|
|
48
|
+
body.nil? || body.empty? ? nil : body
|
|
49
|
+
rescue Textus::Error
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.inline_boot_content(manifest, _latest_seq)
|
|
54
|
+
agent_role = manifest.policy.proposer_role
|
|
55
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
|
|
56
|
+
next unless agent_role
|
|
57
|
+
|
|
58
|
+
writers = manifest.policy.writers_for(ln)
|
|
59
|
+
acc << ln if writers.include?(agent_role)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
author_writers = manifest.policy.writers_for("knowledge")
|
|
63
|
+
agent_has_author = agent_role && author_writers.include?(agent_role)
|
|
64
|
+
write_verbs = if agent_role
|
|
65
|
+
mcp_verbs = Textus::Protocol::VerbRegistry.registered.select { |s| s.write? && s.mcp? }
|
|
66
|
+
unless agent_has_author
|
|
67
|
+
forbidden = %i[accept reject].to_set.freeze
|
|
68
|
+
mcp_verbs = mcp_verbs.reject { |s| forbidden.include?(s.verb) }
|
|
69
|
+
end
|
|
70
|
+
mcp_verbs.map { |s| s.verb.to_s }
|
|
71
|
+
else
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
meta_glossary = {
|
|
76
|
+
"knowledge.loop.adoption" => { "node" => "target loop node", "confidence" => "high|medium|low",
|
|
77
|
+
"signal" => "observation type", "summary" => "brief description" },
|
|
78
|
+
"scratchpad.proposals" => { "proposal" => "{ target_key, action }" },
|
|
79
|
+
"raw" => { "source" => "{ kind, url, path, label }" },
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
blueprints = manifest.data.entries.map do |entry|
|
|
83
|
+
bp = { "key_prefix" => entry.key, "format" => entry.format.to_s, "naming" => entry.naming.to_s }
|
|
84
|
+
bp["schema"] = entry.schema.to_s if entry.schema
|
|
85
|
+
meta = meta_glossary[entry.key]
|
|
86
|
+
bp["meta"] = meta if meta
|
|
87
|
+
bp
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
"lanes" => lanes_for(manifest),
|
|
92
|
+
"entry_blueprints" => blueprints,
|
|
93
|
+
"workflow_tips" => [
|
|
94
|
+
{ "when" => "write an entry", "then" => "`data` param: String for body, Hash for content" },
|
|
95
|
+
{ "when" => "edit a protected path", "then" => "`textus propose key=my-change data=...` — human reviews and accepts" },
|
|
96
|
+
{ "when" => "explore recent changes", "then" => "`textus pulse` then `textus get <changed_key>`" },
|
|
97
|
+
{ "when" => "find related entries", "then" => "`textus graph key=<key> depth=2`" },
|
|
98
|
+
],
|
|
99
|
+
"agent_quickstart" => {
|
|
100
|
+
"read_verbs" => Textus::Protocol::VerbRegistry.registered.select { |s| s.read? && s.mcp? }.map { |s| s.verb.to_s },
|
|
101
|
+
"write_verbs" => write_verbs,
|
|
102
|
+
"writable_lanes" => writable_lanes,
|
|
103
|
+
"propose_lane" => manifest.policy.propose_lane_for(agent_role),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.lanes_for(manifest)
|
|
109
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
110
|
+
row = { "name" => name, "writers" => manifest.policy.writers_for(name) }
|
|
111
|
+
purpose = manifest.data.lane_descs[name]
|
|
112
|
+
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
113
|
+
row
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class NullEntryIndex
|
|
118
|
+
def record(...) = nil
|
|
119
|
+
def remove(...) = nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Boot
|
|
4
|
+
module Protocol
|
|
5
|
+
ENVELOPE_SHAPE = {
|
|
6
|
+
"envelope_shape" => {
|
|
7
|
+
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
8
|
+
"fields" => {
|
|
9
|
+
"_meta" => "hash of structured frontmatter; schema-validated per entry family",
|
|
10
|
+
"body" => "string payload (markdown/text) or nil for json/yaml formats",
|
|
11
|
+
"uid" => "stable 16-char hex identifier; preserved across writes and key renames",
|
|
12
|
+
"etag" => "content hash; pass back on writes to detect concurrent edits",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.build(manifest)
|
|
18
|
+
ENVELOPE_SHAPE.merge(
|
|
19
|
+
"lanes" => lanes_for(manifest),
|
|
20
|
+
"roles" => Textus::Protocol::V4::LANES.values.flat_map { |r| r[:writers] }.uniq,
|
|
21
|
+
"discipline" => "When edit/write tools are blocked for a path, use `textus propose` to " \
|
|
22
|
+
"suggest changes for human review. Never use shell scripts (sed, python, rm) " \
|
|
23
|
+
"to bypass permission gates — that breaks the audit trail and bypasses role gates.",
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.lanes_for(manifest)
|
|
28
|
+
Context.lanes_for(manifest)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative "boot/catalog"
|
|
2
|
+
require_relative "boot/protocol"
|
|
3
|
+
require_relative "boot/context"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Protocol
|
|
7
|
+
module Boot
|
|
8
|
+
PROTOCOL_ID = PROTOCOL
|
|
9
|
+
CURATED_CLI_VERBS = Catalog::CURATED_CLI_VERBS
|
|
10
|
+
|
|
11
|
+
def self.contract_summaries
|
|
12
|
+
Textus::Protocol::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.build(container:)
|
|
16
|
+
manifest = container.manifest
|
|
17
|
+
envelope = Context.build(
|
|
18
|
+
manifest:,
|
|
19
|
+
event_store: container.store.audit_log,
|
|
20
|
+
file_system: container.store.file_system,
|
|
21
|
+
layout: container.layout,
|
|
22
|
+
)
|
|
23
|
+
envelope.merge(
|
|
24
|
+
"catalog" => Catalog.build,
|
|
25
|
+
"agent_protocol" => Protocol.build(manifest),
|
|
26
|
+
"schemas" => build_schemas(container.schemas),
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.build_schemas(registry)
|
|
31
|
+
return [] unless registry
|
|
32
|
+
|
|
33
|
+
registry.all.map do |s|
|
|
34
|
+
h = { "name" => s.name, "required" => s.required, "optional" => s.optional }
|
|
35
|
+
h["fields"] = s.fields unless s.fields.empty?
|
|
36
|
+
h["evolution"] = s.evolution unless s.evolution.empty?
|
|
37
|
+
h
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Builder
|
|
4
|
+
class Domain
|
|
5
|
+
Components = Data.define(:manifest, :schemas, :workflows, :workflow_registry)
|
|
6
|
+
|
|
7
|
+
def call(root, layout:)
|
|
8
|
+
manifest = Manifest.load(root)
|
|
9
|
+
schemas = Schema::Registry.new(layout.schemas_dir)
|
|
10
|
+
workflows = Workflow::Registry.load_all(root)
|
|
11
|
+
workflow_registry = Workflow::Registry.new(workflows)
|
|
12
|
+
Components.new(manifest:, schemas:, workflows:, workflow_registry:)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Builder
|
|
4
|
+
class Infrastructure
|
|
5
|
+
Components = Data.define(:database, :file_system, :store, :layout)
|
|
6
|
+
|
|
7
|
+
def call(root)
|
|
8
|
+
layout = Layout.new(root)
|
|
9
|
+
database = Infra::Database.new(layout:).setup!
|
|
10
|
+
file_system = Infra::FileSystem.new
|
|
11
|
+
store = Infra::Store.new(database:, file_system:)
|
|
12
|
+
Components.new(database:, file_system:, store:, layout:)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Builder
|
|
4
|
+
class Runtime
|
|
5
|
+
Components = Data.define(:store_engine, :gate, :ttl_evaluator, :container)
|
|
6
|
+
|
|
7
|
+
def call(infra:, domain:, root:)
|
|
8
|
+
event_emitter = Textus::Workflow::EventEmitter.new(
|
|
9
|
+
infra.store.workflow_queue,
|
|
10
|
+
registry: domain.workflow_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
store_engine = StoreEngine.new(
|
|
14
|
+
file_system: infra.file_system, manifest: domain.manifest,
|
|
15
|
+
layout: infra.layout, store: infra.store,
|
|
16
|
+
event_emitter:, schemas: domain.schemas
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ttl_evaluator = Freshness::TtlEvaluator.new(
|
|
20
|
+
manifest: domain.manifest,
|
|
21
|
+
file_stat: infra.file_system,
|
|
22
|
+
clock: Infra::Clock.new,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
container = Container.new(
|
|
26
|
+
store: infra.store, layout: infra.layout,
|
|
27
|
+
manifest: domain.manifest, schemas: domain.schemas,
|
|
28
|
+
workflows: domain.workflows,
|
|
29
|
+
store_engine: store_engine,
|
|
30
|
+
root:, ttl_evaluator:
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
gate = Gate.new(
|
|
34
|
+
manifest: domain.manifest,
|
|
35
|
+
resolver: domain.manifest.resolver,
|
|
36
|
+
container:,
|
|
37
|
+
store_engine:,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
container.gate = gate
|
|
41
|
+
Components.new(store_engine:, gate:, ttl_evaluator:, container:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Container
|
|
4
|
+
attr_accessor :gate
|
|
5
|
+
attr_reader :store, :layout, :manifest, :schemas, :workflows, :workflow_registry, :root, :ttl_evaluator, :store_engine
|
|
6
|
+
|
|
7
|
+
def initialize(store:, layout:, manifest:, schemas:, workflows:,
|
|
8
|
+
root: nil, gate: nil, ttl_evaluator: nil, store_engine: nil)
|
|
9
|
+
@store = store
|
|
10
|
+
@layout = layout
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
@schemas = schemas
|
|
13
|
+
@workflows = workflows
|
|
14
|
+
@workflow_registry = workflows ? Textus::Workflow::Registry.new(workflows) : nil
|
|
15
|
+
@root = root
|
|
16
|
+
@gate = gate
|
|
17
|
+
@ttl_evaluator = ttl_evaluator
|
|
18
|
+
@store_engine = store_engine
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Diff
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def body(a_text, b_text)
|
|
7
|
+
a_lines = (a_text || "").lines.map(&:chomp)
|
|
8
|
+
b_lines = (b_text || "").lines.map(&:chomp)
|
|
9
|
+
hunks = build_hunks(myers_diff(a_lines, b_lines), a_lines, b_lines)
|
|
10
|
+
{ "type" => "body", "hunks" => hunks, "stats" => stats(hunks) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def meta(a_hash, b_hash)
|
|
14
|
+
a = a_hash || {}
|
|
15
|
+
b = b_hash || {}
|
|
16
|
+
all_keys = (a.keys + b.keys).uniq.sort
|
|
17
|
+
changes = all_keys.filter_map do |k|
|
|
18
|
+
av = a[k]
|
|
19
|
+
bv = b[k]
|
|
20
|
+
next if av == bv
|
|
21
|
+
|
|
22
|
+
{ "key" => k.to_s, "old" => av, "new" => bv }
|
|
23
|
+
end
|
|
24
|
+
{ "type" => "meta", "changes" => changes, "stats" => { "changed" => changes.size } }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def schema(a_schema, b_schema)
|
|
28
|
+
meta(a_schema, b_schema).merge("type" => "schema")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def summary(diff_result)
|
|
32
|
+
parts = []
|
|
33
|
+
parts << "#{diff_result.dig("body", "stats", "additions") || 0}+" if diff_result["body"]
|
|
34
|
+
parts << "#{diff_result.dig("body", "stats", "deletions") || 0}-" if diff_result["body"]
|
|
35
|
+
parts << "#{diff_result.dig("meta", "stats", "changed") || 0}~" if diff_result["meta"]
|
|
36
|
+
parts.join(" ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def myers_diff(a_lines, b_lines)
|
|
40
|
+
a_count = a_lines.size
|
|
41
|
+
b_count = b_lines.size
|
|
42
|
+
max_d = a_count + b_count
|
|
43
|
+
v = Hash.new(0)
|
|
44
|
+
v[1] = 0
|
|
45
|
+
trace = []
|
|
46
|
+
|
|
47
|
+
(0..max_d).each do |d|
|
|
48
|
+
(-d..d).step(2).each do |k|
|
|
49
|
+
x = if k == -d || (k != d && v[k - 1] < v[k + 1])
|
|
50
|
+
v[k + 1]
|
|
51
|
+
else
|
|
52
|
+
v[k - 1] + 1
|
|
53
|
+
end
|
|
54
|
+
y = x - k
|
|
55
|
+
while x < a_count && y < b_count && a_lines[x] == b_lines[y]
|
|
56
|
+
x += 1
|
|
57
|
+
y += 1
|
|
58
|
+
end
|
|
59
|
+
v[k] = x
|
|
60
|
+
return backtrack(trace, x, y) if x >= a_count && y >= b_count
|
|
61
|
+
end
|
|
62
|
+
trace << v.dup
|
|
63
|
+
end
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def backtrack(trace, x_pos, y_pos)
|
|
68
|
+
script = []
|
|
69
|
+
d = trace.size - 1
|
|
70
|
+
return script if d.negative?
|
|
71
|
+
|
|
72
|
+
k = x_pos - y_pos
|
|
73
|
+
while d.positive?
|
|
74
|
+
prev_v = trace[d - 1]
|
|
75
|
+
prev_k = if k == -d || (k != d && (prev_v[k - 1] || -1) < (prev_v[k + 1] || -1))
|
|
76
|
+
k + 1
|
|
77
|
+
else
|
|
78
|
+
k - 1
|
|
79
|
+
end
|
|
80
|
+
prev_x = prev_v[prev_k] || 0
|
|
81
|
+
prev_y = prev_x - prev_k
|
|
82
|
+
|
|
83
|
+
while x_pos > prev_x && y_pos > prev_y
|
|
84
|
+
script.unshift([:eq, x_pos - 1, y_pos - 1])
|
|
85
|
+
x_pos -= 1
|
|
86
|
+
y_pos -= 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if x_pos > prev_x
|
|
90
|
+
script.unshift([:del, x_pos - 1, nil])
|
|
91
|
+
x_pos -= 1
|
|
92
|
+
elsif y_pos > prev_y
|
|
93
|
+
script.unshift([:ins, nil, y_pos - 1])
|
|
94
|
+
y_pos -= 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
d -= 1
|
|
98
|
+
k = prev_k
|
|
99
|
+
x_pos = prev_x
|
|
100
|
+
y_pos = prev_y
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
script
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_hunks(script, a_lines, b_lines)
|
|
107
|
+
return [] if script.empty?
|
|
108
|
+
|
|
109
|
+
hunks = []
|
|
110
|
+
i = 0
|
|
111
|
+
while i < script.size
|
|
112
|
+
next_context = find_next_change(script, i)
|
|
113
|
+
break unless next_context
|
|
114
|
+
|
|
115
|
+
start = [next_context - 3, 0].max
|
|
116
|
+
hunk_end_script = find_hunk_end(script, next_context)
|
|
117
|
+
hunk_end = [hunk_end_script + 3, script.size - 1].min
|
|
118
|
+
|
|
119
|
+
a_start = nil
|
|
120
|
+
b_start = nil
|
|
121
|
+
a_lines_hunk = []
|
|
122
|
+
b_lines_hunk = []
|
|
123
|
+
|
|
124
|
+
(start..hunk_end).each do |j|
|
|
125
|
+
op, ai, bi = script[j]
|
|
126
|
+
a_start ||= ai if ai
|
|
127
|
+
b_start ||= bi if bi
|
|
128
|
+
|
|
129
|
+
case op
|
|
130
|
+
when :eq
|
|
131
|
+
a_lines_hunk << { "type" => "context", "line" => a_lines[ai] }
|
|
132
|
+
b_lines_hunk << { "type" => "context", "line" => b_lines[bi] }
|
|
133
|
+
when :del
|
|
134
|
+
a_lines_hunk << { "type" => "deletion", "line" => a_lines[ai] }
|
|
135
|
+
when :ins
|
|
136
|
+
b_lines_hunk << { "type" => "addition", "line" => b_lines[bi] }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
hunks << {
|
|
141
|
+
"a_start" => a_start, "b_start" => b_start,
|
|
142
|
+
"a_lines" => a_lines_hunk, "b_lines" => b_lines_hunk
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
i = hunk_end + 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
hunks
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def find_next_change(script, from)
|
|
152
|
+
(from...script.size).find { |i| script[i][0] != :eq }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def find_hunk_end(script, from)
|
|
156
|
+
last_change = from
|
|
157
|
+
(from...[script.size, from + 6].min).each do |i|
|
|
158
|
+
last_change = i if script[i][0] != :eq
|
|
159
|
+
end
|
|
160
|
+
last_change
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def stats(hunks)
|
|
164
|
+
additions = hunks.sum { |h| h["b_lines"].count { |l| l["type"] == "addition" } }
|
|
165
|
+
deletions = hunks.sum { |h| h["a_lines"].count { |l| l["type"] == "deletion" } }
|
|
166
|
+
{ "additions" => additions, "deletions" => deletions, "hunks" => hunks.size }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module EntryConstraint
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def validate_naming!(key, mentry)
|
|
7
|
+
naming = mentry.naming
|
|
8
|
+
return unless naming
|
|
9
|
+
return unless key.start_with?("#{mentry.key}.")
|
|
10
|
+
|
|
11
|
+
last = key.split(".").last
|
|
12
|
+
|
|
13
|
+
case naming
|
|
14
|
+
when "sequential"
|
|
15
|
+
return if last.match?(/\A\d{4}-[a-z][a-z0-9-]*\z/)
|
|
16
|
+
|
|
17
|
+
raise UsageError.new(
|
|
18
|
+
"naming violation: '#{key}' last segment must match NNNN-slug " \
|
|
19
|
+
"(entry '#{mentry.key}' has naming: sequential)",
|
|
20
|
+
)
|
|
21
|
+
when "dated"
|
|
22
|
+
return if last.match?(/\A\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*\z/)
|
|
23
|
+
|
|
24
|
+
raise UsageError.new(
|
|
25
|
+
"naming violation: '#{key}' last segment must match YYYY-MM-DD-slug " \
|
|
26
|
+
"(entry '#{mentry.key}' has naming: dated)",
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_schema!(payload, mentry, schemas:)
|
|
32
|
+
schema_name = mentry.schema
|
|
33
|
+
return unless schema_name && schemas
|
|
34
|
+
|
|
35
|
+
schema = schemas.fetch_or_nil(schema_name)
|
|
36
|
+
return unless schema
|
|
37
|
+
return if mentry.lane == "scratchpad"
|
|
38
|
+
|
|
39
|
+
schema.validate!(payload.meta || {})
|
|
40
|
+
rescue Textus::SchemaViolation
|
|
41
|
+
raise if mentry.lane != "scratchpad"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_format!(payload, mentry, format: nil)
|
|
45
|
+
fmt_name = format || mentry.format
|
|
46
|
+
fmt = Textus::Protocol::Format.for(fmt_name)
|
|
47
|
+
fmt.validate_raw_entry!(
|
|
48
|
+
{ "_meta" => payload.meta, "content" => payload.content },
|
|
49
|
+
mentry.lane,
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "dry-struct"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Envelope < Dry::Struct
|
|
6
|
+
attribute :protocol, Textus::Value::Types::String
|
|
7
|
+
attribute :key, Textus::Value::Types::String
|
|
8
|
+
attribute :lane, Textus::Value::Types::String
|
|
9
|
+
attribute :path, Textus::Value::Types::String
|
|
10
|
+
attribute :format, Textus::Value::Types::FormatName
|
|
11
|
+
attribute :etag, Textus::Value::Types::String
|
|
12
|
+
attribute :uid, Textus::Value::Types::String.optional
|
|
13
|
+
attribute :sources, Textus::Value::Types::Array.of(Textus::Value::Types::Any).optional
|
|
14
|
+
attribute :schema_ref, Textus::Value::Types::String.optional
|
|
15
|
+
attribute :meta, Textus::Value::Types::Hash.default({}.freeze)
|
|
16
|
+
attribute :body, Textus::Value::Types::String.optional
|
|
17
|
+
attribute :content, Textus::Value::Types::Any.optional
|
|
18
|
+
attribute :freshness, Textus::Value::Types::Any.optional
|
|
19
|
+
|
|
20
|
+
def self.build(key:, mentry:, path:, meta:, body:, etag:, content: nil, freshness: nil)
|
|
21
|
+
new(
|
|
22
|
+
protocol: Textus::PROTOCOL,
|
|
23
|
+
key:,
|
|
24
|
+
lane: mentry.lane,
|
|
25
|
+
path:,
|
|
26
|
+
format: mentry.format,
|
|
27
|
+
uid: extract_uid(meta),
|
|
28
|
+
sources: extract_sources(meta),
|
|
29
|
+
etag:,
|
|
30
|
+
schema_ref: mentry.schema,
|
|
31
|
+
meta: meta || {},
|
|
32
|
+
body:,
|
|
33
|
+
content:,
|
|
34
|
+
freshness:,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def with(**attrs) = self.class.new(to_h.merge(attrs))
|
|
39
|
+
|
|
40
|
+
def to_h_for_wire
|
|
41
|
+
h = {
|
|
42
|
+
"protocol" => protocol, "key" => key, "lane" => lane,
|
|
43
|
+
"path" => path, "format" => format, "_meta" => meta,
|
|
44
|
+
"body" => body, "etag" => etag, "schema_ref" => schema_ref, "uid" => uid
|
|
45
|
+
}
|
|
46
|
+
h["sources"] = sources if sources
|
|
47
|
+
h["content"] = content unless content.nil?
|
|
48
|
+
freshness&.to_h_for_wire&.each { |k, v| h[k] = v }
|
|
49
|
+
h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stale? = freshness.is_a?(Hash) && freshness["stale"] == true
|
|
53
|
+
|
|
54
|
+
def fetching? = freshness.is_a?(Hash) && freshness["fetching"] == true
|
|
55
|
+
|
|
56
|
+
def self.extract_uid(meta)
|
|
57
|
+
v = meta.is_a?(Hash) ? meta["uid"] : nil
|
|
58
|
+
v.is_a?(String) ? v : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.extract_sources(meta)
|
|
62
|
+
v = meta.is_a?(Hash) ? meta["sources"] : nil
|
|
63
|
+
return nil unless v.is_a?(Array) && !v.empty?
|
|
64
|
+
|
|
65
|
+
valid = v.select { |s| s.is_a?(String) || (s.is_a?(Hash) && s["key"].is_a?(String)) }
|
|
66
|
+
valid.empty? ? nil : valid
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|