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,104 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
module Put
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(key, mentry:, path:, payload:, deps:, if_etag: nil, fmt: nil)
|
|
8
|
+
format = fmt || mentry.format
|
|
9
|
+
|
|
10
|
+
validate_input(key, payload, format, mentry, path)
|
|
11
|
+
validate_schema(payload, mentry, deps)
|
|
12
|
+
|
|
13
|
+
existing_env = read_existing(key, mentry, path, format, deps)
|
|
14
|
+
meta, content = inject_meta(payload, existing_env, format, deps)
|
|
15
|
+
bytes, eff_meta, eff_body, eff_content = serialize(payload, meta, content, path, format)
|
|
16
|
+
|
|
17
|
+
etag_before = Pipeline.check_etag(key, path, if_etag, deps.file_system)
|
|
18
|
+
deps.file_system.write(path, bytes)
|
|
19
|
+
envelope = Envelope.build(
|
|
20
|
+
key:, mentry:, path:,
|
|
21
|
+
meta: eff_meta, body: eff_body,
|
|
22
|
+
etag: Value::Etag.for_bytes(bytes),
|
|
23
|
+
content: eff_content
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
append_audit(key, etag_before, envelope, deps)
|
|
27
|
+
index_entry(key, mentry, format, eff_body, eff_content, deps)
|
|
28
|
+
Pipeline.emit_event(deps, event_type: "entry.written",
|
|
29
|
+
pending_key: key, emit_key: key,
|
|
30
|
+
emit_lane: mentry.lane)
|
|
31
|
+
envelope
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_input(key, payload, format, mentry, path)
|
|
35
|
+
EntryConstraint.validate_naming!(key, mentry)
|
|
36
|
+
fmt = Format.for(format)
|
|
37
|
+
fmt.enforce_name_match!(path, payload.meta)
|
|
38
|
+
fmt.validate_raw_entry!(
|
|
39
|
+
{ "_meta" => payload.meta, "content" => payload.content },
|
|
40
|
+
mentry.lane,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_schema(payload, mentry, deps)
|
|
45
|
+
EntryConstraint.validate_schema!(payload, mentry, schemas: deps.schemas)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_existing(key, mentry, path, format, deps)
|
|
49
|
+
return nil unless path && deps.file_system.exists?(path)
|
|
50
|
+
|
|
51
|
+
raw = deps.file_system.read(path)
|
|
52
|
+
parsed = Format.for(format).parse(raw, path:)
|
|
53
|
+
Envelope.build(
|
|
54
|
+
key:, mentry:, path:,
|
|
55
|
+
meta: parsed["_meta"] || {},
|
|
56
|
+
body: parsed["body"] || "",
|
|
57
|
+
content: parsed["content"],
|
|
58
|
+
etag: Value::Etag.for_file(path)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inject_meta(payload, existing_env, format, deps)
|
|
63
|
+
existing_meta = existing_env ? existing_env.meta : {}
|
|
64
|
+
Infra::Store::Envelope::Meta.inject_all(
|
|
65
|
+
payload.meta || {}, payload.content, existing_meta,
|
|
66
|
+
format:,
|
|
67
|
+
etag_for: method(:etag_for_key).curry.call(deps)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def etag_for_key(deps, key)
|
|
72
|
+
path = deps.manifest.resolver.resolve(key).path
|
|
73
|
+
return nil unless deps.file_system.exists?(path)
|
|
74
|
+
|
|
75
|
+
Value::Etag.for_file(path)
|
|
76
|
+
rescue Textus::Error
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def serialize(payload, meta, content, path, format)
|
|
81
|
+
Format.for(format).serialize_for_put(
|
|
82
|
+
meta:, body: payload.body, content:, path:,
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def append_audit(key, etag_before, envelope, deps)
|
|
87
|
+
deps.audit_log.append(
|
|
88
|
+
role: deps.call.role, verb: "put", key:,
|
|
89
|
+
etag_before:, etag_after: envelope.etag,
|
|
90
|
+
correlation_id: deps.call.correlation_id
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def index_entry(key, mentry, format, eff_body, eff_content, deps)
|
|
95
|
+
content = eff_content ? eff_content.to_json : eff_body
|
|
96
|
+
deps.entry_index.record(
|
|
97
|
+
key:, lane: mentry.lane, format:,
|
|
98
|
+
schema: mentry.schema, content:
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
def initialize(file_system:, manifest:, layout:, store:, event_emitter: nil, schemas: nil)
|
|
5
|
+
@file_system = file_system
|
|
6
|
+
@manifest = manifest
|
|
7
|
+
@layout = layout
|
|
8
|
+
@store = store
|
|
9
|
+
@event_emitter = event_emitter
|
|
10
|
+
@schemas = schemas
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(key:)
|
|
14
|
+
result = resolve(key:)
|
|
15
|
+
path = result.path
|
|
16
|
+
return nil unless path && @file_system.exists?(path)
|
|
17
|
+
|
|
18
|
+
raw = @file_system.read(path)
|
|
19
|
+
parsed = Format.for(result.entry.format).parse(raw, path:)
|
|
20
|
+
Envelope.build(key:, mentry: result.entry, path:,
|
|
21
|
+
meta: parsed["_meta"] || {},
|
|
22
|
+
body: parsed["body"] || "",
|
|
23
|
+
content: parsed["content"],
|
|
24
|
+
etag: Value::Etag.for_file(path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exists?(key:)
|
|
28
|
+
path = resolve(key:).path
|
|
29
|
+
path && @file_system.exists?(path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def list(prefix: nil, lane: nil, q: nil, schema: nil)
|
|
33
|
+
if @store.entry_index && (q || schema)
|
|
34
|
+
rows = @store.entry_index.search(query: q, schema:, lane:, prefix:)
|
|
35
|
+
(rows || []).map { |r| { "key" => r["key"], "lane" => r["lane"] } }
|
|
36
|
+
else
|
|
37
|
+
rows = @manifest.resolver.enumerate(prefix:)
|
|
38
|
+
rows = rows.select { |r| r[:manifest_entry].lane == lane } if lane
|
|
39
|
+
rows.map { |r| { "key" => r[:key], "lane" => r[:manifest_entry].lane } }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve(key:) = @manifest.resolver.resolve(key)
|
|
44
|
+
def mentry_for(key:)= resolve(key:).entry
|
|
45
|
+
|
|
46
|
+
def put(key:, meta:, body:, content:, call:, if_etag: nil, format: nil)
|
|
47
|
+
result = resolve(key:)
|
|
48
|
+
payload = Value::Payload.new(meta: meta || {}, body:, content:)
|
|
49
|
+
deps = build_deps(reader: nil, call:)
|
|
50
|
+
|
|
51
|
+
if format
|
|
52
|
+
fmt_mod = Textus::Protocol::Format.for(format)
|
|
53
|
+
path = result.path.sub(/\.[^.]+$/, "") + fmt_mod.nested_ext(result.remaining.last)
|
|
54
|
+
Put.call(key, mentry: result.entry, path:, payload:, if_etag:, deps:, fmt: format)
|
|
55
|
+
else
|
|
56
|
+
Put.call(key, mentry: result.entry, path: result.path, payload:, if_etag:, deps:)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def delete(key:, call:, if_etag: nil)
|
|
61
|
+
deps = Pipeline::DeleteDeps.new(
|
|
62
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
63
|
+
layout: @layout, event_emitter: @event_emitter, call:
|
|
64
|
+
)
|
|
65
|
+
Delete.call(key, if_etag:, deps:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def move(from_key:, to_key:, call:, if_etag: nil, dry_run: false)
|
|
69
|
+
deps = Pipeline::MoveDeps.new(
|
|
70
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
71
|
+
layout: @layout, event_emitter: @event_emitter, reader: self, call:
|
|
72
|
+
)
|
|
73
|
+
Move.call(from_key:, to_key:, if_etag:, dry_run:, deps:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_deps(reader:, call:)
|
|
79
|
+
Pipeline::WriteDeps.new(
|
|
80
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
81
|
+
layout: @layout, event_emitter: @event_emitter, schemas: @schemas, reader:, call:
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module V4
|
|
4
|
+
LANES = {
|
|
5
|
+
"knowledge" => { writers: %w[human],
|
|
6
|
+
desc: "the maintained source of truth about the repo",
|
|
7
|
+
retention_ttl: nil },
|
|
8
|
+
"scratchpad" => { writers: %w[human agent],
|
|
9
|
+
desc: "the agent's own durable scratchpad (working memory)",
|
|
10
|
+
retention_ttl: nil },
|
|
11
|
+
"artifacts" => { writers: %w[automation],
|
|
12
|
+
desc: "machine-maintained computed outputs",
|
|
13
|
+
retention_ttl: 3600 * 24 * 30 },
|
|
14
|
+
"raw" => { writers: %w[human agent automation],
|
|
15
|
+
desc: "ingested external source material (write-once)",
|
|
16
|
+
retention_ttl: nil },
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
NAMING_PATTERNS = {
|
|
20
|
+
"sequential" => /\A\d{4}-[a-z0-9-]+\.\w+\z/,
|
|
21
|
+
"dated" => /\A\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.\w+\z/,
|
|
22
|
+
}.freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module VerbRegistry
|
|
4
|
+
ArgSpec = Data.define(
|
|
5
|
+
:name, :type, :required, :positional,
|
|
6
|
+
:description, :wire_name, :default, :source, :coerce, :cli_default
|
|
7
|
+
) do
|
|
8
|
+
def wire = wire_name || name
|
|
9
|
+
|
|
10
|
+
def self.arg(name:, type: String, required: false, positional: false,
|
|
11
|
+
description: nil, wire_name: nil,
|
|
12
|
+
default: nil, source: nil, coerce: nil, cli_default: nil)
|
|
13
|
+
new(name:, type:, required:, positional:,
|
|
14
|
+
description:, wire_name:, default:, source:, coerce:, cli_default:)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module VerbRegistry
|
|
6
|
+
CoreVerbs = {
|
|
7
|
+
audit: VerbSpec.new(
|
|
8
|
+
:audit, "Query the audit log with optional filters.",
|
|
9
|
+
[ArgSpec.arg(name: :key, description: "filter to rows for this key"),
|
|
10
|
+
ArgSpec.arg(name: :lane, description: "filter to keys in this lane"),
|
|
11
|
+
ArgSpec.arg(name: :role, description: "filter to rows written under this role"),
|
|
12
|
+
ArgSpec.arg(name: :verb, description: "filter to rows for this verb"),
|
|
13
|
+
ArgSpec.arg(name: :since, description: "ISO-8601 timestamp or relative offset (e.g. 1h, 30m)"),
|
|
14
|
+
ArgSpec.arg(name: :seq_since, type: Integer, description: "return rows with seq > this cursor value"),
|
|
15
|
+
ArgSpec.arg(name: :correlation_id, description: "filter to rows with this correlation_id"),
|
|
16
|
+
ArgSpec.arg(name: :limit, type: Integer, description: "maximum number of rows to return")],
|
|
17
|
+
%i[cli mcp], :read
|
|
18
|
+
),
|
|
19
|
+
|
|
20
|
+
blame: VerbSpec.new(
|
|
21
|
+
:blame, "Annotate audit rows with the git commit that introduced each file state.",
|
|
22
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key to blame"),
|
|
23
|
+
ArgSpec.arg(name: :limit, type: Integer, description: "maximum number of audit rows to return")],
|
|
24
|
+
%i[cli mcp], :read
|
|
25
|
+
),
|
|
26
|
+
|
|
27
|
+
boot: VerbSpec.new(
|
|
28
|
+
:boot, "Return the orientation contract: lanes, agent_quickstart, agent_protocol.",
|
|
29
|
+
[], %i[cli mcp], :read
|
|
30
|
+
),
|
|
31
|
+
|
|
32
|
+
data_mv: VerbSpec.new(
|
|
33
|
+
:data_mv, "Rename a data lane — manifest + files. Refuses if destination exists.",
|
|
34
|
+
[ArgSpec.arg(name: :from, required: true, positional: true, description: "current data lane name"),
|
|
35
|
+
ArgSpec.arg(name: :to, required: true, positional: true, description: "new data lane name"),
|
|
36
|
+
ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
|
|
37
|
+
description: "when true, returns planned zone move without applying")],
|
|
38
|
+
%i[cli mcp], :write
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
deps: VerbSpec.new(
|
|
42
|
+
:deps, "List the keys a derived entry depends on.",
|
|
43
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key of the derived entry whose source keys you want")],
|
|
44
|
+
%i[cli mcp], :read
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
drain: VerbSpec.new(
|
|
48
|
+
:drain, "Seed materialize + sweep jobs then drain the queue to empty.",
|
|
49
|
+
[], %i[cli mcp], :maintenance
|
|
50
|
+
),
|
|
51
|
+
|
|
52
|
+
get: VerbSpec.new(
|
|
53
|
+
:get, "Read one entry — on-disk read with freshness verdict.",
|
|
54
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key to read, e.g. 'knowledge.project'")],
|
|
55
|
+
%i[cli mcp], :read
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
graph: VerbSpec.new(
|
|
59
|
+
:graph, "Query the link graph — neighbors (direct links) or reachable (transitive, with optional depth limit).",
|
|
60
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key to query"),
|
|
61
|
+
ArgSpec.arg(name: :depth, type: Integer, description: "max traversal depth for reachable query (unlimited when omitted)")],
|
|
62
|
+
%i[cli mcp], :read
|
|
63
|
+
),
|
|
64
|
+
|
|
65
|
+
ingest: VerbSpec.new(
|
|
66
|
+
verb: :ingest, summary: "Capture external source material into the raw lane. Write-once.",
|
|
67
|
+
args: [ArgSpec.arg(name: :kind, required: true, positional: true, description: "source kind: url | file | asset"),
|
|
68
|
+
ArgSpec.arg(name: :slug, required: true, description: "human slug for the key suffix (kebab-case)"),
|
|
69
|
+
ArgSpec.arg(name: :url, description: "remote URL (required when kind=url)"),
|
|
70
|
+
ArgSpec.arg(name: :path, description: "local file path (required when kind=file or kind=asset)"),
|
|
71
|
+
ArgSpec.arg(name: :lane, description: "asset group subdirectory (required when kind=asset)"),
|
|
72
|
+
ArgSpec.arg(name: :label, description: "human label stored in source.label")],
|
|
73
|
+
surfaces: %i[cli mcp], category: :write, lane: :ingest
|
|
74
|
+
),
|
|
75
|
+
|
|
76
|
+
key_delete: VerbSpec.new(
|
|
77
|
+
:key_delete, "Delete one entry by key. Returns {ok, key, deleted}.",
|
|
78
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key to delete"),
|
|
79
|
+
ArgSpec.arg(name: :if_etag, description: "optimistic-concurrency guard: etag you last read")],
|
|
80
|
+
%i[cli mcp], :write
|
|
81
|
+
),
|
|
82
|
+
|
|
83
|
+
key_delete_prefix: VerbSpec.new(
|
|
84
|
+
:key_delete_prefix, "Bulk-delete every leaf key under prefix.",
|
|
85
|
+
[ArgSpec.arg(name: :prefix, required: true, positional: true, description: "every leaf key under this dotted prefix is deleted"),
|
|
86
|
+
ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
|
|
87
|
+
description: "when true, returns keys that would be deleted without deleting")],
|
|
88
|
+
%i[cli mcp], :write
|
|
89
|
+
),
|
|
90
|
+
|
|
91
|
+
key_mv: VerbSpec.new(
|
|
92
|
+
:key_mv, "Rename one entry (same zone + format). Refuses if target exists.",
|
|
93
|
+
[ArgSpec.arg(name: :old_key, required: true, positional: true, description: "current dotted key"),
|
|
94
|
+
ArgSpec.arg(name: :new_key, required: true, positional: true, description: "new dotted key (same zone and format)"),
|
|
95
|
+
ArgSpec.arg(name: :dry_run, type: :boolean, description: "when true, returns planned move without applying")],
|
|
96
|
+
%i[cli mcp], :write
|
|
97
|
+
),
|
|
98
|
+
|
|
99
|
+
key_mv_prefix: VerbSpec.new(
|
|
100
|
+
:key_mv_prefix, "Bulk-rename every leaf key under from_prefix to to_prefix.",
|
|
101
|
+
[ArgSpec.arg(name: :from_prefix, required: true, positional: true, description: "dotted prefix whose leaf keys are renamed"),
|
|
102
|
+
ArgSpec.arg(name: :to_prefix, required: true, positional: true, description: "dotted prefix the keys are renamed to"),
|
|
103
|
+
ArgSpec.arg(name: :dry_run, type: :boolean, default: false, description: "when true, returns planned moves without applying")],
|
|
104
|
+
%i[cli mcp], :write
|
|
105
|
+
),
|
|
106
|
+
|
|
107
|
+
list: VerbSpec.new(
|
|
108
|
+
:list, "List keys filtered by lane and/or prefix.",
|
|
109
|
+
[ArgSpec.arg(name: :prefix, description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.execution'"),
|
|
110
|
+
ArgSpec.arg(name: :lane, description: "restrict to one lane by name (see `boot` lanes)"),
|
|
111
|
+
ArgSpec.arg(name: :q, description: "full-text search query over entry content (FTS5)"),
|
|
112
|
+
ArgSpec.arg(name: :schema, description: "filter to entries whose schema matches this name")],
|
|
113
|
+
%i[cli mcp], :read
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
pulse: VerbSpec.new(
|
|
117
|
+
:pulse, "Delta since cursor — changed entries, pending proposals, index freshness.",
|
|
118
|
+
[ArgSpec.arg(name: :since, type: Integer, description: "audit seq to diff from; defaults to the session cursor")],
|
|
119
|
+
%i[cli mcp], :read
|
|
120
|
+
),
|
|
121
|
+
|
|
122
|
+
put: VerbSpec.new(
|
|
123
|
+
:put, "Create or update an entry. Schema-validated. Returns {uid, etag}.",
|
|
124
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"),
|
|
125
|
+
ArgSpec.arg(name: :meta, type: Hash, wire_name: :_meta,
|
|
126
|
+
description: "frontmatter; reads back as `_meta`. Schema-validated — call `schema KEY` first"),
|
|
127
|
+
ArgSpec.arg(name: :data, description: "unified payload — String for body (markdown/text), Hash for content (json/yaml)"),
|
|
128
|
+
ArgSpec.arg(name: :if_etag, description: "optimistic-concurrency guard; write rejected if entry changed since"),
|
|
129
|
+
ArgSpec.arg(name: :format, description: "override entry format for this write (e.g. script)")],
|
|
130
|
+
%i[cli mcp], :write
|
|
131
|
+
),
|
|
132
|
+
|
|
133
|
+
rdeps: VerbSpec.new(
|
|
134
|
+
:rdeps, "List the derived entries that depend on a key (reverse deps).",
|
|
135
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key whose dependents you want")],
|
|
136
|
+
%i[cli mcp], :read
|
|
137
|
+
),
|
|
138
|
+
|
|
139
|
+
schema_list: VerbSpec.new(
|
|
140
|
+
:schema_list, "List all registered schemas with their fields.",
|
|
141
|
+
[], %i[cli mcp], :read
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
schema_show: VerbSpec.new(
|
|
145
|
+
:schema_show, "Return the schema (field shape) for an entry's family.",
|
|
146
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "any key in the family whose schema you want")],
|
|
147
|
+
%i[cli mcp], :read
|
|
148
|
+
),
|
|
149
|
+
|
|
150
|
+
uid: VerbSpec.new(
|
|
151
|
+
:uid, "Return the stable UID of an entry without reading its body.",
|
|
152
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key")],
|
|
153
|
+
%i[cli mcp], :read
|
|
154
|
+
),
|
|
155
|
+
|
|
156
|
+
where: VerbSpec.new(
|
|
157
|
+
:where, "Resolve a key to its zone, owner, and path without reading the body.",
|
|
158
|
+
[ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key to locate")],
|
|
159
|
+
%i[cli mcp], :read
|
|
160
|
+
),
|
|
161
|
+
}.freeze
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module VerbRegistry
|
|
4
|
+
TYPE_MAP = {
|
|
5
|
+
String => "string", Integer => "integer", Hash => "object",
|
|
6
|
+
Array => "array", :boolean => "boolean"
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
SYSTEM_VERBS = %i[boot audit blame pulse drain workflow_spec].to_set.freeze
|
|
10
|
+
|
|
11
|
+
VerbSpec = Data.define(:verb, :summary, :args, :surfaces, :category) do
|
|
12
|
+
def initialize(verb:, summary:, args:, surfaces:, category:, lane: nil)
|
|
13
|
+
@lane = lane
|
|
14
|
+
super(verb:, summary:, args:, surfaces:, category:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def lane = @lane
|
|
18
|
+
def system? = SYSTEM_VERBS.include?(verb)
|
|
19
|
+
def mcp? = surfaces.include?(:mcp)
|
|
20
|
+
def cli? = surfaces.include?(:cli)
|
|
21
|
+
def cli_path = verb.to_s.tr("_", " ")
|
|
22
|
+
def cli_words = cli_path.split
|
|
23
|
+
def cli_group = cli_words.size > 1 ? cli_words.first : nil
|
|
24
|
+
def cli_leaf = cli_words.last
|
|
25
|
+
def required_args = args.select(&:required)
|
|
26
|
+
def read? = category == :read
|
|
27
|
+
def write? = category == :write
|
|
28
|
+
def maintenance? = category == :maintenance
|
|
29
|
+
|
|
30
|
+
def input_schema
|
|
31
|
+
props = args.to_h do |a|
|
|
32
|
+
json_type = VerbRegistry::TYPE_MAP[a.type] || "string"
|
|
33
|
+
h = { "type" => json_type }
|
|
34
|
+
h["description"] = a.description if a.description
|
|
35
|
+
[a.wire.to_s, h]
|
|
36
|
+
end
|
|
37
|
+
{ type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module VerbRegistry
|
|
4
|
+
require_relative "verb_registry/arg_spec"
|
|
5
|
+
require_relative "verb_registry/verb_spec"
|
|
6
|
+
|
|
7
|
+
VERBS = {}
|
|
8
|
+
POSITIONAL = {}
|
|
9
|
+
|
|
10
|
+
def self.register(spec)
|
|
11
|
+
VERBS[spec.verb] = spec
|
|
12
|
+
POSITIONAL[spec.verb] = spec.args.select(&:positional).map(&:name)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.for(verb) = VERBS[verb]
|
|
16
|
+
def self.positional_for(verb) = POSITIONAL[verb] || []
|
|
17
|
+
def self.summary_for(verb) = VERBS[verb]&.summary
|
|
18
|
+
def self.registered = VERBS.values
|
|
19
|
+
|
|
20
|
+
ENTRY_VERBS = %i[
|
|
21
|
+
get put list key_delete key_mv propose accept reject
|
|
22
|
+
audit blame where uid deps rdeps graph diff ingest loop
|
|
23
|
+
session_open session_close
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
OPS_VERBS = %i[
|
|
27
|
+
boot drain pulse data_mv
|
|
28
|
+
key_mv_prefix key_delete_prefix
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
VERB_DOMAIN = ENTRY_VERBS.to_h { |v| [v, :entry] }
|
|
32
|
+
.merge(OPS_VERBS.to_h { |v| [v, :ops] }).freeze
|
|
33
|
+
|
|
34
|
+
require_relative "verb_registry/core_verbs"
|
|
35
|
+
CoreVerbs.each_value { |spec| register(spec) }
|
|
36
|
+
|
|
37
|
+
require_relative "../lanes/knowledge/verbs"
|
|
38
|
+
require_relative "../lanes/scratchpad/verbs"
|
|
39
|
+
require_relative "../lanes/artifact/verbs"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -13,7 +13,7 @@ module Textus
|
|
|
13
13
|
# verb name — `spec.verb` remains the single source of dispatch.
|
|
14
14
|
class Base < Verb
|
|
15
15
|
class << self
|
|
16
|
-
attr_accessor :spec
|
|
16
|
+
attr_accessor :spec, :stdin_json
|
|
17
17
|
|
|
18
18
|
# ADR 0064: derive the CLI command name from the contract's cli_leaf
|
|
19
19
|
# when not set explicitly, so an escape-hatch class never restates its
|
|
@@ -50,17 +50,18 @@ module Textus
|
|
|
50
50
|
module_function
|
|
51
51
|
|
|
52
52
|
def dispatch(verb_instance, store, spec)
|
|
53
|
-
inputs = Textus::
|
|
53
|
+
inputs = Textus::Protocol::Binder.inputs_from_ordered(
|
|
54
54
|
spec, verb_instance.positional, verb_instance.flag_values(spec)
|
|
55
55
|
)
|
|
56
|
-
inputs = inputs.merge(Surface::CLI::Sources.from_stdin(spec, verb_instance.stdin)) if
|
|
56
|
+
inputs = inputs.merge(Surface::CLI::Sources.from_stdin(spec, verb_instance.stdin)) if verb_instance.class.stdin_json
|
|
57
57
|
inputs = Surface::CLI::Sources.acquire(spec, inputs)
|
|
58
58
|
inputs = apply_cli_defaults(spec, inputs)
|
|
59
59
|
role = verb_instance.resolved_role(store)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
s = store.with_role(role)
|
|
62
|
+
result = Textus::Surface::Dispatch.call(s, spec.verb, **inputs)
|
|
62
63
|
verb_instance.emit(result)
|
|
63
|
-
rescue Textus::
|
|
64
|
+
rescue Textus::Protocol::MissingArgs => e
|
|
64
65
|
raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -71,7 +72,7 @@ module Textus
|
|
|
71
72
|
# contract, not hidden in a hand class.
|
|
72
73
|
def apply_cli_defaults(spec, inputs)
|
|
73
74
|
spec.args.each_with_object(inputs.dup) do |a, h|
|
|
74
|
-
next if a.cli_default
|
|
75
|
+
next if a.cli_default.nil? || h.key?(a.name)
|
|
75
76
|
|
|
76
77
|
h[a.name] = a.cli_default
|
|
77
78
|
end
|
|
@@ -83,7 +84,7 @@ module Textus
|
|
|
83
84
|
# polarity so a verb that applies-by-default on the CLI but plans-by-default
|
|
84
85
|
# for agents (migrate, data_mv) gets a `--dry-run` flag, not `--no-dry-run`.
|
|
85
86
|
def effective_default(arg)
|
|
86
|
-
arg.cli_default
|
|
87
|
+
arg.cli_default.nil? ? arg.default : arg.cli_default
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def flagspec_for(arg)
|
|
@@ -91,7 +92,7 @@ module Textus
|
|
|
91
92
|
if arg.type == :boolean
|
|
92
93
|
effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
|
|
93
94
|
else
|
|
94
|
-
"--#{wire}
|
|
95
|
+
"--#{wire} VALUE"
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
98
|
|
|
@@ -126,16 +127,7 @@ module Textus
|
|
|
126
127
|
|
|
127
128
|
# Contract verbs whose CLI is a plain `< Verb` command, not a projection at
|
|
128
129
|
# all — composite reports assembled outside the contract.
|
|
129
|
-
|
|
130
|
-
# generic projection now generates it; the hand-authored CLI::Verb::Boot is
|
|
131
|
-
# deleted in ADR 0101.)
|
|
132
|
-
# (doctor retained: hand-authored to preserve --check=NAME flag spelling and
|
|
133
|
-
# the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
|
|
134
|
-
# projection cannot yet express; kept in ADR 0101 pending a future pass.)
|
|
135
|
-
# (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
|
|
136
|
-
# driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
|
|
137
|
-
# read-through that once also drove it.)
|
|
138
|
-
NON_PROJECTED_CLI = %i[doctor].freeze
|
|
130
|
+
NON_PROJECTED_CLI = %i[].freeze
|
|
139
131
|
|
|
140
132
|
# The installer skips generation for either category.
|
|
141
133
|
HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
|
|
@@ -144,10 +136,7 @@ module Textus
|
|
|
144
136
|
|
|
145
137
|
def install!
|
|
146
138
|
@installed ||= {}
|
|
147
|
-
Textus::
|
|
148
|
-
next unless action_class.respond_to?(:contract?) && action_class.contract?
|
|
149
|
-
|
|
150
|
-
spec = action_class.contract
|
|
139
|
+
Textus::Protocol::VerbRegistry.registered.each do |spec|
|
|
151
140
|
next unless spec.cli?
|
|
152
141
|
next if hand_authored?(spec.verb)
|
|
153
142
|
next if @installed[spec.verb]
|
|
@@ -157,17 +146,28 @@ module Textus
|
|
|
157
146
|
end
|
|
158
147
|
end
|
|
159
148
|
|
|
149
|
+
STDIN_VERBS = %i[propose].freeze
|
|
150
|
+
CLI_PATHS = {
|
|
151
|
+
uid: "key uid",
|
|
152
|
+
key_mv_prefix: "key mv-prefix",
|
|
153
|
+
key_delete_prefix: "key delete-prefix",
|
|
154
|
+
}.freeze
|
|
155
|
+
|
|
160
156
|
def install_for(spec)
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
cli_path = CLI_PATHS[spec.verb] || spec.cli_path
|
|
158
|
+
cli_words = cli_path.split
|
|
159
|
+
group_name = cli_words.size > 1 ? cli_words.first : nil
|
|
160
|
+
group = group_name ? ensure_group(group_name) : nil
|
|
161
|
+
leaf = cli_words.last
|
|
163
162
|
non_positional = spec.args.reject(&:positional)
|
|
164
163
|
|
|
165
164
|
klass = Class.new(Base)
|
|
166
165
|
klass.spec = spec
|
|
166
|
+
klass.stdin_json = STDIN_VERBS.include?(spec.verb)
|
|
167
167
|
klass.command_name leaf
|
|
168
168
|
klass.parent_group group if group
|
|
169
169
|
klass.option :as_flag, "--as=ROLE"
|
|
170
|
-
klass.option :use_stdin, "--stdin" if
|
|
170
|
+
klass.option :use_stdin, "--stdin" if klass.stdin_json
|
|
171
171
|
non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
|
|
172
172
|
|
|
173
173
|
# Anchor the anonymous class to a constant so descendants discovery is
|
|
@@ -25,10 +25,8 @@ module Textus
|
|
|
25
25
|
# Parse a cli_stdin :json envelope into a by-name inputs hash, mapping
|
|
26
26
|
# envelope keys (wire-names) to arg names.
|
|
27
27
|
def from_stdin(spec, stream)
|
|
28
|
-
return {} unless spec.cli_stdin == :json
|
|
29
|
-
|
|
30
28
|
raw = stream.read.to_s
|
|
31
|
-
return {} if raw.strip.empty?
|
|
29
|
+
return {} if raw.strip.empty?
|
|
32
30
|
|
|
33
31
|
envelope = JSON.parse(raw)
|
|
34
32
|
spec.args.each_with_object({}) do |a, h|
|
|
@@ -3,16 +3,13 @@ module Textus
|
|
|
3
3
|
class CLI
|
|
4
4
|
class Verb
|
|
5
5
|
class Get < Runner::Base
|
|
6
|
-
self.spec = Textus::
|
|
6
|
+
self.spec = Textus::Protocol::VerbRegistry.for(:get)
|
|
7
7
|
option :as_flag, "--as=ROLE"
|
|
8
8
|
|
|
9
9
|
def invoke(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
14
|
-
|
|
15
|
-
emit(result)
|
|
11
|
+
s = store.with_role(resolved_role(store))
|
|
12
|
+
emit(s.get(key: key))
|
|
16
13
|
end
|
|
17
14
|
end
|
|
18
15
|
end
|