textus 0.53.0 → 0.54.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 +34 -0
- data/SPEC.md +2 -5
- data/docs/architecture/README.md +149 -159
- data/lib/textus/action/accept.rb +0 -9
- data/lib/textus/action/audit.rb +0 -2
- data/lib/textus/action/blame.rb +0 -2
- data/lib/textus/action/boot.rb +2 -11
- data/lib/textus/action/data_mv.rb +1 -3
- data/lib/textus/action/deps.rb +1 -15
- data/lib/textus/action/doctor.rb +0 -2
- data/lib/textus/action/drain.rb +4 -6
- data/lib/textus/action/enqueue.rb +3 -5
- data/lib/textus/action/get.rb +1 -3
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +1 -3
- data/lib/textus/action/key_delete.rb +0 -10
- data/lib/textus/action/key_delete_prefix.rb +1 -3
- data/lib/textus/action/key_mv.rb +1 -15
- data/lib/textus/action/key_mv_prefix.rb +1 -3
- data/lib/textus/action/list.rb +0 -2
- data/lib/textus/action/propose.rb +0 -2
- data/lib/textus/action/published.rb +0 -2
- data/lib/textus/action/pulse.rb +7 -38
- data/lib/textus/action/put.rb +1 -12
- data/lib/textus/action/rdeps.rb +2 -12
- data/lib/textus/action/reject.rb +0 -12
- data/lib/textus/action/rule_explain.rb +0 -4
- data/lib/textus/action/rule_lint.rb +1 -3
- data/lib/textus/action/rule_list.rb +0 -4
- data/lib/textus/action/schema_envelope.rb +0 -2
- data/lib/textus/action/uid.rb +0 -2
- data/lib/textus/action/where.rb +0 -2
- data/lib/textus/action/write_verb.rb +4 -18
- data/lib/textus/boot.rb +33 -109
- data/lib/textus/command.rb +6 -1
- data/lib/textus/container.rb +1 -1
- data/lib/textus/core/freshness/evaluator.rb +18 -34
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check.rb +0 -1
- data/lib/textus/doctor.rb +2 -42
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/errors.rb +8 -4
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +58 -12
- data/lib/textus/gate.rb +2 -0
- data/lib/textus/init.rb +12 -75
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/{background → jobs}/plan.rb +1 -1
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/{background → jobs}/worker.rb +2 -2
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +4 -19
- data/lib/textus/manifest/entry/parser.rb +27 -10
- data/lib/textus/manifest/entry/produced.rb +13 -34
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -4
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/manifest/policy/source.rb +14 -62
- data/lib/textus/manifest/policy.rb +2 -5
- data/lib/textus/manifest/resolver.rb +2 -2
- data/lib/textus/manifest/schema/keys.rb +6 -20
- data/lib/textus/manifest/schema/validator.rb +3 -11
- data/lib/textus/manifest/schema/vocabulary.rb +1 -0
- data/lib/textus/ports/{queue → job_store}/job.rb +1 -1
- data/lib/textus/ports/{queue.rb → job_store.rb} +6 -13
- data/lib/textus/produce/engine.rb +50 -0
- data/lib/textus/produce/render.rb +18 -0
- data/lib/textus/session.rb +1 -1
- data/lib/textus/store.rb +1 -10
- data/lib/textus/surfaces/mcp/catalog.rb +1 -1
- data/lib/textus/surfaces/mcp/errors.rb +0 -9
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +101 -62
- data/lib/textus/surfaces/role_scope.rb +0 -4
- data/lib/textus/surfaces/watcher.rb +3 -3
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +12 -1
- metadata +33 -61
- data/lib/textus/action/pulse/scanner.rb +0 -118
- data/lib/textus/background/job/base.rb +0 -27
- data/lib/textus/background/job/materialize.rb +0 -31
- data/lib/textus/background/job/refresh.rb +0 -22
- data/lib/textus/background/job/sweep.rb +0 -31
- data/lib/textus/background/job.rb +0 -19
- data/lib/textus/background/planner/plan.rb +0 -113
- data/lib/textus/background/retention/apply.rb +0 -50
- data/lib/textus/doctor/check/handler_permit.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -25
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -211
- data/lib/textus/events.rb +0 -21
- data/lib/textus/init/templates/machine_intake.rb +0 -49
- data/lib/textus/init/templates/orientation_reducer.rb +0 -21
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/manifest/policy/handler_permit.rb +0 -17
- data/lib/textus/pipeline/acquire/handler.rb +0 -29
- data/lib/textus/pipeline/acquire/intake.rb +0 -132
- data/lib/textus/pipeline/acquire/projection.rb +0 -129
- data/lib/textus/pipeline/acquire/serializer/json.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer/text.rb +0 -16
- data/lib/textus/pipeline/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer.rb +0 -17
- data/lib/textus/pipeline/engine.rb +0 -97
- data/lib/textus/pipeline/render.rb +0 -25
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/step/base.rb +0 -35
- data/lib/textus/step/builtin/csv_fetch.rb +0 -19
- data/lib/textus/step/builtin/ical_events_fetch.rb +0 -30
- data/lib/textus/step/builtin/json_fetch.rb +0 -18
- data/lib/textus/step/builtin/markdown_links_fetch.rb +0 -20
- data/lib/textus/step/builtin/rss_fetch.rb +0 -26
- data/lib/textus/step/builtin.rb +0 -22
- data/lib/textus/step/catalog.rb +0 -37
- data/lib/textus/step/context.rb +0 -74
- data/lib/textus/step/discovery.rb +0 -24
- data/lib/textus/step/error_log.rb +0 -32
- data/lib/textus/step/event_bus.rb +0 -113
- data/lib/textus/step/fetch.rb +0 -13
- data/lib/textus/step/fire_report.rb +0 -23
- data/lib/textus/step/loader.rb +0 -108
- data/lib/textus/step/observe.rb +0 -31
- data/lib/textus/step/registry_store.rb +0 -66
- data/lib/textus/step/signature.rb +0 -31
- data/lib/textus/step/transform.rb +0 -12
- data/lib/textus/step/validate.rb +0 -11
- data/lib/textus/step.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f98dbc5f461b8cffaa4cf8fb17c8b79cf51f4c5f78af62527e0abe27d84d828
|
|
4
|
+
data.tar.gz: 0bfd928765a9797078f7534aecbf89321de754a4d56ee71986c00fb6a3a7b255
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32fd23741a0f3e69368dfde1fb8733381190b17eb592c459079020a692c98c181a689ab5a83af6b3bf2d16c37ab2b52e40faf6a47ce876e4bafc0709e082ebed
|
|
7
|
+
data.tar.gz: daadc9db41cf87e2a348ddf9b4472fc515a48b4f5e7ca77d9c00a6c95ed736d26c59b3ad1ec799a42e3eba940052236dc77e5ee3edaf4737150e8f030a5fbcaa
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,40 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
9
9
|
bump is a breaking change that requires a store migration; the gem version
|
|
10
10
|
tracks both additive improvements and breaking protocol bumps independently.
|
|
11
11
|
|
|
12
|
+
## 0.54.0 — 2026-06-16 — Workflow system, raw ingest pipeline, MCP resources
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Workflow system** (PR #218): Ruby DSL files in `.textus/workflows/` replace the `step/` extension model. `Workflow::Registry` (loaded from `.textus/workflows/**/*.rb`) is resolved by `Produce::Engine` at produce time; `Workflow::Runner` threads data through each named step and persists. Core components: `Workflow::Context`, `Workflow::Pattern` (glob matching), `Workflow::DSL`, `Workflow::Collector`, `Workflow::Registry`, `Workflow::Loader`, `Workflow::Runner`.
|
|
17
|
+
- **`artifacts.index` produced workflow**: pre-computed full-store catalog (`artifacts.index`) served as a produced artifact — enumerates every manifest entry with its key, lane, format, etag, and `generated_at` timestamp.
|
|
18
|
+
- **MCP `resources/list` and `resources/read`** (PR #221): machine-lane `produced` entries are now exposed as MCP resources at `textus://<key>` URIs with correct MIME types. Clients discover them via the `resources` capability advertised in the `initialize` handshake.
|
|
19
|
+
- **Raw ingest pipeline** (PR #219, [ADR 0114](docs/architecture/decisions/0114-foundation-refactor.md), [ADR 0116](docs/architecture/decisions/0116-raw-lane-and-ingest-verb.md)): new `raw` zone-kind with a `write-once` gate and an `ingest` capability/verb (`Action::Ingest`). Doctor checks and an asset sentinel guard the lane. The `raw` bijection entry maps `raw → ingest`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`artifacts.derived.*` → `artifacts.*`**: the `derived` key segment is dropped — all machine-lane entries flatten to `artifacts.<name>` in both the manifest and the on-disk store. Workflows, boot, and all other callers updated.
|
|
24
|
+
- **`boot` reads from `artifacts.index`**: inline computation removed from the boot action; the pre-computed catalog drives the response. `--lean` flag removed (PR #220).
|
|
25
|
+
- **`pulse` slimmed**: `stale`, `doctor`, and `next_due_at` removed from the pulse envelope; `index_etag` added so clients can cheaply detect catalog drift without a full list.
|
|
26
|
+
- **`ContractDrift` moved to core** (PR #220): `Textus::ContractDrift` extracted from the MCP surface into the core library so it is reusable across surfaces.
|
|
27
|
+
- **MCP `Server` refactored per Sandi Metz rules** (PR #220): routing extracted to `Routing` module; methods decomposed to ≤ 5 lines each.
|
|
28
|
+
- **Plugin manifest wiring** (PR #220): `claude_plugin` uses the installed `textus` binary (external use); `mcp_config` keeps `bundle exec` (dev environment). `--lean` references removed from the plugin manifest spec.
|
|
29
|
+
- **Namespace renames** (PR #218, internal): `entry/` → `format/`, `envelope/io/` → `envelope/`, `Ports::Queue` → `Ports::JobStore`, `background/` → `jobs/`, `pipeline/` → `produce/`.
|
|
30
|
+
- **Workflows reorganised**: workflow files split into `artifacts/` and `config/` subdirectories under `.textus/workflows/`.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
- **Step system** (PR #218): entire `lib/textus/step/` directory deleted along with all step-related specs, `container.steps`, dead `hooks.rb` doctor check, and `ports/audit_subscriber.rb`. **Migration:** rewrite step files as `.textus/workflows/*.rb` using the Workflow DSL.
|
|
35
|
+
- **`Jobs::Refresh`** ([ADR 0115](docs/architecture/decisions/0115-role-trust-model.md)): workflow-based periodic re-derivation replaces the background refresh job.
|
|
36
|
+
- **`handler_permit` manifest field** (PR #218): doctor-only field with no runtime enforcement deleted along with its policy class and schema key.
|
|
37
|
+
- **Dead guards and constants**: `derived_write?` no-op guard from `WriteVerb#cascade_to_rdeps`; `publish_each` runtime guard from `Entry::Publish`; duplicate inline `check_action!` from `put`/`key_delete`/`reject`; `BURN = :sync` constant; cross-lane notebook side-effect from `Action::Ingest`.
|
|
38
|
+
- **`source.from: fetch` and `source.from: derive`** narrowed to `external` only — fetch/derive methods removed from `Produced`; freshness/pulse use retention rules; boot drops intake/derived keys.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **MCP `resources` capability missing from `initialize` handshake** (PR #221): the server only advertised `tools`, so MCP clients never called `resources/list`. Adding `"resources" => {}` makes all 9 produced artifacts discoverable as resources.
|
|
43
|
+
- **`adr_log` workflow**: `include_keyless: true` added for tree-publish of `knowledge.decisions`.
|
|
44
|
+
- **SpecLayout violations** (PR #221): `workflow_index_spec` and `workflow/errors_spec` (string-described) moved to `spec/conformance/workflow/`.
|
|
45
|
+
|
|
12
46
|
## 0.53.0 — 2026-06-15 — Auth consolidation and code quality
|
|
13
47
|
|
|
14
48
|
### Added
|
data/SPEC.md
CHANGED
|
@@ -899,16 +899,13 @@ The agent's MCP write surface includes the single-key `key_delete` and `key_mv`
|
|
|
899
899
|
{
|
|
900
900
|
"cursor": 1845,
|
|
901
901
|
"changed": [ { "seq": 1843, "key": "knowledge.notes.x", "verb": "put", "role": "human", "ts": "..." } ],
|
|
902
|
-
"stale": [ "artifacts.marketplace" ],
|
|
903
902
|
"pending_review": [ "proposals.proposal.123" ],
|
|
904
|
-
"doctor": { "ok": true, "warn": 0, "fail": 0 },
|
|
905
903
|
"contract_etag": "sha256:1f3a…",
|
|
906
|
-
"
|
|
907
|
-
"hook_errors": [ { "seq": 1844, "event": "after_put", "hook": "notify", "key": "knowledge.notes.x", "error_class": "Timeout::Error", "error_message": "…", "at": "..." } ]
|
|
904
|
+
"index_etag": "sha256:8f3c…"
|
|
908
905
|
}
|
|
909
906
|
```
|
|
910
907
|
|
|
911
|
-
`cursor` is the new high-water mark; pass it as `--since` on the next call. `changed` is sourced from `audit --seq-since`. `
|
|
908
|
+
`cursor` is the new high-water mark; pass it as `--since` on the next call. `changed` is sourced from `audit --seq-since`. `pending_review` lists all keys in the queue zone. `contract_etag` is the `sha256:`-prefixed composite content hash of the contract — the manifest plus hooks and schemas (ADR 0074, via ADR 0025) — for cheap change-detection. `index_etag` is the etag of the `artifacts.index` catalog file, or `null` when it does not exist — agents use this to detect when the catalog has been rebuilt. When `--since` is below the oldest available seq (due to audit log rotation), pulse returns `CursorExpired`.
|
|
912
909
|
|
|
913
910
|
**`put` input** (read from stdin when `--stdin` is given):
|
|
914
911
|
|
data/docs/architecture/README.md
CHANGED
|
@@ -1,139 +1,154 @@
|
|
|
1
1
|
# Textus architecture
|
|
2
2
|
|
|
3
3
|
> **Explanation** · for contributors · **read this first** for orientation before SPEC
|
|
4
|
-
> **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch
|
|
4
|
+
> **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch paths) · **reviewed** 2026-06 (v0.54)
|
|
5
5
|
|
|
6
6
|
```mermaid
|
|
7
7
|
flowchart TD
|
|
8
|
-
surfaces["Surfaces — CLI
|
|
8
|
+
surfaces["Surfaces — CLI · MCP · RoleScope"]
|
|
9
9
|
contract["Contract — per-verb DSL (source of truth for public interfaces)"]
|
|
10
|
-
|
|
10
|
+
gate["Gate — routing (VERB_COMMAND) · Auth"]
|
|
11
|
+
action["Actions — per-verb use cases (action/)"]
|
|
12
|
+
jobs["Jobs — Worker · Planner · Materialize · Retention · Sweep"]
|
|
13
|
+
produce["Produce — Engine · Render"]
|
|
14
|
+
workflow["Workflow — Runner · Registry · Loader · DSL"]
|
|
11
15
|
manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
|
|
12
|
-
core["Core — pure value types (Freshness,
|
|
13
|
-
ports["Ports — IO adapters (FileStore, AuditLog,
|
|
14
|
-
step["Step — user-injectable wrappers (Fetch, Transform, Validate, Observe)"]
|
|
16
|
+
core["Core — pure value types (Freshness, Retention, Duration, Sentinel)"]
|
|
17
|
+
ports["Ports — IO adapters (FileStore, AuditLog, JobStore, Publisher…)"]
|
|
15
18
|
surfaces --> contract
|
|
16
|
-
contract -->
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
contract --> gate
|
|
20
|
+
gate --> action
|
|
21
|
+
action --> jobs
|
|
22
|
+
action --> produce
|
|
23
|
+
produce --> workflow
|
|
24
|
+
action --> manifest
|
|
25
|
+
action --> core
|
|
26
|
+
action --> ports
|
|
27
|
+
jobs --> produce
|
|
21
28
|
```
|
|
22
29
|
|
|
23
|
-
*Dependency rule: inward only.*
|
|
30
|
+
*Dependency rule: inward only.* Surfaces → Contract → Gate → Actions → inner layers. Actions never reference surfaces.
|
|
24
31
|
|
|
25
32
|
### What lives in each layer
|
|
26
33
|
|
|
27
|
-
**
|
|
34
|
+
**Surfaces**
|
|
28
35
|
|
|
29
36
|
```
|
|
30
|
-
CLI
|
|
31
|
-
|
|
37
|
+
surfaces/cli/ CLI command generation from contracts
|
|
38
|
+
surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
|
|
39
|
+
surfaces/role_scope.rb
|
|
40
|
+
(Store#as(role)) — holds (container, role, dry_run, correlation_id);
|
|
41
|
+
all verb methods injected via define_method in textus.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Contract**
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
```
|
|
47
|
+
contract/ Per-verb DSL — verb, summary, surfaces, arg, view.
|
|
48
|
+
Contract::Binder.inputs_from_ordered splits the uniform inputs hash
|
|
49
|
+
into positional/keyword args for every surface.
|
|
34
50
|
```
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
mirror of `MCP::Catalog`. The contract now owns the whole request lifecycle —
|
|
38
|
-
`acquire → bind → invoke → render` (ADRs 0066–0068): one `Contract::Binder.bind`
|
|
39
|
-
splits the uniform by-name `inputs` hash into the use-case's positional/keyword
|
|
40
|
-
args for every surface; per-surface `view`s shape the output (`view` for
|
|
41
|
-
MCP/Ruby, `view(:cli)` for the operator envelope); declarative `source:`/
|
|
42
|
-
`coerce:`/`cli_stdin` populate inputs from files and stdin; `around:` resources
|
|
43
|
-
wrap the single dispatch site (`RoleScope#dispatch_bound`) for stateful verbs;
|
|
44
|
-
and `cli_default:` declares a CLI default that diverges from the agent default.
|
|
45
|
-
`CLI::Runner` generates a command per `:cli` contract, dispatching `contract.verb`
|
|
46
|
-
by construction. Only verbs with genuine *behavior* — `put` (entry persistence),
|
|
47
|
-
`get` (UnknownKey + resolver suggestions, CLI-only), and `doctor` (not yet
|
|
48
|
-
generatable) — stay hand-authored, plus commands with no dispatcher verb (`init`,
|
|
49
|
-
`hook`, `mcp serve`, `schema diff/init`). `boot` is auto-generated from its
|
|
50
|
-
contract. Total reconciliation specs make name/dispatch/facet drift unrepresentable.
|
|
52
|
+
**Gate + Auth**
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
```
|
|
55
|
+
gate.rb VERB_COMMAND (verb symbol → Command class)
|
|
56
|
+
Gate#dispatch(cmd) — Auth → action call
|
|
57
|
+
gate/auth.rb Authorization engine — FLOOR predicates + rule guards
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Actions**
|
|
53
61
|
|
|
54
62
|
```
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
action/{get,list,put,key_delete,key_mv,accept,reject,propose,
|
|
64
|
+
drain,enqueue,audit,blame,deps,rdeps,published,boot,doctor,
|
|
65
|
+
rule_explain,rule_list,rule_lint,pulse,
|
|
66
|
+
data_mv,key_mv_prefix,key_delete_prefix,
|
|
67
|
+
schema_envelope,where,uid,jobs}.rb
|
|
68
|
+
```
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
**Jobs**
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
jobs/planner.rb Rules-driven job planning — seeds materialize/refresh/sweep jobs
|
|
74
|
+
jobs/worker.rb Queue drain — leases and runs jobs from JobStore
|
|
75
|
+
jobs/materialize.rb Per-key produce job → Produce::Engine.converge
|
|
76
|
+
jobs/refresh.rb Per-key refresh job (retention-rule-triggered)
|
|
77
|
+
jobs/retention.rb Retention policy check
|
|
78
|
+
jobs/sweep.rb Lane sweep job
|
|
60
79
|
```
|
|
61
80
|
|
|
62
|
-
**
|
|
81
|
+
**Produce**
|
|
63
82
|
|
|
64
83
|
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
catalog/events.rb (dotted event name constants)
|
|
84
|
+
produce/engine.rb Engine.converge(container:, call:, keys:)
|
|
85
|
+
For each key: looks up a registered workflow and runs it,
|
|
86
|
+
or publishes existing bytes for entries with publish targets.
|
|
87
|
+
produce/render.rb Mustache template expansion for publish targets
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Workflow**
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
workflow/runner.rb Runs a Definition: build_context → execute_steps → publish
|
|
94
|
+
workflow/registry.rb In-memory list of registered Definitions; Registry#for(key)
|
|
95
|
+
workflow/loader.rb Loads .textus/workflows/*.rb files via Collector DSL
|
|
96
|
+
workflow/dsl.rb Textus.workflow "name" { match "...", step :fetch { ... }, publish }
|
|
97
|
+
workflow/context.rb Context passed to each step (key, entry, config, container, call)
|
|
98
|
+
workflow/pattern.rb Key-matching pattern (glob via File.fnmatch?)
|
|
99
|
+
workflow/errors.rb NotFound, StepFailed
|
|
82
100
|
```
|
|
83
101
|
|
|
84
102
|
**Core (pure value types)**
|
|
85
103
|
|
|
86
104
|
```
|
|
87
105
|
Freshness::{Verdict,Evaluator}
|
|
88
|
-
|
|
89
|
-
Duration Sentinel
|
|
106
|
+
Core::Duration Core::Sentinel
|
|
90
107
|
```
|
|
91
108
|
|
|
92
109
|
**Infrastructure**
|
|
93
110
|
|
|
94
111
|
```
|
|
95
|
-
Store (composition root — wires ports,
|
|
96
|
-
vends a Container + dispatches verbs)
|
|
112
|
+
Store (composition root — wires ports, vends Container)
|
|
97
113
|
Storage::FileStore (bytes-only port: read/write/delete/exists?/etag)
|
|
98
114
|
Manifest (Data, Resolver, Policy, Rules)
|
|
99
115
|
Schemas (eager-load cache)
|
|
100
|
-
Ports::{AuditLog,
|
|
101
|
-
BuildLock,
|
|
102
|
-
Step::{EventBus,RegistryStore,Loader,Context,FireReport,
|
|
103
|
-
Signature,Builtin,ErrorLog,Fetch,Transform,Validate,Observe}
|
|
116
|
+
Ports::{AuditLog,JobStore,Publisher,Clock,
|
|
117
|
+
BuildLock,SentinelStore}
|
|
104
118
|
Entry::{Markdown,Json,Yaml,Text} (format strategies)
|
|
105
|
-
Doctor::Validator (schema + role-authority validation — called by doctor check)
|
|
106
119
|
```
|
|
107
120
|
|
|
108
121
|
## How a verb becomes a method
|
|
109
122
|
|
|
110
|
-
All actions live under `lib/textus/
|
|
123
|
+
All actions live under `lib/textus/action/`. The shape is uniform:
|
|
111
124
|
|
|
112
125
|
```ruby
|
|
113
126
|
module Textus
|
|
114
|
-
module
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
module Action
|
|
128
|
+
class Get < Base
|
|
129
|
+
extend Textus::Contract::DSL
|
|
130
|
+
|
|
131
|
+
verb :get
|
|
132
|
+
summary "Read an entry by key."
|
|
133
|
+
surfaces :cli, :mcp
|
|
134
|
+
arg :key, String, required: true, positional: true
|
|
135
|
+
|
|
136
|
+
BURN = :sync
|
|
137
|
+
|
|
138
|
+
def initialize(key:)
|
|
139
|
+
super()
|
|
140
|
+
@key = key
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def call(container:, call:)
|
|
144
|
+
...
|
|
122
145
|
end
|
|
123
146
|
end
|
|
124
147
|
end
|
|
125
148
|
end
|
|
126
149
|
```
|
|
127
150
|
|
|
128
|
-
Verbs are looked up in a static frozen table (`Textus::
|
|
129
|
-
|
|
130
|
-
The instantiate-and-call step lives in `Dispatcher.invoke`. `RoleScope` builds the `Call` (request state) and delegates to `Dispatcher.invoke`. Every system interaction flows through `Dispatch::Gate#fire(event)` — surfaces, internal cascades (rdeps), and async job workers all use the same path. Gate runs Auth → Ledger → Executor in sequence.
|
|
131
|
-
|
|
132
|
-
`boot` and `doctor` are actions like any other — reached through `Dispatcher::VERBS`.
|
|
133
|
-
|
|
134
|
-
One collaborator lives outside the dispatcher because it's composed by actions, not invoked as a verb:
|
|
135
|
-
|
|
136
|
-
- `Envelope::IO::{Reader,Writer}` — own the parse and persist halves of the write pipeline; the audit-append-as-final-step invariant lives in `Writer`.
|
|
151
|
+
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.
|
|
137
152
|
|
|
138
153
|
## Container
|
|
139
154
|
|
|
@@ -142,130 +157,105 @@ Use cases never see the raw `Store`. `Textus::Container` is a single record hold
|
|
|
142
157
|
```ruby
|
|
143
158
|
Container = Data.define(
|
|
144
159
|
:manifest, :file_store, :schemas, :root,
|
|
145
|
-
:audit_log, :
|
|
160
|
+
:audit_log, :workflows, :gate
|
|
146
161
|
)
|
|
147
162
|
```
|
|
148
163
|
|
|
149
|
-
The `Store` builds one `Container` at boot; every action receives it via `(container:, call:)`.
|
|
164
|
+
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).
|
|
150
165
|
|
|
151
166
|
## Ports
|
|
152
167
|
|
|
153
|
-
Ports are infrastructure adapters with an interface defined by the domain. Each port is independently replaceable — swap the implementation for tests or alternative runtimes without touching application or domain code.
|
|
154
|
-
|
|
155
168
|
| Class | Role |
|
|
156
169
|
|---|---|
|
|
157
|
-
| `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`.
|
|
158
|
-
| `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking,
|
|
159
|
-
| `Ports::Clock` | Supplies `Time.now` —
|
|
160
|
-
| `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel
|
|
161
|
-
| `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline.
|
|
162
|
-
| `Ports::
|
|
163
|
-
| `Ports::SentinelStore` | Reads and writes
|
|
164
|
-
| `Ports::WatcherLock` | Single-watcher `flock` guard used by `Dispatch::Runtime::Watch` to ensure only one watcher loop is active per store root. |
|
|
170
|
+
| `Ports::Storage::FileStore` | Bytes-only FS I/O — `read`, `write`, `delete`, `exists?`, `etag`. |
|
|
171
|
+
| `Ports::AuditLog` | Append-only structured log (`audit.log`). Owns seq numbering, file-locking, rotation. |
|
|
172
|
+
| `Ports::Clock` | Supplies `Time.now` — module-function so tests can swap without DI boilerplate. |
|
|
173
|
+
| `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel. |
|
|
174
|
+
| `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. |
|
|
175
|
+
| `Ports::JobStore` | Persistent job queue used by `drain` workers; tracks ready/leased/done/failed jobs. |
|
|
176
|
+
| `Ports::SentinelStore` | Reads and writes per-target sentinel files for managed-file detection. |
|
|
165
177
|
|
|
166
178
|
Application use cases access ports only through `Container` fields — never through the raw `Store`.
|
|
167
179
|
|
|
168
|
-
###
|
|
169
|
-
|
|
170
|
-
`Envelope::IO::Reader` and `Envelope::IO::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
|
|
180
|
+
### Envelope
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
`Envelope::Reader` and `Envelope::Writer` split the envelope pipeline into read-only parse and write-with-audit halves.
|
|
173
183
|
|
|
174
|
-
**
|
|
184
|
+
**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.
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
**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.
|
|
177
187
|
|
|
178
|
-
Both are built from a `Container` via named constructors — `Writer.from(container:, call:)`
|
|
188
|
+
Both are built from a `Container` via named constructors — `Writer.from(container:, call:)` and `Reader.from(container:)`.
|
|
179
189
|
|
|
180
190
|
## Manifest carving
|
|
181
191
|
|
|
182
|
-
Manifest
|
|
183
|
-
|
|
184
|
-
`Manifest` itself is a `Data.define` struct — a composition record with four named members:
|
|
192
|
+
`Manifest` is a `Data.define` struct with four named members:
|
|
185
193
|
|
|
186
194
|
| Member | Class | Responsibility |
|
|
187
195
|
|---|---|---|
|
|
188
|
-
| `data` | `Manifest::Data` | Frozen value:
|
|
189
|
-
| `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`.
|
|
190
|
-
| `policy` | `Manifest::Policy` | Zone/capability authority — `
|
|
191
|
-
| `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)`
|
|
192
|
-
|
|
193
|
-
Rationale: cleaner test seams — a use case that only needs key resolution constructs a `Manifest::Resolver` from a stub `Data`; one that only needs rule lookup constructs a `Manifest::Rules` directly. No consumer is forced to build the full manifest to exercise one sub-view.
|
|
194
|
-
|
|
195
|
-
The four members are wired in `Manifest.build` (`lib/textus/manifest.rb`). `Manifest::Data` constructs `Policy` internally during `initialize`; the others are assembled by the loader and handed in as named arguments.
|
|
196
|
+
| `data` | `Manifest::Data` | Frozen value: raw, root, lanes, entries, audit_config, role_caps. |
|
|
197
|
+
| `resolver` | `Manifest::Resolver` | Key → `Resolution(entry, path, remaining)`. Nested entry enumeration and fuzzy-match suggestions. |
|
|
198
|
+
| `policy` | `Manifest::Policy` | Zone/capability authority — `verb_for_lane`, `roles_with_capability`, `propose_lane_for(role)`. |
|
|
199
|
+
| `rules` | `Manifest::Rules` | Pattern-matched rule engine. `rules.for(key)` → `RuleSet(guard, retention, react)`. |
|
|
196
200
|
|
|
197
201
|
## Read path (`store.get(key)`)
|
|
198
202
|
|
|
199
|
-
`
|
|
203
|
+
`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.
|
|
200
204
|
|
|
201
|
-
1. CLI
|
|
202
|
-
2. `
|
|
203
|
-
3. `
|
|
205
|
+
1. CLI/MCP surface calls `store.as(role).get(key)`.
|
|
206
|
+
2. `Gate#dispatch` runs Auth → `Action::Get#call`.
|
|
207
|
+
3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is returned stale — the read does not refresh it; `drain` does.
|
|
206
210
|
|
|
207
211
|
## Write path (`store.put(key, ...)`)
|
|
208
212
|
|
|
209
|
-
1. CLI/MCP surface calls `store.as(role).put(key, meta:, body
|
|
210
|
-
2. `
|
|
211
|
-
3. `
|
|
212
|
-
4. `
|
|
213
|
-
5. Publishes `:entry_written` via `container.steps` and fires a cascade Gate event for rdep materialization.
|
|
214
|
-
|
|
215
|
-
`Actions::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape. All write actions inherit `WriteVerb#run_with_cascade`, which enqueues `materialize` jobs for rdeps after the write completes.
|
|
213
|
+
1. CLI/MCP surface calls `store.as(role).put(key, meta:, body:)`.
|
|
214
|
+
2. `Gate#dispatch` runs Auth → `Action::Put#call`.
|
|
215
|
+
3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
|
|
216
|
+
4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
`Action::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape.
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
## Workflow path (`drain` + materialize jobs)
|
|
220
221
|
|
|
221
|
-
`
|
|
222
|
+
The workflow system owns the produce pipeline. `Produce::Engine.converge(container:, call:, keys:)` is the entry point that `Jobs::Materialize` calls.
|
|
222
223
|
|
|
223
224
|
For each key, `Engine#produce_one`:
|
|
224
225
|
|
|
225
|
-
1.
|
|
226
|
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
- On success: normalises the handler result, checks auth, persists via `Envelope::IO::Writer`, publishes `:entry_fetched` unless the etag is unchanged.
|
|
231
|
-
- `Acquire::Handler` resolves and invokes the step under the timeout deadline. (The sibling **projection** sub-path — `from: derive` entries — runs `Acquire::Projection`, which renders data files through `Acquire::Serializer::{Json,Yaml,Text}` before persisting.)
|
|
232
|
-
2. **Render phase** — `entry.publish_via(context)` calls `Pipeline::Render#bytes_for(target:, data:, boot:)` to expand the Mustache template and copy the result to the publish target via `Ports::Publisher`. Returns `nil` if no publish is configured (skipped).
|
|
233
|
-
|
|
234
|
-
Per-entry failures are published as `:produce_failed` by `Actions::Materialize` after `Engine.converge` returns. A held `BuildLock` is a soft miss — the in-flight build already produces fresh output.
|
|
235
|
-
|
|
236
|
-
Reactive produce is enqueued as `materialize` jobs onto `Ports::Queue` when `entry_written`/`entry_deleted`/`entry_renamed` fires; a worker (`drain`/`serve`) runs them through `converge`. A held `BuildLock` is a soft miss — the in-flight build already produces fresh output.
|
|
226
|
+
1. Looks up a registered workflow via `container.workflows.for(key)` (Pattern matching against the key).
|
|
227
|
+
2. If a workflow matches: `Workflow::Runner.new(definition, container:, call:).run(key)`.
|
|
228
|
+
- `Runner` builds a `Workflow::Context`, executes each step in sequence, then publishes.
|
|
229
|
+
- **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.
|
|
230
|
+
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.
|
|
237
231
|
|
|
238
|
-
|
|
232
|
+
**Workflow files** live in `.textus/workflows/*.rb`. The DSL:
|
|
239
233
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
234
|
+
```ruby
|
|
235
|
+
Textus.workflow "my-producer" do
|
|
236
|
+
match "artifacts.derived.*"
|
|
237
|
+
step :fetch do |data, ctx|
|
|
238
|
+
{ content: { "key" => ctx.key } }
|
|
239
|
+
end
|
|
240
|
+
publish # uses built-in publish (writes to entry, then to publish targets)
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
243
|
|
|
244
244
|
## Agent surface (boot + pulse + MCP)
|
|
245
245
|
|
|
246
|
-
Agents and plugins talk to a textus store through three layers:
|
|
247
|
-
|
|
248
246
|
```
|
|
249
|
-
|
|
247
|
+
agent/plugin ──▶ gate (CLI | MCP) ──▶ Store ──▶ memory (.textus/)
|
|
250
248
|
```
|
|
251
249
|
|
|
252
250
|
Two transports, one façade:
|
|
253
251
|
|
|
254
252
|
- **CLI** — human/script surface. `textus boot`, `textus pulse --since=N`, `textus get/put/...`.
|
|
255
|
-
- **MCP** — agent surface. `textus mcp serve` runs a stdio JSON-RPC 2.0 server
|
|
256
|
-
|
|
257
|
-
Both transports call `store.<verb>(..., role:)` (or `store.as(role).<verb>(...)`). No duplicate logic.
|
|
258
|
-
|
|
259
|
-
The agent loop (cadence guide in [`agents-mcp.md`](../how-to/agents-mcp.md)):
|
|
260
|
-
|
|
261
|
-
1. **Session start:** `boot()` → contract envelope (zones, entries, schemas, write_flows, agent_quickstart with `latest_seq`).
|
|
262
|
-
2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor}`.
|
|
263
|
-
3. **On demand:** `get`, `put`, `propose`, `fetch`, `schema_show`, `rule_explain`.
|
|
264
|
-
|
|
265
|
-
Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change to the manifest, hooks, or schemas; ADR 0074); audit cursor falls off the keep window as `CursorExpired`. Both signal "call `boot` again."
|
|
253
|
+
- **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
254
|
|
|
267
|
-
|
|
255
|
+
The agent loop:
|
|
268
256
|
|
|
269
|
-
|
|
257
|
+
1. **Session start:** `boot()` → contract envelope (lanes, entries, workflows, write_flows, agent_quickstart).
|
|
258
|
+
2. **Per turn:** `pulse(since=cursor)` → `{cursor, changed, stale, pending_review, doctor, next_due_at}`.
|
|
259
|
+
3. **On demand:** `get`, `put`, `propose`, `schema_show`, `rule_explain`, `drain`.
|
|
270
260
|
|
|
271
|
-
|
|
261
|
+
Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change to manifest/schemas); audit cursor expiry as `CursorExpired`. Both signal "call `boot` again."
|
data/lib/textus/action/accept.rb
CHANGED
|
@@ -11,8 +11,6 @@ module Textus
|
|
|
11
11
|
cli "accept"
|
|
12
12
|
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
13
|
|
|
14
|
-
BURN = :sync
|
|
15
|
-
|
|
16
14
|
def initialize(pending_key:)
|
|
17
15
|
super()
|
|
18
16
|
@pending_key = pending_key
|
|
@@ -39,13 +37,6 @@ module Textus
|
|
|
39
37
|
|
|
40
38
|
Textus::Action::KeyDelete.new(key: @pending_key).call(container: container, call: call)
|
|
41
39
|
|
|
42
|
-
container.steps.publish(
|
|
43
|
-
:proposal_accepted,
|
|
44
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
45
|
-
key: @pending_key,
|
|
46
|
-
target_key: target,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
40
|
{ "protocol" => Textus::PROTOCOL, "accepted" => @pending_key, "target_key" => target, "action" => action }
|
|
50
41
|
end
|
|
51
42
|
end
|
data/lib/textus/action/audit.rb
CHANGED
|
@@ -24,8 +24,6 @@ module Textus
|
|
|
24
24
|
arg :limit, Integer, required: false, description: "maximum number of rows to return"
|
|
25
25
|
view(:cli) { |rows, _i| { "verb" => "audit", "rows" => rows } }
|
|
26
26
|
|
|
27
|
-
BURN = :sync
|
|
28
|
-
|
|
29
27
|
def initialize(**kwargs)
|
|
30
28
|
super()
|
|
31
29
|
@query = Query.build(**kwargs.slice(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit))
|
data/lib/textus/action/blame.rb
CHANGED
|
@@ -15,8 +15,6 @@ module Textus
|
|
|
15
15
|
arg :limit, Integer, required: false, description: "maximum number of audit rows to return"
|
|
16
16
|
view(:cli) { |rows, inputs| { "verb" => "blame", "key" => inputs[:key], "rows" => rows } }
|
|
17
17
|
|
|
18
|
-
BURN = :sync
|
|
19
|
-
|
|
20
18
|
def initialize(key:, limit: nil)
|
|
21
19
|
super()
|
|
22
20
|
@key = key
|
data/lib/textus/action/boot.rb
CHANGED
|
@@ -6,20 +6,11 @@ module Textus
|
|
|
6
6
|
extend Textus::Contract::DSL
|
|
7
7
|
|
|
8
8
|
verb :boot
|
|
9
|
-
summary "Return the orientation contract:
|
|
9
|
+
summary "Return the orientation contract: lanes, agent_quickstart, agent_protocol, and pre-computed artifacts."
|
|
10
10
|
surfaces :cli, :mcp
|
|
11
|
-
arg :lean, :boolean,
|
|
12
|
-
description: "return only orientation essentials (zones, agent_quickstart, contract_etag) for cheap session-start injection"
|
|
13
|
-
|
|
14
|
-
BURN = :sync
|
|
15
|
-
|
|
16
|
-
def initialize(lean: nil)
|
|
17
|
-
super()
|
|
18
|
-
@lean = lean
|
|
19
|
-
end
|
|
20
11
|
|
|
21
12
|
def call(container:, **)
|
|
22
|
-
Textus::Boot.build(container: container
|
|
13
|
+
Textus::Boot.build(container: container)
|
|
23
14
|
end
|
|
24
15
|
end
|
|
25
16
|
end
|
|
@@ -19,8 +19,6 @@ module Textus
|
|
|
19
19
|
"defaults to false, so omitting it applies the move immediately"
|
|
20
20
|
view { |v, _i| v.to_h }
|
|
21
21
|
|
|
22
|
-
BURN = :sync
|
|
23
|
-
|
|
24
22
|
def initialize(from:, to:, dry_run: false)
|
|
25
23
|
super()
|
|
26
24
|
@from = from
|
|
@@ -45,7 +43,7 @@ module Textus
|
|
|
45
43
|
{ "op" => "mv", "from" => key, "to" => "#{@to}#{key[@from.length..]}" }
|
|
46
44
|
end
|
|
47
45
|
|
|
48
|
-
plan = Textus::
|
|
46
|
+
plan = Textus::Jobs::Plan.new(steps: steps, warnings: [])
|
|
49
47
|
return plan if @dry_run
|
|
50
48
|
|
|
51
49
|
rewrite_manifest!(root)
|