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
data/lib/textus/format.rb
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
SEP = "---".freeze
|
|
4
|
-
|
|
5
|
-
STRATEGIES = {
|
|
6
|
-
"markdown" => -> { Format::Markdown },
|
|
7
|
-
"json" => -> { Format::Json },
|
|
8
|
-
"yaml" => -> { Format::Yaml },
|
|
9
|
-
"text" => -> { Format::Text },
|
|
10
|
-
}.freeze
|
|
11
|
-
|
|
12
|
-
EXT_TO_FORMAT = {
|
|
13
|
-
".md" => "markdown",
|
|
14
|
-
".json" => "json",
|
|
15
|
-
".yaml" => "yaml",
|
|
16
|
-
".yml" => "yaml",
|
|
17
|
-
".txt" => "text",
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
def self.for(format)
|
|
21
|
-
STRATEGIES.fetch(format.to_s) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.infer_from_extension(ext)
|
|
25
|
-
EXT_TO_FORMAT[ext]
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.formats
|
|
29
|
-
EXT_TO_FORMAT.values.uniq
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.parse(raw, path: nil, format: "markdown")
|
|
33
|
-
Format.for(format).parse(raw, path: path)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
|
|
37
|
-
Format.for(format).serialize(meta: meta, body: body, content: content)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def self.data_to_payload(data, format)
|
|
41
|
-
return { meta: {}, body: "", content: nil } if data.nil?
|
|
42
|
-
|
|
43
|
-
Format.for(format).data_to_payload(data)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
data/lib/textus/gate/auth.rb
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Gate
|
|
5
|
-
class Auth
|
|
6
|
-
FLOOR = {
|
|
7
|
-
put: %w[lane_writable_by raw_lane_ingest_only],
|
|
8
|
-
key_delete: %w[lane_deletable_by],
|
|
9
|
-
key_mv: %w[lane_writable_by raw_lane_ingest_only],
|
|
10
|
-
accept: %w[author_held],
|
|
11
|
-
reject: %w[author_held],
|
|
12
|
-
propose: %w[lane_writable_by raw_lane_ingest_only],
|
|
13
|
-
key_mv_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
14
|
-
key_delete_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
15
|
-
ingest: %w[lane_writable_by raw_write_once],
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
AuthContext = Struct.new(
|
|
19
|
-
:actor, :actor_caps, :lane_verb,
|
|
20
|
-
:action, :target, :envelope,
|
|
21
|
-
:mentry, :manifest,
|
|
22
|
-
keyword_init: true
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
def initialize(container)
|
|
26
|
-
@manifest = container.manifest
|
|
27
|
-
@schemas = container.schemas
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def check!(cmd)
|
|
31
|
-
key = extract_key(cmd)
|
|
32
|
-
return unless key
|
|
33
|
-
|
|
34
|
-
evaluate_predicates(
|
|
35
|
-
action: cmd.verb,
|
|
36
|
-
actor: cmd.role.to_s,
|
|
37
|
-
key: key,
|
|
38
|
-
envelope: nil,
|
|
39
|
-
extra: {},
|
|
40
|
-
)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Backward-compatible check for inline action auth (accept, put, etc.).
|
|
44
|
-
def check_action!(action:, actor:, key:, envelope: nil, extra: {})
|
|
45
|
-
evaluate_predicates(
|
|
46
|
-
action: action.to_sym,
|
|
47
|
-
actor: actor,
|
|
48
|
-
key: key,
|
|
49
|
-
envelope: envelope,
|
|
50
|
-
extra: extra,
|
|
51
|
-
)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def evaluate_predicates(action:, actor:, key:, envelope:, extra:)
|
|
57
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
58
|
-
lane_verb = @manifest.policy.verb_for_lane(mentry.lane.to_s)
|
|
59
|
-
actor_caps = Set.new(@manifest.data.role_caps.fetch(actor, []))
|
|
60
|
-
|
|
61
|
-
ctx = AuthContext.new(
|
|
62
|
-
actor:, actor_caps:, lane_verb:,
|
|
63
|
-
action:, target: key, envelope:,
|
|
64
|
-
mentry:, manifest: @manifest
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
failures = []
|
|
68
|
-
floor_preds = FLOOR.fetch(action, [])
|
|
69
|
-
rule_preds = rule_declared_predicates(action, key)
|
|
70
|
-
(floor_preds + rule_preds).uniq.each do |pred|
|
|
71
|
-
result = evaluate(pred, ctx, extra)
|
|
72
|
-
next if result[:pass]
|
|
73
|
-
raise result[:error] if result[:error]
|
|
74
|
-
|
|
75
|
-
failures << [pred, result[:reason]]
|
|
76
|
-
end
|
|
77
|
-
raise Textus::GuardFailed.new(failures) unless failures.empty?
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def extract_key(cmd)
|
|
81
|
-
cmd.params.key?(:pending_key) ? cmd.pending_key : cmd.key
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def rule_declared_predicates(action, key)
|
|
85
|
-
guard_map = @manifest.rules.for(key).guard
|
|
86
|
-
return [] if guard_map.nil?
|
|
87
|
-
|
|
88
|
-
Array(guard_map[action.to_s])
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def evaluate(pred_name, ctx, extra)
|
|
92
|
-
case pred_name
|
|
93
|
-
when "lane_writable_by" then evaluate_lane_writable_by(ctx)
|
|
94
|
-
when "author_held" then evaluate_author_held(ctx)
|
|
95
|
-
when "target_is_canon" then evaluate_target_is_canon(ctx)
|
|
96
|
-
when "etag_match" then evaluate_etag_match(ctx, extra)
|
|
97
|
-
when "schema_valid" then evaluate_schema_valid(ctx)
|
|
98
|
-
when "fresh_within" then { pass: true }
|
|
99
|
-
when "raw_lane_ingest_only" then evaluate_raw_lane_ingest_only(ctx)
|
|
100
|
-
when "raw_write_once" then evaluate_raw_write_once(ctx)
|
|
101
|
-
when "lane_deletable_by" then evaluate_lane_deletable_by(ctx)
|
|
102
|
-
else raise Textus::UsageError.new("unknown predicate '#{pred_name}'")
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def evaluate_lane_writable_by(ctx)
|
|
107
|
-
pass = ctx.actor_caps.include?(ctx.lane_verb.to_s)
|
|
108
|
-
return { pass: true } if pass
|
|
109
|
-
|
|
110
|
-
holders = @manifest.policy.roles_with_capability(ctx.lane_verb.to_s)
|
|
111
|
-
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane, verb: ctx.lane_verb, holders:) }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def evaluate_author_held(ctx)
|
|
115
|
-
holders = @manifest.policy.roles_with_capability("author")
|
|
116
|
-
pass = holders.include?(ctx.actor.to_s)
|
|
117
|
-
reason = if pass
|
|
118
|
-
nil
|
|
119
|
-
elsif holders.empty?
|
|
120
|
-
"no role holds the 'author' capability; #{ctx.action} is disabled"
|
|
121
|
-
else
|
|
122
|
-
"role '#{ctx.actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
|
|
123
|
-
end
|
|
124
|
-
{ pass:, reason: }
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def evaluate_target_is_canon(ctx)
|
|
128
|
-
kind = @manifest.policy.declared_kind(ctx.mentry.lane.to_s)
|
|
129
|
-
pass = kind == :canon
|
|
130
|
-
{ pass:, reason: pass ? nil : "target lane '#{ctx.mentry.lane}' is not canon (kind: #{kind})" }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def evaluate_etag_match(ctx, extra)
|
|
134
|
-
if_etag = extra[:if_etag]
|
|
135
|
-
return { pass: true } if if_etag.nil?
|
|
136
|
-
|
|
137
|
-
current = ctx.envelope&.etag
|
|
138
|
-
pass = current.nil? || current == if_etag
|
|
139
|
-
{ pass:, error: pass ? nil : Textus::EtagMismatch.new(ctx.target, if_etag, current) }
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def evaluate_schema_valid(ctx)
|
|
143
|
-
return { pass: true } unless ctx.envelope
|
|
144
|
-
|
|
145
|
-
schema_ref = ctx.mentry.schema
|
|
146
|
-
return { pass: true } unless schema_ref
|
|
147
|
-
|
|
148
|
-
schema = @schemas.fetch_or_nil(schema_ref)
|
|
149
|
-
return { pass: true } unless schema
|
|
150
|
-
|
|
151
|
-
frontmatter = ctx.envelope.meta&.dig("_meta") || ctx.envelope.meta || {}
|
|
152
|
-
begin
|
|
153
|
-
schema.validate!(frontmatter)
|
|
154
|
-
{ pass: true }
|
|
155
|
-
rescue Textus::SchemaViolation => e
|
|
156
|
-
{ pass: false, reason: schema_reason(e) }
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def evaluate_raw_lane_ingest_only(ctx)
|
|
161
|
-
return { pass: true } unless @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
162
|
-
return { pass: true } if ctx.action == :ingest
|
|
163
|
-
|
|
164
|
-
{ pass: false, error: Textus::Error.new(
|
|
165
|
-
:raw_lane_ingest_only,
|
|
166
|
-
"raw lane '#{ctx.mentry.lane}' only accepts `textus ingest` — " \
|
|
167
|
-
"use that verb instead of '#{ctx.action}'",
|
|
168
|
-
) }
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def evaluate_raw_write_once(ctx)
|
|
172
|
-
path = @manifest.resolver.resolve(ctx.target).path
|
|
173
|
-
return { pass: true } unless File.exist?(path)
|
|
174
|
-
|
|
175
|
-
{ pass: false, error: Textus::Error.new(
|
|
176
|
-
:raw_write_once,
|
|
177
|
-
"raw entry '#{ctx.target}' already exists; " \
|
|
178
|
-
"delete it first (`textus key-delete #{ctx.target}`), then re-ingest",
|
|
179
|
-
) }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Deletion authority: the lane's write capability OR the author capability.
|
|
183
|
-
# On raw-kind lanes only the author capability grants deletion (correction
|
|
184
|
-
# escape hatch); the lane's own verb (ingest) is write-only. On all other
|
|
185
|
-
# lane kinds the behaviour matches lane_writable_by — the lane's writer
|
|
186
|
-
# can delete as before.
|
|
187
|
-
def evaluate_lane_deletable_by(ctx)
|
|
188
|
-
is_raw = @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
189
|
-
pass = if is_raw
|
|
190
|
-
ctx.actor_caps.include?("author")
|
|
191
|
-
else
|
|
192
|
-
ctx.actor_caps.include?(ctx.lane_verb.to_s) || ctx.actor_caps.include?("author")
|
|
193
|
-
end
|
|
194
|
-
return { pass: true } if pass
|
|
195
|
-
|
|
196
|
-
extra_holders = is_raw ? ["author"] : [ctx.lane_verb.to_s, "author"]
|
|
197
|
-
holders = extra_holders.flat_map { |v| @manifest.policy.roles_with_capability(v) }.uniq
|
|
198
|
-
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane,
|
|
199
|
-
verb: ctx.lane_verb, holders:) }
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def schema_reason(err)
|
|
203
|
-
d = err.details
|
|
204
|
-
return err.message.dup unless d.is_a?(Hash)
|
|
205
|
-
return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
|
|
206
|
-
return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
|
|
207
|
-
|
|
208
|
-
err.message.dup
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|
data/lib/textus/gate.rb
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Gate
|
|
5
|
-
def initialize(container)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def dispatch(spec:, inputs:, role:, correlation_id: nil, session: nil, surface: nil)
|
|
10
|
-
resolved = Binder.bind(spec, inputs, session: session)
|
|
11
|
-
cmd = Value::Command.new(verb: spec.verb, params: resolved.freeze, role: role)
|
|
12
|
-
|
|
13
|
-
cmd = normalize_propose_key(cmd) if cmd.verb == :propose
|
|
14
|
-
action_class = Textus::Action::VERBS.fetch(cmd.verb) do
|
|
15
|
-
raise Textus::UsageError.new("unknown command verb: #{cmd.verb}")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
auth = Gate::Auth.new(@container)
|
|
19
|
-
auth.check!(cmd)
|
|
20
|
-
check_dispatch_auth(cmd, resolved, auth)
|
|
21
|
-
call_obj = build_call(cmd, correlation_id: correlation_id)
|
|
22
|
-
result = run_action(action_class, resolved, call_obj)
|
|
23
|
-
cascade(cmd, result, call_obj) if CASCADE_VERBS.include?(cmd.verb) && !call_obj.dry_run
|
|
24
|
-
return result unless surface
|
|
25
|
-
|
|
26
|
-
spec.view(surface).call(result, resolved)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
CASCADE_VERBS = %i[put propose accept reject key_mv key_delete].freeze
|
|
30
|
-
|
|
31
|
-
AUTH_KEYS = {
|
|
32
|
-
key_mv: ->(params) { [params[:old_key], params[:new_key]] },
|
|
33
|
-
ingest: ->(params) { Textus::Action::Ingest.dispatch_key(**params) },
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def check_dispatch_auth(cmd, resolved, auth)
|
|
39
|
-
return unless (resolver = AUTH_KEYS[cmd.verb])
|
|
40
|
-
|
|
41
|
-
keys = Array(resolver.call(resolved))
|
|
42
|
-
keys.each { |k| auth.check_action!(action: cmd.verb, actor: cmd.role, key: k) }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def normalize_propose_key(cmd)
|
|
46
|
-
return cmd if cmd.pending_key
|
|
47
|
-
|
|
48
|
-
zone = @container.manifest.policy.propose_lane_for(cmd.role.to_s)
|
|
49
|
-
key = zone ? "#{zone}.#{cmd.key}" : nil
|
|
50
|
-
cmd.with(params: cmd.params.merge(pending_key: key))
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def run_action(klass, params, call_obj)
|
|
54
|
-
result = klass.call(container: @container, call: call_obj, **params)
|
|
55
|
-
Value::Result.unwrap(result)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def build_call(cmd, correlation_id: nil)
|
|
59
|
-
dry_run = cmd.params.key?(:dry_run) ? !cmd.params[:dry_run].nil? : false
|
|
60
|
-
Textus::Value::Call.build(role: cmd.role, dry_run:, correlation_id: correlation_id)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def cascade(cmd, result, call)
|
|
64
|
-
key = result.is_a?(Hash) ? result["cascade_key"] : nil
|
|
65
|
-
key ||= cascade_key_from_params(cmd)
|
|
66
|
-
return unless key
|
|
67
|
-
|
|
68
|
-
rdeps_result = Textus::Action::Rdeps.call(container: @container, call: call, key: key)
|
|
69
|
-
rdeps = Value::Result.unwrap(rdeps_result).fetch("rdeps", [])
|
|
70
|
-
producible = rdeps.select { |dep_key| producible?(dep_key) }
|
|
71
|
-
producible.each do |dep_key|
|
|
72
|
-
Textus::Store::Jobs::Materialize.call(container: @container, call: call, key: dep_key)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def cascade_key_from_params(cmd)
|
|
77
|
-
case cmd.verb
|
|
78
|
-
when :put, :key_delete then cmd.params[:key]
|
|
79
|
-
when :key_mv then cmd.params[:new_key]
|
|
80
|
-
when :propose, :reject then cmd.params[:pending_key]
|
|
81
|
-
when :accept then nil
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def producible?(key)
|
|
86
|
-
entry = @container.manifest.resolver.resolve(key).entry
|
|
87
|
-
!entry.publish_tree.nil?
|
|
88
|
-
rescue Textus::Error
|
|
89
|
-
false
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
data/lib/textus/init.rb
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "pathname"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Init
|
|
6
|
-
ZONES = %w[knowledge notebook proposals artifacts raw].freeze
|
|
7
|
-
|
|
8
|
-
DEFAULT_MANIFEST = <<~YAML
|
|
9
|
-
version: textus/4
|
|
10
|
-
roles:
|
|
11
|
-
- { name: human, can: [author, propose] }
|
|
12
|
-
- { name: agent, can: [propose, keep] }
|
|
13
|
-
- { name: automation, can: [converge] }
|
|
14
|
-
lanes:
|
|
15
|
-
- { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
|
|
16
|
-
- { name: notebook, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
|
|
17
|
-
- { name: proposals, kind: queue, desc: "changes awaiting your accept" }
|
|
18
|
-
- { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
|
|
19
|
-
entries:
|
|
20
|
-
- { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf }
|
|
21
|
-
- { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
|
|
22
|
-
- { key: notebook.notes, path: data/notebook/notes, lane: notebook, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
23
|
-
- { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
24
|
-
# A per-host snapshot, populated by a registered workflow. Nested so it
|
|
25
|
-
# grows to a fleet — add leaves over SSH without renaming. tracked:false →
|
|
26
|
-
# gitignored (machine info can be sensitive/noisy) but still protocol-readable
|
|
27
|
-
# via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
|
|
28
|
-
- key: artifacts.feeds.machines
|
|
29
|
-
path: data/artifacts/feeds/machines
|
|
30
|
-
lane: artifacts
|
|
31
|
-
format: yaml
|
|
32
|
-
nested: true
|
|
33
|
-
tracked: false
|
|
34
|
-
kind: nested
|
|
35
|
-
rules: []
|
|
36
|
-
YAML
|
|
37
|
-
|
|
38
|
-
AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
|
|
39
|
-
- { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
40
|
-
- { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
41
|
-
- key: artifacts.derived.orientation
|
|
42
|
-
path: data/artifacts/derived/orientation.json
|
|
43
|
-
lane: artifacts
|
|
44
|
-
publish:
|
|
45
|
-
- { to: CLAUDE.md, template: orientation.erb, inject_boot: true }
|
|
46
|
-
- { to: AGENTS.md, template: orientation.erb, inject_boot: true }
|
|
47
|
-
kind: produced
|
|
48
|
-
YAML
|
|
49
|
-
|
|
50
|
-
def self.run(target_root, with_agent: false)
|
|
51
|
-
check_target!(target_root)
|
|
52
|
-
scaffold_dir = File.expand_path("init/templates", __dir__)
|
|
53
|
-
create_directories(target_root)
|
|
54
|
-
write_manifest(target_root, with_agent:)
|
|
55
|
-
mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
56
|
-
setup_state_dirs(target_root)
|
|
57
|
-
write_gitignore(target_root)
|
|
58
|
-
build_result(target_root, with_agent:, mcp_status:)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def self.check_target!(target_root)
|
|
62
|
-
raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
ASSET_ZONES = %w[raw].freeze
|
|
66
|
-
|
|
67
|
-
def self.create_directories(target_root)
|
|
68
|
-
FileUtils.mkdir_p(File.join(target_root, "schemas"))
|
|
69
|
-
FileUtils.mkdir_p(File.join(target_root, "templates"))
|
|
70
|
-
FileUtils.mkdir_p(File.join(target_root, "workflows"))
|
|
71
|
-
ZONES.each do |z|
|
|
72
|
-
dir = File.join(target_root, "data", z)
|
|
73
|
-
FileUtils.mkdir_p(dir)
|
|
74
|
-
File.write(File.join(dir, ".gitkeep"), "")
|
|
75
|
-
end
|
|
76
|
-
ASSET_ZONES.each do |z|
|
|
77
|
-
dir = File.join(target_root, "assets", z)
|
|
78
|
-
FileUtils.mkdir_p(dir)
|
|
79
|
-
File.write(File.join(dir, ".gitkeep"), "")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.write_manifest(target_root, with_agent:)
|
|
84
|
-
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
88
|
-
return nil unless with_agent
|
|
89
|
-
|
|
90
|
-
scaffold_agent_profile(target_root, scaffold_dir)
|
|
91
|
-
write_mcp_config(target_root, scaffold_dir)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.setup_state_dirs(target_root)
|
|
95
|
-
FileUtils.mkdir_p(Textus::Store::Geometry.new(target_root).audit_dir_path)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def self.write_gitignore(target_root)
|
|
99
|
-
File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def self.build_result(target_root, with_agent:, mcp_status:)
|
|
103
|
-
result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
|
|
104
|
-
result["mcp_config"] = mcp_status if with_agent
|
|
105
|
-
result
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Composes the agent profile by inserting AGENT_ENTRIES immediately before the
|
|
109
|
-
# top-level `rules:` block of DEFAULT_MANIFEST — that block is load-bearing for
|
|
110
|
-
# this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
|
|
111
|
-
def self.manifest_yaml(with_agent:)
|
|
112
|
-
return DEFAULT_MANIFEST unless with_agent
|
|
113
|
-
|
|
114
|
-
DEFAULT_MANIFEST.sub(/^rules:/, "#{AGENT_ENTRIES}rules:")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Copies the proven orientation bundle into a freshly-init'd store.
|
|
118
|
-
def self.scaffold_agent_profile(target_root, scaffold_dir)
|
|
119
|
-
{
|
|
120
|
-
"project.schema.yaml" => File.join("schemas", "project.yaml"),
|
|
121
|
-
"runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
|
|
122
|
-
"orientation.erb" => File.join("templates", "orientation.erb"),
|
|
123
|
-
}.each do |src, dest|
|
|
124
|
-
File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# The one file init writes outside .textus/: a starter .mcp.json at the
|
|
129
|
-
# project root. Write-once — never clobber a hand-authored config. The
|
|
130
|
-
# command form assumes a gem-installed `textus` on PATH; the user owns
|
|
131
|
-
# the file after this first write.
|
|
132
|
-
def self.write_mcp_config(target_root, scaffold_dir)
|
|
133
|
-
dest = File.join(File.dirname(target_root), ".mcp.json")
|
|
134
|
-
return "skipped" if File.exist?(dest)
|
|
135
|
-
|
|
136
|
-
File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
|
|
137
|
-
"written"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# The store's `.gitignore` is generated, never hand-kept (ADR 0038), and now
|
|
141
|
-
# derived from the manifest: the run subtree plus every `tracked: false`
|
|
142
|
-
# entry's resolved path (ADR 0043).
|
|
143
|
-
def self.derived_gitignore(target_root)
|
|
144
|
-
manifest = Textus::Manifest.load(target_root)
|
|
145
|
-
root = Pathname.new(target_root)
|
|
146
|
-
untracked = manifest.data.entries.reject(&:tracked?).map do |e|
|
|
147
|
-
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
|
|
148
|
-
rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
|
|
149
|
-
"#{rel}/"
|
|
150
|
-
else
|
|
151
|
-
Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
Textus::Store::Geometry.new(target_root).gitignore_body(untracked_entries: untracked)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
data/lib/textus/jobs.rb
DELETED
data/lib/textus/key/distance.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
# Small utilities for ranking key suggestions. Bounded inputs only —
|
|
4
|
-
# Levenshtein is O(n*m) so we refuse to compute on long strings.
|
|
5
|
-
class Distance
|
|
6
|
-
MAX_LEN = 200
|
|
7
|
-
|
|
8
|
-
# Length of the shared dot-separated prefix between two dotted keys.
|
|
9
|
-
def self.shared_prefix_segments(left, right)
|
|
10
|
-
asegs = left.split(".")
|
|
11
|
-
bsegs = right.split(".")
|
|
12
|
-
n = [asegs.length, bsegs.length].min
|
|
13
|
-
i = 0
|
|
14
|
-
i += 1 while i < n && asegs[i] == bsegs[i]
|
|
15
|
-
i
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
|
|
19
|
-
def self.levenshtein(left, right)
|
|
20
|
-
return nil if left.length > MAX_LEN || right.length > MAX_LEN
|
|
21
|
-
return right.length if left.empty?
|
|
22
|
-
return left.length if right.empty?
|
|
23
|
-
|
|
24
|
-
prev = (0..right.length).to_a
|
|
25
|
-
curr = Array.new(right.length + 1, 0)
|
|
26
|
-
(1..left.length).each do |i|
|
|
27
|
-
curr[0] = i
|
|
28
|
-
(1..right.length).each do |j|
|
|
29
|
-
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
30
|
-
curr[j] = [
|
|
31
|
-
curr[j - 1] + 1, # insertion
|
|
32
|
-
prev[j] + 1, # deletion
|
|
33
|
-
prev[j - 1] + cost, # substitution
|
|
34
|
-
].min
|
|
35
|
-
end
|
|
36
|
-
prev, curr = curr, prev
|
|
37
|
-
end
|
|
38
|
-
prev[right.length]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Rank candidate keys against requested. Returns up to `limit` keys.
|
|
42
|
-
# Sort: longer shared prefix first; then smaller Levenshtein distance.
|
|
43
|
-
def self.suggest(requested, candidates, limit: 5)
|
|
44
|
-
return [] if requested.nil? || requested.empty?
|
|
45
|
-
|
|
46
|
-
scored = candidates.first(200).map do |k|
|
|
47
|
-
prefix = shared_prefix_segments(requested, k)
|
|
48
|
-
dist = levenshtein(requested, k) || Float::INFINITY
|
|
49
|
-
[k, prefix, dist]
|
|
50
|
-
end
|
|
51
|
-
scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
data/lib/textus/key/grammar.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
module Grammar
|
|
4
|
-
SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
|
|
5
|
-
MAX_SEGMENTS = 8
|
|
6
|
-
MAX_SEGMENT_LEN = 64
|
|
7
|
-
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def validate!(key) # rubocop:disable Naming/PredicateMethod
|
|
11
|
-
raise UsageError.new("key must be a String") unless key.is_a?(String)
|
|
12
|
-
raise UsageError.new("empty key") if key.empty?
|
|
13
|
-
|
|
14
|
-
segs = key.split(".")
|
|
15
|
-
raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
|
|
16
|
-
|
|
17
|
-
segs.each do |seg|
|
|
18
|
-
if seg.empty?
|
|
19
|
-
raise UsageError.new("empty segment in key '#{key}'")
|
|
20
|
-
elsif seg.length > MAX_SEGMENT_LEN
|
|
21
|
-
raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
|
|
22
|
-
elsif !seg.match?(SEGMENT)
|
|
23
|
-
raise UsageError.new(
|
|
24
|
-
"invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
|
|
25
|
-
"(lowercase, digits, hyphens; no underscores or uppercase)",
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
true
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/textus/key/matching.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
# Dotted-key scope matching, shared by all prefix-scoped sweeps
|
|
4
|
-
# (WS4 / ADR 0089-era cleanup). Canonicalised here so every consumer
|
|
5
|
-
# uses a consistent dotted-boundary check with proper Nested ancestor
|
|
6
|
-
# handling. ADR 0093: Produce is the sole engine calling this.
|
|
7
|
-
module Matching
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
# Is `key` within the `prefix` scope?
|
|
11
|
-
# - exact match, or a dotted descendant (the `prefix.` boundary, so
|
|
12
|
-
# prefix "art" does NOT match key "artifacts"), and
|
|
13
|
-
# - for a nested entry, also when `prefix` descends INTO it — the nested
|
|
14
|
-
# parent owns the leaf the prefix names (e.g. prefix
|
|
15
|
-
# "feeds.machines.host1" still selects the nested entry
|
|
16
|
-
# "feeds.machines").
|
|
17
|
-
def matches_prefix?(key, prefix, nested: false)
|
|
18
|
-
return true if key == prefix || key.start_with?("#{prefix}.")
|
|
19
|
-
|
|
20
|
-
nested && prefix.start_with?("#{key}.")
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/textus/key/path.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
module Path
|
|
4
|
-
# Returns the absolute filesystem path for a manifest entry (the leaf file,
|
|
5
|
-
# not a nested directory). Adds the format's primary extension when the
|
|
6
|
-
# manifest entry's `path:` is extensionless.
|
|
7
|
-
#
|
|
8
|
-
# The first argument is a Manifest::Data (or anything responding to .root);
|
|
9
|
-
# callers historically passed the whole Manifest but should now pass
|
|
10
|
-
# `manifest.data`.
|
|
11
|
-
def self.resolve(data, mentry)
|
|
12
|
-
primary_ext = Format.for(mentry.format).extensions.first
|
|
13
|
-
rel_path = normalize_relative_path(mentry.path)
|
|
14
|
-
if File.extname(mentry.path) == ""
|
|
15
|
-
File.join(data.root, rel_path + primary_ext)
|
|
16
|
-
else
|
|
17
|
-
File.join(data.root, rel_path)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.normalize_relative_path(path)
|
|
22
|
-
return path if path.start_with?("data/")
|
|
23
|
-
|
|
24
|
-
File.join("data", path)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|