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,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,19 +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 =
|
|
63
|
-
result = spec.view(:cli).call(result, inputs) if spec.view(:cli)
|
|
62
|
+
result = Textus::Surface::Dispatch.call(s, spec.verb, **inputs)
|
|
64
63
|
verb_instance.emit(result)
|
|
65
|
-
rescue Textus::
|
|
64
|
+
rescue Textus::Protocol::MissingArgs => e
|
|
66
65
|
raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
|
|
67
66
|
end
|
|
68
67
|
|
|
@@ -93,7 +92,7 @@ module Textus
|
|
|
93
92
|
if arg.type == :boolean
|
|
94
93
|
effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
|
|
95
94
|
else
|
|
96
|
-
"--#{wire}
|
|
95
|
+
"--#{wire} VALUE"
|
|
97
96
|
end
|
|
98
97
|
end
|
|
99
98
|
|
|
@@ -128,16 +127,7 @@ module Textus
|
|
|
128
127
|
|
|
129
128
|
# Contract verbs whose CLI is a plain `< Verb` command, not a projection at
|
|
130
129
|
# all — composite reports assembled outside the contract.
|
|
131
|
-
|
|
132
|
-
# generic projection now generates it; the hand-authored CLI::Verb::Boot is
|
|
133
|
-
# deleted in ADR 0101.)
|
|
134
|
-
# (doctor retained: hand-authored to preserve --check=NAME flag spelling and
|
|
135
|
-
# the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
|
|
136
|
-
# projection cannot yet express; kept in ADR 0101 pending a future pass.)
|
|
137
|
-
# (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
|
|
138
|
-
# driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
|
|
139
|
-
# read-through that once also drove it.)
|
|
140
|
-
NON_PROJECTED_CLI = %i[doctor].freeze
|
|
130
|
+
NON_PROJECTED_CLI = %i[].freeze
|
|
141
131
|
|
|
142
132
|
# The installer skips generation for either category.
|
|
143
133
|
HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
|
|
@@ -146,7 +136,7 @@ module Textus
|
|
|
146
136
|
|
|
147
137
|
def install!
|
|
148
138
|
@installed ||= {}
|
|
149
|
-
Textus::VerbRegistry.registered.each do |spec|
|
|
139
|
+
Textus::Protocol::VerbRegistry.registered.each do |spec|
|
|
150
140
|
next unless spec.cli?
|
|
151
141
|
next if hand_authored?(spec.verb)
|
|
152
142
|
next if @installed[spec.verb]
|
|
@@ -156,17 +146,28 @@ module Textus
|
|
|
156
146
|
end
|
|
157
147
|
end
|
|
158
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
|
+
|
|
159
156
|
def install_for(spec)
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
162
|
non_positional = spec.args.reject(&:positional)
|
|
163
163
|
|
|
164
164
|
klass = Class.new(Base)
|
|
165
165
|
klass.spec = spec
|
|
166
|
+
klass.stdin_json = STDIN_VERBS.include?(spec.verb)
|
|
166
167
|
klass.command_name leaf
|
|
167
168
|
klass.parent_group group if group
|
|
168
169
|
klass.option :as_flag, "--as=ROLE"
|
|
169
|
-
klass.option :use_stdin, "--stdin" if
|
|
170
|
+
klass.option :use_stdin, "--stdin" if klass.stdin_json
|
|
170
171
|
non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
|
|
171
172
|
|
|
172
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,18 +3,13 @@ module Textus
|
|
|
3
3
|
class CLI
|
|
4
4
|
class Verb
|
|
5
5
|
class Get < Runner::Base
|
|
6
|
-
self.spec = Textus::VerbRegistry.for(:get)
|
|
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
|
-
spec = Textus::VerbRegistry.for(:get)
|
|
12
11
|
s = store.with_role(resolved_role(store))
|
|
13
|
-
|
|
14
|
-
result = spec.view(:cli).call(result, { key: key }) if spec.view(:cli)
|
|
15
|
-
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
16
|
-
|
|
17
|
-
emit(result)
|
|
12
|
+
emit(s.get(key: key))
|
|
18
13
|
end
|
|
19
14
|
end
|
|
20
15
|
end
|
|
@@ -3,22 +3,22 @@ module Textus
|
|
|
3
3
|
class CLI
|
|
4
4
|
class Verb
|
|
5
5
|
class Put < Runner::Base
|
|
6
|
-
self.spec = Textus::VerbRegistry.for(:put)
|
|
6
|
+
self.spec = Textus::Protocol::VerbRegistry.for(:put)
|
|
7
7
|
option :as_flag, "--as=ROLE"
|
|
8
8
|
option :use_stdin, "--stdin"
|
|
9
|
+
option :as_format, "--as-format=FORMAT"
|
|
9
10
|
|
|
10
11
|
def invoke(store)
|
|
11
12
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
12
13
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
13
14
|
|
|
14
15
|
payload = JSON.parse(@stdin.read)
|
|
15
|
-
|
|
16
|
-
inputs = { key: key, meta: payload["_meta"] || {},
|
|
17
|
-
|
|
16
|
+
Textus::Protocol::VerbRegistry.for(:put)
|
|
17
|
+
inputs = { key: key, meta: payload["_meta"] || {}, data: payload["data"],
|
|
18
|
+
if_etag: payload["if_etag"] }
|
|
19
|
+
inputs[:format] = as_format if as_format
|
|
18
20
|
s = store.with_role(resolved_role(store))
|
|
19
|
-
|
|
20
|
-
result = spec.view(:cli).call(result, inputs) if spec.view(:cli)
|
|
21
|
-
emit(result)
|
|
21
|
+
emit(s.put(**inputs))
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
class CLI
|
|
4
|
+
class Verb
|
|
5
|
+
class WebServe < Verb
|
|
6
|
+
command_name "serve"
|
|
7
|
+
parent_group Group::Web
|
|
8
|
+
option :as_flag, "--as=ROLE"
|
|
9
|
+
option :port, "--port=N"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
role = resolved_role(store, default: Textus::Value::Role::AGENT)
|
|
13
|
+
Textus::Surface::Web.set :textus_session, store.with_role(role)
|
|
14
|
+
Textus::Surface::Web.set :port, (port || 9292).to_i
|
|
15
|
+
Textus::Surface::Web.run!
|
|
16
|
+
0
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/textus/surface/cli.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Textus
|
|
|
40
40
|
# `textus hook list --root=X`). Without this, `order!` below only sees
|
|
41
41
|
# options before the first verb token, so a trailing `--root` reached the
|
|
42
42
|
# verb's own parser and raised InvalidOption (#161 F5). TEXTUS_ROOT already
|
|
43
|
-
# works everywhere via
|
|
43
|
+
# works everywhere via Session, so this brings the flag to parity.
|
|
44
44
|
@root_arg = extract_root!(argv)
|
|
45
45
|
|
|
46
46
|
# Define --version/--help ourselves so OptionParser doesn't intercept them
|
|
@@ -99,7 +99,29 @@ module Textus
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def store
|
|
102
|
-
@store ||=
|
|
102
|
+
@store ||= Textus::Protocol::Session.open(store_root)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def store_root
|
|
106
|
+
@store_root ||= explicit_root || discovered_root
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def explicit_root
|
|
110
|
+
@root_arg || ENV.fetch("TEXTUS_ROOT", nil)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def discovered_root
|
|
114
|
+
dir = File.expand_path(@cwd)
|
|
115
|
+
loop do
|
|
116
|
+
if File.directory?(File.join(dir, ".textus")) && File.exist?(File.join(dir, ".textus", "manifest.yaml"))
|
|
117
|
+
return File.join(dir, ".textus")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
parent = File.dirname(dir)
|
|
121
|
+
raise Textus::IoError.new("no .textus directory found from #{@cwd}") if parent == dir
|
|
122
|
+
|
|
123
|
+
dir = parent
|
|
124
|
+
end
|
|
103
125
|
end
|
|
104
126
|
|
|
105
127
|
def dispatch(klass, argv)
|
|
@@ -131,14 +153,11 @@ module Textus
|
|
|
131
153
|
textus blame KEY [--limit=N]
|
|
132
154
|
textus pulse [--since=N]
|
|
133
155
|
textus boot
|
|
134
|
-
textus doctor
|
|
135
156
|
textus drain [PREFIX] --as=ROLE
|
|
136
157
|
textus watch
|
|
137
|
-
textus jobs
|
|
138
|
-
|
|
139
158
|
textus key {delete,mv,mv-prefix,delete-prefix,uid}
|
|
140
159
|
textus rule {explain,lint,list}
|
|
141
|
-
textus schema {
|
|
160
|
+
textus schema {show}
|
|
142
161
|
textus data {mv}
|
|
143
162
|
textus mcp {serve}
|
|
144
163
|
HELP
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module Dispatch
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def call(session, verb_name, raw_args = nil, **bound)
|
|
7
|
+
if raw_args
|
|
8
|
+
spec = Textus::Protocol::VerbRegistry.for(verb_name.to_sym)
|
|
9
|
+
raise Textus::UsageError.new("unknown verb: #{verb_name}") unless spec
|
|
10
|
+
|
|
11
|
+
bound = Textus::Protocol::Binder.inputs_from_wire(spec, raw_args)
|
|
12
|
+
end
|
|
13
|
+
session.public_send(verb_name.to_sym, **bound)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
module Adapters
|
|
5
|
+
class McpAdapter
|
|
6
|
+
def server(name:, version:, tools:, resources:, server_context:)
|
|
7
|
+
::MCP::Server.new(
|
|
8
|
+
name: name,
|
|
9
|
+
version: version,
|
|
10
|
+
tools: tools,
|
|
11
|
+
resources: resources,
|
|
12
|
+
server_context: server_context,
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tool(name:, description:, input_schema:, &)
|
|
17
|
+
::MCP::Tool.define(
|
|
18
|
+
name: name,
|
|
19
|
+
description: description,
|
|
20
|
+
input_schema: input_schema,
|
|
21
|
+
&
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,19 +2,19 @@ module Textus
|
|
|
2
2
|
module Surface
|
|
3
3
|
module MCP
|
|
4
4
|
module Catalog
|
|
5
|
-
|
|
5
|
+
MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
|
|
6
6
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def specs
|
|
10
|
-
VerbRegistry.registered.select(&:mcp?)
|
|
10
|
+
Textus::Protocol::VerbRegistry.registered.select(&:mcp?)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def build_tools(mcp_server)
|
|
14
14
|
specs.map do |spec|
|
|
15
15
|
schema = spec.input_schema
|
|
16
16
|
schema = schema.reject { |k, v| k == :required && Array(v).empty? }
|
|
17
|
-
|
|
17
|
+
MCP_ADAPTER.tool(
|
|
18
18
|
name: spec.verb.to_s,
|
|
19
19
|
description: spec.summary,
|
|
20
20
|
input_schema: schema,
|
|
@@ -29,23 +29,23 @@ module Textus
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def read_verbs
|
|
32
|
-
VerbRegistry.registered
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Textus::Protocol::VerbRegistry.registered
|
|
33
|
+
.select { |s| s.read? && s.mcp? }
|
|
34
|
+
.map { |s| s.verb.to_s }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def write_verbs
|
|
38
|
-
VerbRegistry.registered
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Textus::Protocol::VerbRegistry.registered
|
|
39
|
+
.select { |s| s.write? && s.mcp? }
|
|
40
|
+
.map { |s| s.verb.to_s }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def call(name, store:, args:)
|
|
44
|
-
spec = VerbRegistry.for(name.to_sym)
|
|
44
|
+
spec = Textus::Protocol::VerbRegistry.for(name.to_sym)
|
|
45
45
|
raise ToolError.new("unknown tool: #{name}") unless spec&.mcp?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
rescue Textus::
|
|
47
|
+
Textus::Surface::Dispatch.call(store, name.to_sym, args)
|
|
48
|
+
rescue Textus::Protocol::MissingArgs => e
|
|
49
49
|
raise ToolError.new("#{name}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
50
50
|
rescue Textus::ContractDrift, Textus::CursorExpired
|
|
51
51
|
raise
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
class CursorManager
|
|
5
|
+
def initialize(store:, role:)
|
|
6
|
+
@store = store
|
|
7
|
+
@role = role
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resume!
|
|
11
|
+
saved = @store.container.store.cursor_store.get(@role)[:seq]
|
|
12
|
+
latest = @store.container.store.audit_log.latest[:seq]
|
|
13
|
+
@store.advance_cursor(saved) if saved.positive? && saved <= latest
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def advance!(seq:)
|
|
17
|
+
@store.advance_cursor(seq)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
module DriftAnnotation
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def apply(result, verb_name:, drifted:, is_read:, current_etag:, previous_etag:)
|
|
8
|
+
result["contract_drifted"] = drifted if verb_name == :pulse
|
|
9
|
+
|
|
10
|
+
if drifted && !is_read && verb_name != :boot
|
|
11
|
+
result["_warning"] = "contract drifted (was #{previous_etag}, now #{current_etag}); call boot() to re-arm"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|