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
data/docs/architecture/README.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
# Textus architecture
|
|
2
|
-
|
|
3
|
-
> **Explanation** · for contributors · **read this first** for orientation before SPEC
|
|
4
|
-
> **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch paths) · **reviewed** 2026-06 (v0.54)
|
|
5
|
-
|
|
6
|
-
```mermaid
|
|
7
|
-
flowchart TD
|
|
8
|
-
surfaces["Surfaces — CLI · MCP · RoleScope"]
|
|
9
|
-
contract["Contract — per-verb DSL (Arg, Spec, DSL)"]
|
|
10
|
-
dispatch["Dispatch — Binder · Around · CommandBuilder · Dispatcher"]
|
|
11
|
-
gate["Gate — routing (VERB_COMMAND) · Auth"]
|
|
12
|
-
action["Actions — per-verb use cases (action/)"]
|
|
13
|
-
jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
|
|
14
|
-
produce["Produce — Engine · Render"]
|
|
15
|
-
workflow["Workflow — Runner · Registry · Loader · DSL"]
|
|
16
|
-
manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
|
|
17
|
-
core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
|
|
18
|
-
ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
|
|
19
|
-
surfaces --> dispatch
|
|
20
|
-
surfaces -.->|declare| contract
|
|
21
|
-
contract --> dispatch
|
|
22
|
-
dispatch --> gate
|
|
23
|
-
gate --> action
|
|
24
|
-
action --> jobs
|
|
25
|
-
action --> produce
|
|
26
|
-
produce --> workflow
|
|
27
|
-
action --> manifest
|
|
28
|
-
action --> core
|
|
29
|
-
action --> ports
|
|
30
|
-
jobs --> produce
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
*Dependency rule: inward only.* Surfaces → Dispatch → Gate → Actions → inner layers. Actions never reference surfaces.
|
|
34
|
-
|
|
35
|
-
### What lives in each layer
|
|
36
|
-
|
|
37
|
-
**Surfaces**
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
surfaces/cli/ CLI command generation from contracts
|
|
41
|
-
sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
|
|
42
|
-
surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
|
|
43
|
-
surfaces/role_scope.rb
|
|
44
|
-
(Store#with_role(role)) — holds (container, role, dry_run, correlation_id);
|
|
45
|
-
all verb methods injected via define_method in textus.rb
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**Contract + Dispatch**
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
contract/ Per-verb DSL — verb, summary, surfaces, arg, view, around.
|
|
52
|
-
Pure declaration: Spec and DSL only.
|
|
53
|
-
|
|
54
|
-
dispatch/ Runtime machinery consumed by every surface.
|
|
55
|
-
binder.rb Binder — validates required args, resolves session/literal defaults.
|
|
56
|
-
around.rb Around — registry of stateful wrappers (cursor, build_lock).
|
|
57
|
-
view.rb View.render(spec, surface, result, inputs) — output shaping.
|
|
58
|
-
command_builder.rb
|
|
59
|
-
CommandBuilder — command construction + role injection.
|
|
60
|
-
dispatcher.rb Dispatcher — unified pipeline: Around → Binder → Command → Gate.
|
|
61
|
-
resources/ Around resource implementations (Cursor, BuildLock).
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Gate + Auth**
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
gate.rb VERB_COMMAND (verb symbol → Command class)
|
|
68
|
-
Gate#dispatch(cmd) — Auth → action call
|
|
69
|
-
gate/auth.rb Authorization engine — FLOOR predicates + rule guards
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**Actions**
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
action/{get,list,put,key_delete,key_mv,accept,reject,propose,
|
|
76
|
-
drain,enqueue,audit,blame,deps,rdeps,published,boot,doctor,
|
|
77
|
-
rule_explain,rule_list,rule_lint,pulse,
|
|
78
|
-
data_mv,key_mv_prefix,key_delete_prefix,
|
|
79
|
-
schema_envelope,where,uid,jobs}.rb
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Jobs**
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
jobs/planner.rb Rules-driven job planning — seeds materialize/refresh/sweep jobs
|
|
86
|
-
jobs/worker.rb Queue drain — leases and runs jobs from JobStore
|
|
87
|
-
jobs/materialize.rb Per-key produce job → Produce::Engine.converge
|
|
88
|
-
jobs/refresh.rb Per-key refresh job (retention-rule-triggered)
|
|
89
|
-
jobs/retention.rb Retention policy check
|
|
90
|
-
jobs/sweep.rb Lane sweep job
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
**Produce**
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
produce/engine.rb Engine.converge(container:, call:, keys:)
|
|
97
|
-
For each key: looks up a registered workflow and runs it,
|
|
98
|
-
or publishes existing bytes for entries with publish targets.
|
|
99
|
-
produce/render.rb Mustache template expansion for publish targets
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Workflow**
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
workflow/runner.rb Runs a Definition: build_context → execute_steps → publish
|
|
106
|
-
workflow/registry.rb In-memory list of registered Definitions; Registry#for(key)
|
|
107
|
-
workflow/loader.rb Loads .textus/workflows/*.rb files via Collector DSL
|
|
108
|
-
workflow/dsl.rb Textus.workflow "name" { match "...", step :fetch { ... }, publish }
|
|
109
|
-
workflow/context.rb Context passed to each step (key, entry, config, container, call)
|
|
110
|
-
workflow/pattern.rb Key-matching pattern (glob via File.fnmatch?)
|
|
111
|
-
workflow/errors.rb NotFound, StepFailed
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**Core (pure value types)**
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
Freshness::{Verdict,Evaluator}
|
|
118
|
-
Core::Duration Core::Sentinel
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**Infrastructure**
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
Store (composition root — wires ports, vends Container)
|
|
125
|
-
Storage::FileStore (bytes-only port: read/write/delete/exists?/etag)
|
|
126
|
-
Manifest (Data, Resolver, Policy, Rules)
|
|
127
|
-
Schemas (eager-load cache)
|
|
128
|
-
Ports::{AuditLog,JobStore,Publisher,Clock,
|
|
129
|
-
BuildLock,SentinelStore}
|
|
130
|
-
Entry::{Markdown,Json,Yaml,Text} (format strategies)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## How a verb becomes a method
|
|
134
|
-
|
|
135
|
-
All actions live under `lib/textus/action/`. The shape is uniform:
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
module Textus
|
|
139
|
-
module Action
|
|
140
|
-
class Get < Base
|
|
141
|
-
extend Textus::Contract::DSL
|
|
142
|
-
|
|
143
|
-
verb :get
|
|
144
|
-
summary "Read an entry by key."
|
|
145
|
-
surfaces :cli, :mcp
|
|
146
|
-
arg :key, String, required: true, positional: true
|
|
147
|
-
|
|
148
|
-
BURN = :sync
|
|
149
|
-
|
|
150
|
-
def initialize(key:)
|
|
151
|
-
super()
|
|
152
|
-
@key = key
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def call(container:, call:)
|
|
156
|
-
...
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Verbs are looked up in a static frozen table (`Textus::Action::VERBS`) that maps `:get → Action::Get`, `:put → Action::Put`, etc. Adding a new verb is one entry in `VERBS` plus the class — no metaprogramming.
|
|
164
|
-
|
|
165
|
-
## Container
|
|
166
|
-
|
|
167
|
-
Use cases never see the raw `Store`. `Textus::Container` is a single record holding the wired collaborators:
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
Container = Data.define(
|
|
171
|
-
:manifest, :file_store, :schemas, :root,
|
|
172
|
-
:audit_log, :workflows, :gate
|
|
173
|
-
)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
The `Store` builds one `Container` at boot; every action receives it via `(container:, call:)`. Workflow steps receive a `Workflow::Context` (key, entry, config, container, call).
|
|
177
|
-
|
|
178
|
-
## Ports
|
|
179
|
-
|
|
180
|
-
| Class | Role |
|
|
181
|
-
|---|---|
|
|
182
|
-
| `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`. |
|
|
183
|
-
| `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, rotation. |
|
|
184
|
-
| `Ports::Clock` | Supplies `Time.now` — module-function so tests can swap without DI boilerplate. |
|
|
185
|
-
| `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel. |
|
|
186
|
-
| `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. |
|
|
187
|
-
| `Ports::JobStore` | Persistent job queue used by `drain` workers; tracks ready/leased/done/failed jobs. |
|
|
188
|
-
| `Ports::SentinelStore` | Reads and writes per-target sentinel files for managed-file detection. |
|
|
189
|
-
|
|
190
|
-
Application use cases access ports only through `Container` fields — never through the raw `Store`.
|
|
191
|
-
|
|
192
|
-
### Envelope
|
|
193
|
-
|
|
194
|
-
`Envelope::Reader` and `Envelope::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
|
|
195
|
-
|
|
196
|
-
**Reader** (`lib/textus/envelope/reader.rb`) — resolves a key through `manifest.resolver`, reads bytes via `FileStore`, parses via the format strategy, returns an `Envelope`. No audit, no permissions.
|
|
197
|
-
|
|
198
|
-
**Writer** (`lib/textus/envelope/writer.rb`) — owns the full write pipeline: serialize → schema-validate → etag-check → `FileStore#write` → `AuditLog#append`. The audit append is the commit step.
|
|
199
|
-
|
|
200
|
-
Both are built from a `Container` via named constructors — `Writer.from(container:, call:)` and `Reader.from(container:)`.
|
|
201
|
-
|
|
202
|
-
## Manifest carving
|
|
203
|
-
|
|
204
|
-
`Manifest` is a `Data.define` struct with four named members:
|
|
205
|
-
|
|
206
|
-
| Member | Class | Responsibility |
|
|
207
|
-
|---|---|---|
|
|
208
|
-
| `data` | `Manifest::Data` | Frozen value: raw, root, lanes, entries, audit_config, role_caps. |
|
|
209
|
-
| `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`. Nested entry enumeration and fuzzy-match suggestions. |
|
|
210
|
-
| `policy` | `Manifest::Policy` | Zone/capability authority — `verb_for_lane`, `roles_with_capability`, `propose_lane_for(role)`. |
|
|
211
|
-
| `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)` → `RuleSet(guard, retention, react)`. |
|
|
212
|
-
|
|
213
|
-
## Read path (`store.get(key)`)
|
|
214
|
-
|
|
215
|
-
`Action::Get` is a **pure read** — it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict. It never ingests and never mutates.
|
|
216
|
-
|
|
217
|
-
1. CLI/MCP surface calls `store.with_role(role).get(key)`.
|
|
218
|
-
2. `Gate#dispatch` runs Auth → `Action::Get#call`.
|
|
219
|
-
3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
|
|
220
|
-
|
|
221
|
-
Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is returned stale — the read does not refresh it; `drain` does.
|
|
222
|
-
|
|
223
|
-
## Write path (`store.put(key, ...)`)
|
|
224
|
-
|
|
225
|
-
1. CLI/MCP surface calls `store.with_role(role).put(key, meta:, body:)`.
|
|
226
|
-
2. `Gate#dispatch` runs Auth → `Action::Put#call`.
|
|
227
|
-
3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
|
|
228
|
-
4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
|
|
229
|
-
|
|
230
|
-
`Action::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape.
|
|
231
|
-
|
|
232
|
-
## Workflow path (`drain` + materialize jobs)
|
|
233
|
-
|
|
234
|
-
The workflow system owns the produce pipeline. `Produce::Engine.converge(container:, call:, keys:)` is the entry point that `Jobs::Materialize` calls.
|
|
235
|
-
|
|
236
|
-
For each key, `Engine#produce_one`:
|
|
237
|
-
|
|
238
|
-
1. Looks up a registered workflow via `container.workflows.for(key)` (Pattern matching against the key).
|
|
239
|
-
2. If a workflow matches: `Workflow::Runner.new(definition, container:, call:).run(key)`.
|
|
240
|
-
- `Runner` builds a `Workflow::Context`, executes each step in sequence, then publishes.
|
|
241
|
-
- **Built-in publish**: writes the step output to the entry via `Envelope::Writer`, then calls `entry.publish_via(pctx)` for any `publish_to` or `publish_tree` targets.
|
|
242
|
-
3. If no workflow matches but the entry has publish targets: runs `publish_only` — publishes existing store bytes via `entry.publish_via` without re-fetching.
|
|
243
|
-
|
|
244
|
-
**Workflow files** live in `.textus/workflows/*.rb`. The DSL:
|
|
245
|
-
|
|
246
|
-
```ruby
|
|
247
|
-
Textus.workflow "my-producer" do
|
|
248
|
-
match "artifacts.derived.*"
|
|
249
|
-
step :fetch do |data, ctx|
|
|
250
|
-
{ content: { "key" => ctx.key } }
|
|
251
|
-
end
|
|
252
|
-
publish # uses built-in publish (writes to entry, then to publish targets)
|
|
253
|
-
end
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## Agent surface (boot + pulse + MCP)
|
|
257
|
-
|
|
258
|
-
```
|
|
259
|
-
agent/plugin ──▶ gate (CLI | MCP) ──▶ Store ──▶ memory (.textus/)
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Two transports, one façade:
|
|
263
|
-
|
|
264
|
-
- **CLI** — human/script surface. `textus boot`, `textus pulse --since=N`, `textus get/put/...`.
|
|
265
|
-
- **MCP** — agent surface. `textus mcp serve` runs a stdio JSON-RPC 2.0 server. Tools are auto-derived from contracts. Session state (cursor, role, contract_etag) is server-side.
|
|
266
|
-
|
|
267
|
-
The agent loop:
|
|
268
|
-
|
|
269
|
-
1. **Session start:** `boot()` → contract envelope (lanes, entries, workflows, write_flows, agent_quickstart).
|
|
270
|
-
2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor, next_due_at}`.
|
|
271
|
-
3. **On demand:** `get`, `put`, `propose`, `schema_show`, `rule_explain`, `drain`.
|
|
272
|
-
|
|
273
|
-
Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change to manifest/schemas); audit cursor expiry as `CursorExpired`. Both signal "call `boot` again."
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Conventions
|
|
2
|
-
|
|
3
|
-
> **Reference** · for integrators · **read when** you're shaping a `.textus/` tree and want the idiomatic choices
|
|
4
|
-
> **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-06 (v0.43)
|
|
5
|
-
|
|
6
|
-
Guidelines for shaping a `.textus/` tree, naming keys, organising schemas, and integrating with build automation. The spec ([`../../SPEC.md`](../../SPEC.md)) defines what's enforceable; this document captures what's *idiomatic*.
|
|
7
|
-
|
|
8
|
-
## Key naming
|
|
9
|
-
|
|
10
|
-
- **Segments are lowercase, kebab- or snake-case.** The grammar `^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$` is the hard limit. Prefer `acme-dashboard` over `acmedashboard` when there's a natural word break.
|
|
11
|
-
- **Lead with the lane in the key path.** `working.projects.acme.dashboard`, not `projects.acme.dashboard`. The lane prefix makes it obvious from the key alone whether a write will be accepted.
|
|
12
|
-
- **Mirror the directory structure.** If `working.projects.acme.dashboard` resolves to `working/projects/acme/dashboard.md`, do not invent shortcuts that diverge.
|
|
13
|
-
- **Don't pluralise the leaf.** `working.network.org.jane`, not `working.network.org.janes`. Pluralise the container, not the entry.
|
|
14
|
-
|
|
15
|
-
## Lane layout
|
|
16
|
-
|
|
17
|
-
Recommended top-level layout — the spec allows alternatives, but this is what tooling will default to:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
.textus/
|
|
21
|
-
manifest.yaml
|
|
22
|
-
schemas/ # YAML schema definitions
|
|
23
|
-
workflows/ # Textus.workflow DSL files for produced entry acquisition
|
|
24
|
-
templates/ # ERB templates for publish rendering
|
|
25
|
-
data/
|
|
26
|
-
knowledge/ # authored truth: identity, voice, decisions — author-holders write
|
|
27
|
-
scratchpad/ # agent's own durable lane (workspace) — keep-holders write
|
|
28
|
-
proposals/ # AI proposals awaiting accept (propose)
|
|
29
|
-
artifacts/ # computed outputs produced by drain — never edit by hand
|
|
30
|
-
raw/ # write-once external source material (ingest)
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Inside `knowledge/`, group by **domain** (identity, people, projects, decisions, runbooks), not by file type or date. `knowledge.identity.*` is the convention for slow-changing identity facts. Inside `artifacts/`, group by **producer** (`artifacts/catalogs/`, `artifacts/indexes/`) so it's clear which build job owns what.
|
|
34
|
-
|
|
35
|
-
## Schema design
|
|
36
|
-
|
|
37
|
-
- **One schema per entry type, not per directory.** `person.yaml`, `project.yaml`, `decision.yaml` — applied across multiple subtrees if the shape matches.
|
|
38
|
-
- **Required = "this entry is meaningless without it."** Everything else is `optional`. Resist the urge to mark organisational metadata (like `tags`) required.
|
|
39
|
-
- **Prefer `enum` over free-text** for low-cardinality fields (relationship type, status, severity). Agents are far better at picking from a list than at producing exact strings.
|
|
40
|
-
- **Cap string lengths** with `max:` where the field has a natural bound (names, summaries). Skip for prose body — bodies are not schema-validated, only frontmatter is.
|
|
41
|
-
|
|
42
|
-
## Owner strings
|
|
43
|
-
|
|
44
|
-
The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here*. The form is `<archetype>` or `<archetype>:<subject>`; the archetype must be one of `human`, `agent`, `automation` (validated at load — ADR 0045), and the subject is free-form:
|
|
45
|
-
|
|
46
|
-
- `human:network` — humans curate
|
|
47
|
-
- `agent:planner` — a specific named agent
|
|
48
|
-
- `automation:catalog-skills` — a specific build job
|
|
49
|
-
|
|
50
|
-
Tooling around `git blame` or audit logs may filter on owner; the gem itself only echoes it back in envelopes.
|
|
51
|
-
|
|
52
|
-
## Produced entries
|
|
53
|
-
|
|
54
|
-
A produced entry declares `source: { from: external, command: "true", sources: [] }` and a matching `Textus.workflow` block in `.textus/workflows/**/*.rb`. The `source:` acquires **data** — it never renders; rendering is a publish concern (below).
|
|
55
|
-
|
|
56
|
-
```yaml
|
|
57
|
-
- key: artifacts.feeds.skills
|
|
58
|
-
lane: artifacts
|
|
59
|
-
kind: produced
|
|
60
|
-
format: json
|
|
61
|
-
source: { from: external, command: "true", sources: [] }
|
|
62
|
-
publish:
|
|
63
|
-
- { to: docs/reference/skills.md, template: feeds/skills.erb }
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The matching workflow block:
|
|
67
|
-
|
|
68
|
-
```ruby
|
|
69
|
-
# .textus/workflows/feeds/agentskills.rb
|
|
70
|
-
Textus.workflow "agentskills" do
|
|
71
|
-
match "artifacts.feeds.skills"
|
|
72
|
-
|
|
73
|
-
step :fetch do |_, _ctx|
|
|
74
|
-
{ "content" => { "skills" => [...], "count" => 1 } }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
publish
|
|
78
|
-
end
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
`drain` discovers workflows, matches entries, runs the steps, and writes the result back to the entry's data path. Publishing then copies or renders each `publish:` target.
|
|
82
|
-
|
|
83
|
-
Age-based GC uses a `retention:` rule in the top-level `rules:` block:
|
|
84
|
-
|
|
85
|
-
```yaml
|
|
86
|
-
rules:
|
|
87
|
-
- match: artifacts.feeds.**
|
|
88
|
-
retention: { ttl: 90d, action: archive }
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Full contract in [`../../SPEC.md` §5.2](../../SPEC.md).
|
|
92
|
-
|
|
93
|
-
## Freshness
|
|
94
|
-
|
|
95
|
-
A `get` is always a pure read (ADR 0089) — it never triggers a re-produce:
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
textus pulse --output=json # `stale` lists entries past their ttl
|
|
99
|
-
textus drain --as=automation # re-produces stale entries
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
See [`./lanes.md`](lanes.md) for lane semantics and [`../how-to/configuring-lanes.md`](../how-to/configuring-lanes.md) for setting up produced entries and workflows.
|
|
103
|
-
|
|
104
|
-
### Read vs. refresh
|
|
105
|
-
|
|
106
|
-
There is one public read operation, and it is pure (ADR 0089):
|
|
107
|
-
|
|
108
|
-
| Operation | Behaviour | Use for |
|
|
109
|
-
|-----------|-----------|---------|
|
|
110
|
-
| `ops.get` | A pure on-disk read annotated with a freshness verdict — it NEVER ingests, regardless of the entry's `action`. A stale `refresh` entry reads back stale until the next `drain`. | every caller — interactive reads, dashboards, scripts, and internal pipelines (materializer, projection, schema tooling, accept/reject/publish, uid, validator) |
|
|
111
|
-
|
|
112
|
-
Refreshing a stale entry is `drain`'s job (or a `hook run` event), never a read's — so no caller can accidentally trigger network I/O by reading.
|
|
113
|
-
|
|
114
|
-
## Body content
|
|
115
|
-
|
|
116
|
-
- **Bodies are Markdown.** Headings, lists, code fences — whatever a human or agent finds useful.
|
|
117
|
-
- **The schema does not validate the body.** If a field belongs in structured data, put it in frontmatter, not the body.
|
|
118
|
-
- **Keep entries short.** If a project entry hits 500 lines, it probably wants to be split into sub-entries (e.g. `working.projects.acme.dashboard` + `working.projects.acme.api`) rather than one mega-document.
|
|
119
|
-
|
|
120
|
-
## Concurrency
|
|
121
|
-
|
|
122
|
-
For multi-writer environments, **always pass `if_etag`** on `put`. The gem treats etag-less writes as last-writer-wins on purpose (single-writer scripts, fresh-file creation), but anything resembling a daemon or a long-running agent should round-trip the etag.
|
|
123
|
-
|
|
124
|
-
## Application layering
|
|
125
|
-
|
|
126
|
-
The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](../architecture/decisions/0018-manifest-carving.md), [ADR 0017](../architecture/decisions/0017-envelope-io-split.md), [ADR 0022](../architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](../architecture/decisions/0023-uniform-use-case-shape.md).
|
|
127
|
-
|
|
128
|
-
- **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(lane)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
|
|
129
|
-
- **`Store` is the single dispatch module.** `Store` holds the `Container`, the active role, session state (cursor, propose_lane, contract_etag), and dispatches verbs through `method_missing`. Each verb call (`store.get(key: ...)`, `store.put(key:, meta:, body:)`) goes through `VerbRegistry`, binds inputs via `Dispatch::Binder`, routes through `Dispatch::Pipeline` (middleware chain: auth, cascade, audit), and reaches the handler. `Store#with_role(role)` returns a new `Store` bound to that role.
|
|
130
|
-
- **`Container` is a `Data.define` composition** of `Infrastructure` (`file_store`, `schemas`, `audit_log`, `job_store`, `geometry`) and `Coordination` (`manifest`, `workflows`, `pipeline`). Built in a single pass — no circular dependencies or lazy proxies.
|
|
131
|
-
- **Dispatch lives in `lib/textus/dispatch/`**: `Dispatch.dispatch` orchestrates verb dispatch, `Dispatch::Pipeline` runs middleware then invokes the handler, `Dispatch::Binder` handles input binding (wire format, CLI flags, session defaults). Auth predicates moved to `Manifest::Policy::Predicates`.
|
|
132
|
-
- **Handlers receive `container:`** and access I/O via `@container.pipeline.read/write/delete/move` and manifest config via `@container.manifest`.
|
|
133
|
-
- **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
|
|
134
|
-
|
|
135
|
-
The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
|
|
136
|
-
|
|
137
|
-
## Pairing with other tools
|
|
138
|
-
|
|
139
|
-
- **MCP servers**: a thin server that exposes `textus get` and `textus put` as tools is the recommended way to give Claude/agents access. Don't bake MCP into this gem.
|
|
140
|
-
- **Vector stores**: index `body` content into a vector store if you want fuzzy retrieval. `frontmatter` stays in textus as the source of truth for deterministic facts.
|
|
141
|
-
- **CI**: run `textus doctor` (the `generator_drift` check) or `textus pulse` (the `stale` list) in CI to catch drift between derived entries and their sources.
|
data/lib/textus/boot.rb
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Read-only "what's in this store and how do I use it" envelope.
|
|
3
|
-
# Boot is side-effect-free. Reads from pre-computed artifacts and
|
|
4
|
-
# the store catalog rather than computing inline.
|
|
5
|
-
module Boot
|
|
6
|
-
PROTOCOL_ID = PROTOCOL
|
|
7
|
-
|
|
8
|
-
# Human-readable name(s) for the live lane(s) of a given kind, or `fallback`
|
|
9
|
-
# when the manifest declares none. Lets write-flow guidance name the live
|
|
10
|
-
# lane by kind instead of a hardcoded instance name (ADR 0034).
|
|
11
|
-
def self.lane_label(manifest, kind, fallback)
|
|
12
|
-
lanes = manifest.policy.lanes_of_kind(kind)
|
|
13
|
-
lanes.empty? ? fallback : lanes.join(", ")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Static, store-independent parts of the agent-facing protocol. The
|
|
17
|
-
# `recipes` and `role_resolution` blocks are derived per-manifest in
|
|
18
|
-
# agent_protocol(...) because lane and role names are user-configurable.
|
|
19
|
-
AGENT_PROTOCOL_TEMPLATE = {
|
|
20
|
-
"envelope_shape" => {
|
|
21
|
-
"summary" => "every read/write payload is a JSON envelope with _meta, body, uid, and etag",
|
|
22
|
-
"fields" => {
|
|
23
|
-
"_meta" => "hash of structured frontmatter; schema-validated per entry family",
|
|
24
|
-
"body" => "string payload (markdown/text) or nil for json/yaml formats where body lives in _meta",
|
|
25
|
-
"uid" => "stable 16-char hex identifier; preserved across writes and key renames",
|
|
26
|
-
"etag" => "content hash; pass back on writes to detect concurrent edits",
|
|
27
|
-
},
|
|
28
|
-
"ref" => "SPEC.md §8",
|
|
29
|
-
},
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
# Curated agent-facing verb catalog. This declares which verbs the operator
|
|
33
|
-
# CLI surfaces and in what order — the editorial presentation. The summary of
|
|
34
|
-
# each verb is a fact, not presentation: it is derived from `contract.summary`
|
|
35
|
-
# at load time (ADR 0039). A literal "summary" survives here only for grouped
|
|
36
|
-
# CLI tokens (schema/key/rule/hook) that aggregate several sub-contracts and so
|
|
37
|
-
# have no single contract to derive from. CLI_VERBS itself is assigned in
|
|
38
|
-
# textus.rb after Zeitwerk eager_load so all contract files are present.
|
|
39
|
-
CURATED_CLI_VERBS = [
|
|
40
|
-
{ "name" => "boot" },
|
|
41
|
-
{ "name" => "list" },
|
|
42
|
-
{ "name" => "get" },
|
|
43
|
-
{ "name" => "ingest" },
|
|
44
|
-
{ "name" => "where" },
|
|
45
|
-
{ "name" => "schema", "summary" => "schema operations: 'schema show KEY', 'schema diff', 'schema init', 'schema migrate'" },
|
|
46
|
-
{ "name" => "put" },
|
|
47
|
-
{ "name" => "propose" },
|
|
48
|
-
{ "name" => "accept" },
|
|
49
|
-
{ "name" => "enqueue" },
|
|
50
|
-
{ "name" => "key", "summary" => "key operations: 'key delete', 'key mv', 'key uid'" },
|
|
51
|
-
{ "name" => "drain" },
|
|
52
|
-
{ "name" => "audit" },
|
|
53
|
-
{ "name" => "blame" },
|
|
54
|
-
{ "name" => "rule", "summary" => "inspect effective rules: 'rule list', 'rule explain KEY'" },
|
|
55
|
-
{ "name" => "doctor" },
|
|
56
|
-
{ "name" => "jobs" },
|
|
57
|
-
{ "name" => "pulse" },
|
|
58
|
-
].freeze
|
|
59
|
-
|
|
60
|
-
def self.contract_summaries
|
|
61
|
-
Textus::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
65
|
-
# one exists, falling back to the curated editorial string for grouped tokens
|
|
66
|
-
# (schema/key/rule/hook). Called once from textus.rb after eager_load.
|
|
67
|
-
def self.build_cli_verbs
|
|
68
|
-
summaries = contract_summaries
|
|
69
|
-
CURATED_CLI_VERBS.map do |entry|
|
|
70
|
-
derived = summaries[entry["name"]]
|
|
71
|
-
derived ? entry.merge("summary" => derived) : entry
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.build(container:)
|
|
76
|
-
etag = Textus::Value::Etag.for_contract(container.root)
|
|
77
|
-
latest_seq = container.audit_log.latest_seq
|
|
78
|
-
artifact = read_artifact_content(container, "artifacts.boot")
|
|
79
|
-
context = read_boot_context(container)
|
|
80
|
-
|
|
81
|
-
# Prefer pre-computed artifact (drain computes, boot reads).
|
|
82
|
-
# Fall back to inline manifest projection for stores that have not yet
|
|
83
|
-
# run drain (test fixtures, fresh inits).
|
|
84
|
-
stable = artifact || inline_boot_content(container.manifest, latest_seq)
|
|
85
|
-
|
|
86
|
-
if stable["agent_quickstart"]
|
|
87
|
-
stable = stable.merge(
|
|
88
|
-
"agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
|
|
89
|
-
)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
payload = {
|
|
93
|
-
"protocol" => PROTOCOL_ID,
|
|
94
|
-
"store_root" => container.root,
|
|
95
|
-
"contract_etag" => etag,
|
|
96
|
-
}.merge(stable)
|
|
97
|
-
payload["context"] = context if context
|
|
98
|
-
payload
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.inline_boot_content(manifest, _latest_seq)
|
|
102
|
-
agent_role = manifest.policy.proposer_role
|
|
103
|
-
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
|
|
104
|
-
next unless agent_role
|
|
105
|
-
|
|
106
|
-
verb = manifest.policy.verb_for_lane(ln)
|
|
107
|
-
writers = manifest.policy.roles_with_capability(verb)
|
|
108
|
-
acc << ln if writers.include?(agent_role)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
"lanes" => lanes_for(manifest),
|
|
113
|
-
"agent_quickstart" => {
|
|
114
|
-
"read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
|
|
115
|
-
"write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
|
|
116
|
-
"writable_lanes" => writable_lanes,
|
|
117
|
-
"propose_lane" => manifest.policy.propose_lane_for(agent_role),
|
|
118
|
-
},
|
|
119
|
-
"agent_protocol" => agent_protocol(manifest),
|
|
120
|
-
}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def self.agent_protocol(manifest)
|
|
124
|
-
queue = manifest.policy.queue_lane
|
|
125
|
-
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
126
|
-
AGENT_PROTOCOL_TEMPLATE.merge(
|
|
127
|
-
"recipes" => {
|
|
128
|
-
"read" => { "purpose" => "find and read an entry",
|
|
129
|
-
"steps" => ["list (lane:, prefix:) — discover keys", "get KEY — returns the entry envelope"] },
|
|
130
|
-
"write" => { "purpose" => "create or update an entry",
|
|
131
|
-
"steps" => ["schema KEY — learn field shape", "put KEY — persist it (role-gated)"] },
|
|
132
|
-
"propose" => { "purpose" => "agent suggests a change for human review",
|
|
133
|
-
"agent_steps" => ["propose KEY — writes to #{queue} lane"],
|
|
134
|
-
"human_steps" => ["accept #{queue}.KEY — promotes to target lane"] },
|
|
135
|
-
"drain" => { "purpose" => "keep machine lanes fresh",
|
|
136
|
-
"steps" => ["pulse — stale list names overdue entries",
|
|
137
|
-
"drain (lane: #{feeds}) — re-pull stale entries"] },
|
|
138
|
-
},
|
|
139
|
-
"role_resolution" => {
|
|
140
|
-
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
141
|
-
"then a transport default ('human' for CLI, 'agent' for MCP)",
|
|
142
|
-
"roles" => manifest.data.role_caps.keys,
|
|
143
|
-
"ref" => "SPEC.md §5",
|
|
144
|
-
},
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def self.lanes_for(manifest)
|
|
149
|
-
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
150
|
-
verb = manifest.policy.verb_for_lane(name)
|
|
151
|
-
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
152
|
-
kind = manifest.policy.declared_kind(name)
|
|
153
|
-
row["kind"] = kind.to_s if kind
|
|
154
|
-
purpose = manifest.data.lane_descs[name]
|
|
155
|
-
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
156
|
-
row
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def self.read_artifact_content(container, key)
|
|
161
|
-
res = container.manifest.resolver.resolve(key)
|
|
162
|
-
return nil unless res.path && File.exist?(res.path)
|
|
163
|
-
|
|
164
|
-
env = Textus::Store::Entry::Reader.from(container: container).read(key)
|
|
165
|
-
env&.content
|
|
166
|
-
rescue Textus::Error
|
|
167
|
-
nil
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def self.read_boot_context(container)
|
|
171
|
-
res = container.manifest.resolver.resolve("knowledge.boot")
|
|
172
|
-
return nil unless res.path && File.exist?(res.path)
|
|
173
|
-
|
|
174
|
-
env = Textus::Store::Entry::Reader.from(container: container).read("knowledge.boot")
|
|
175
|
-
body = env&.body&.strip
|
|
176
|
-
body.nil? || body.empty? ? nil : body
|
|
177
|
-
rescue Textus::Error
|
|
178
|
-
nil
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Dispatch
|
|
3
|
-
module Contracts
|
|
4
|
-
GetEntry = Data.define(:key)
|
|
5
|
-
|
|
6
|
-
PutEntry = Data.define(:key, :meta, :body, :content, :if_etag)
|
|
7
|
-
|
|
8
|
-
ListKeys = Data.define(:prefix, :lane, :q, :schema)
|
|
9
|
-
|
|
10
|
-
DeleteKey = Data.define(:key, :if_etag)
|
|
11
|
-
|
|
12
|
-
MoveKey = Data.define(:old_key, :new_key, :if_etag, :dry_run)
|
|
13
|
-
|
|
14
|
-
ProposeEntry = Data.define(:key, :meta, :body, :content)
|
|
15
|
-
|
|
16
|
-
AcceptProposal = Data.define(:pending_key)
|
|
17
|
-
|
|
18
|
-
RejectProposal = Data.define(:pending_key)
|
|
19
|
-
|
|
20
|
-
EnqueueJob = Data.define(:type, :args)
|
|
21
|
-
|
|
22
|
-
AuditEntries = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit)
|
|
23
|
-
|
|
24
|
-
PulseEntries = Data.define(:since)
|
|
25
|
-
|
|
26
|
-
BlameEntry = Data.define(:key, :limit)
|
|
27
|
-
|
|
28
|
-
WhereEntry = Data.define(:key)
|
|
29
|
-
|
|
30
|
-
UidEntry = Data.define(:key)
|
|
31
|
-
|
|
32
|
-
DepsEntry = Data.define(:key)
|
|
33
|
-
|
|
34
|
-
RdepsEntry = Data.define(:key)
|
|
35
|
-
|
|
36
|
-
BootStore = Data.define
|
|
37
|
-
|
|
38
|
-
DoctorStore = Data.define(:checks)
|
|
39
|
-
|
|
40
|
-
PublishedEntries = Data.define
|
|
41
|
-
|
|
42
|
-
RuleExplain = Data.define(:key, :detail)
|
|
43
|
-
|
|
44
|
-
RuleList = Data.define
|
|
45
|
-
|
|
46
|
-
SchemaEnvelope = Data.define(:key)
|
|
47
|
-
|
|
48
|
-
DrainStore = Data.define(:prefix, :lane)
|
|
49
|
-
|
|
50
|
-
IngestEntry = Data.define(:kind, :slug, :url, :path, :lane, :label)
|
|
51
|
-
|
|
52
|
-
JobsAction = Data.define(:state, :action, :job_id)
|
|
53
|
-
|
|
54
|
-
RuleLint = Data.define(:candidate_yaml)
|
|
55
|
-
|
|
56
|
-
DataMv = Data.define(:from, :to, :dry_run)
|
|
57
|
-
|
|
58
|
-
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run)
|
|
59
|
-
|
|
60
|
-
KeyDeletePrefix = Data.define(:prefix, :dry_run)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Dispatch
|
|
3
|
-
class HandlerRegistry
|
|
4
|
-
def initialize
|
|
5
|
-
@handlers = {}
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def register(command_class, handler)
|
|
9
|
-
@handlers[command_class] = handler
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def for(command_class)
|
|
13
|
-
@handlers[command_class] || raise("no handler registered for #{command_class}")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def registered?(command_class)
|
|
17
|
-
@handlers.key?(command_class)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|