textus 0.12.1 → 0.14.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/ARCHITECTURE.md +60 -40
- data/CHANGELOG.md +214 -0
- data/README.md +6 -12
- data/SPEC.md +4 -1
- data/docs/conventions.md +8 -8
- data/lib/textus/application/context.rb +4 -0
- data/lib/textus/application/reads/blame.rb +1 -1
- data/lib/textus/application/reads/deps.rb +15 -0
- data/lib/textus/application/reads/freshness.rb +2 -2
- data/lib/textus/application/reads/get.rb +8 -11
- data/lib/textus/application/reads/list.rb +15 -0
- data/lib/textus/application/reads/published.rb +15 -0
- data/lib/textus/application/reads/rdeps.rb +15 -0
- data/lib/textus/application/reads/schema_envelope.rb +15 -0
- data/lib/textus/application/reads/stale.rb +15 -0
- data/lib/textus/application/reads/uid.rb +15 -0
- data/lib/textus/application/reads/validate_all.rb +15 -0
- data/lib/textus/application/reads/where.rb +15 -0
- data/lib/textus/application/refresh/all.rb +2 -2
- data/lib/textus/application/refresh/worker.rb +3 -3
- data/lib/textus/application/writes/accept.rb +7 -7
- data/lib/textus/application/writes/build.rb +10 -47
- data/lib/textus/application/writes/mv.rb +144 -0
- data/lib/textus/application/writes/publish.rb +41 -9
- data/lib/textus/application/writes/reject.rb +37 -0
- data/lib/textus/cli/verb/accept.rb +1 -2
- data/lib/textus/cli/verb/audit.rb +3 -3
- data/lib/textus/cli/verb/blame.rb +1 -2
- data/lib/textus/cli/verb/build.rb +6 -2
- data/lib/textus/cli/verb/delete.rb +1 -2
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -2
- data/lib/textus/cli/verb/get.rb +2 -3
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mv.rb +1 -1
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/put.rb +2 -2
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -2
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -2
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb.rb +6 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/policy/predicates/schema_valid.rb +3 -3
- data/lib/textus/entry/base.rb +28 -0
- data/lib/textus/entry/json.rb +59 -0
- data/lib/textus/entry/markdown.rb +46 -0
- data/lib/textus/entry/text.rb +35 -0
- data/lib/textus/entry/yaml.rb +59 -0
- data/lib/textus/entry.rb +16 -0
- data/lib/textus/envelope.rb +44 -14
- data/lib/textus/intro.rb +56 -0
- data/lib/textus/manifest/entry/parser.rb +84 -0
- data/lib/textus/manifest/entry/validators/events.rb +21 -0
- data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
- data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
- data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
- data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
- data/lib/textus/manifest/entry/validators.rb +20 -0
- data/lib/textus/manifest/entry.rb +35 -213
- data/lib/textus/manifest.rb +6 -16
- data/lib/textus/operations/reads.rb +39 -0
- data/lib/textus/operations/refresh.rb +27 -0
- data/lib/textus/operations/writes.rb +21 -0
- data/lib/textus/operations.rb +44 -0
- data/lib/textus/projection.rb +5 -4
- data/lib/textus/refresh.rb +3 -4
- data/lib/textus/schema/tools.rb +8 -7
- data/lib/textus/store/reader.rb +1 -1
- data/lib/textus/store/validator.rb +3 -3
- data/lib/textus/store/writer.rb +5 -74
- data/lib/textus/store.rb +1 -55
- data/lib/textus/version.rb +1 -1
- metadata +23 -4
- data/lib/textus/composition.rb +0 -72
- data/lib/textus/proposal.rb +0 -10
- data/lib/textus/store/mover.rb +0 -167
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24eda2a2cfec1884eb3c675189493827b6c0f43d5765922dbee06b70fbdc9685
|
|
4
|
+
data.tar.gz: 84e3850666541900f4c3b99f71c59b73dd142c7001f80fa9e8233d38f984719e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d68ad56bc7891aefda10e47234f1bbd1efbfd75b576f7981c3374641efbd306dc66947b382b74c70da24bc58122090c3e0789c33b1a8291aa1005b9e6c952e2e
|
|
7
|
+
data.tar.gz: fd1c1cefad648636b4fb5aeba100d81c66472f8e1b50d07ba2239607c19179842902abf0b76925786c66868049acfd507a9a76bd07e39026dd2aa0d261ebfdad
|
data/ARCHITECTURE.md
CHANGED
|
@@ -2,73 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
```
|
|
4
4
|
┌─ Interface ────────────────────────────────────────────────┐
|
|
5
|
-
│ CLI verbs:
|
|
6
|
-
│
|
|
5
|
+
│ CLI verbs: ops = Operations.for(store, role:) │
|
|
6
|
+
│ ops.reads.<name>.call(...) │
|
|
7
|
+
│ ops.writes.<name>.call(...) │
|
|
8
|
+
│ ops.refresh.<name>.call(...) │
|
|
7
9
|
└──────────────────────┬─────────────────────────────────────┘
|
|
8
10
|
│
|
|
9
11
|
┌─ Application ────────▼─────────────────────────────────────┐
|
|
10
12
|
│ Context (per-request: store, role, correlation, │
|
|
11
|
-
│ clock, dry_run; can_read?/can_write
|
|
12
|
-
│
|
|
13
|
+
│ clock, dry_run; can_read?/can_write?; │
|
|
14
|
+
│ Context.system(store) for infra path) │
|
|
15
|
+
│ Operations (facade with .reads/.writes/.refresh) │
|
|
13
16
|
│ │
|
|
14
|
-
│ reads/get
|
|
15
|
-
│
|
|
16
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│
|
|
17
|
+
│ reads/{get,list,where,uid,schema_envelope,deps,rdeps, │
|
|
18
|
+
│ published,stale,validate_all,freshness,audit, │
|
|
19
|
+
│ blame,policy_explain}.rb │
|
|
20
|
+
│ writes/{put,delete,mv,accept,reject,build,publish}.rb │
|
|
21
|
+
│ refresh/{worker,orchestrator,all}.rb │
|
|
19
22
|
└──────────┬───────────────────────────────┬─────────────────┘
|
|
20
23
|
│ uses domain │ uses ports
|
|
21
24
|
┌─ Domain ─▼─────────────────────────────────────────────────┐
|
|
22
|
-
│ Permission
|
|
23
|
-
│ Freshness::Policy
|
|
24
|
-
│
|
|
25
|
+
│ Permission (write/read predicate per zone) │
|
|
26
|
+
│ Freshness::{Policy,Verdict,Evaluator} │
|
|
27
|
+
│ Action Outcome │
|
|
28
|
+
│ Policy::Promotion (proposal accept gates) │
|
|
25
29
|
└──────────────────────────────────────────┬─────────────────┘
|
|
26
30
|
│ implements
|
|
27
31
|
┌─ Infrastructure ─────────────────────────▼─────────────────┐
|
|
28
|
-
│ Store (pure adapter —
|
|
29
|
-
│ Reader#
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
│
|
|
32
|
+
│ Store (pure adapter — filesystem + hooks) │
|
|
33
|
+
│ Reader#{get,list,where,uid,deps,rdeps,published, │
|
|
34
|
+
│ stale,validate_all,read_raw_envelope, │
|
|
35
|
+
│ schema_envelope} │
|
|
36
|
+
│ Writer#{write_envelope_to_disk,delete_envelope_from_ │
|
|
37
|
+
│ disk,existing_uid_for,ensure_uid, │
|
|
38
|
+
│ enforce_name_match!,serialize_for_put} │
|
|
39
|
+
│ AuditLog, Staleness, Validator, Sentinel │
|
|
40
|
+
│ Manifest (Entry, Rules, Schema, permission_for) │
|
|
41
|
+
│ Hooks::{Registry,Dispatcher,Loader,Dsl,Builtin} │
|
|
42
|
+
│ Infra::{Publisher,EventBus,Clock,Refresh::Lock, │
|
|
43
|
+
│ Refresh::Detached} │
|
|
44
|
+
│ Entry::{Markdown,Json,Yaml,Text} (format strategies) │
|
|
35
45
|
└────────────────────────────────────────────────────────────┘
|
|
36
46
|
|
|
37
47
|
Dependency rule: arrows point DOWN. Domain has zero outbound
|
|
38
48
|
imports. Application imports Domain + Infra (via ports).
|
|
39
49
|
```
|
|
40
50
|
|
|
41
|
-
## Read path (`
|
|
51
|
+
## Read path (`ops.reads.get.call(key)`)
|
|
42
52
|
|
|
43
|
-
1. CLI verb (or any caller)
|
|
44
|
-
2. `
|
|
45
|
-
3. `Reads::Get#call` reads the envelope from disk via
|
|
46
|
-
4. Resolves `
|
|
53
|
+
1. CLI verb (or any external caller) builds `ops = Textus::Operations.for(store, role:)` then `ops.reads.get.call(key)`.
|
|
54
|
+
2. `Operations::Reads#get` returns an `Application::Reads::Get.new(ctx:, orchestrator:)` instance bound to the request context.
|
|
55
|
+
3. `Reads::Get#call(key)` reads the bare envelope from disk via `@ctx.store.reader.read_raw_envelope(key)`.
|
|
56
|
+
4. Resolves the manifest rules for the key via `@ctx.store.manifest.rules_for(key)` and extracts the `refresh` policy.
|
|
47
57
|
5. `Domain::Freshness::Evaluator.call(policy, envelope, now:)` returns a `Verdict`.
|
|
48
58
|
6. If fresh → annotate envelope (`stale: false`, `refreshing: false`) and return.
|
|
49
59
|
7. Otherwise `policy.decide(verdict) → Action` (data, not behavior).
|
|
50
|
-
8. `Orchestrator
|
|
60
|
+
8. `Refresh::Orchestrator#execute(action, key:)` interprets the `Action`:
|
|
51
61
|
- `Action::Return` → `Outcome::Skipped`
|
|
52
|
-
- `Action::RefreshSync` → run Worker inline → `Refreshed | Failed`
|
|
53
|
-
- `Action::RefreshTimed(budget_ms:)` → race Worker thread vs budget; on timeout, kill thread, fire `:
|
|
62
|
+
- `Action::RefreshSync` → run `Refresh::Worker` inline → `Refreshed | Failed`
|
|
63
|
+
- `Action::RefreshTimed(budget_ms:)` → race Worker thread vs budget; on timeout, kill thread, fire `:refresh_backgrounded`, fork+detach child, return `Outcome::Detached`
|
|
54
64
|
9. Map outcome → envelope annotations (`stale`, `refreshing`, `refresh_error`) and return.
|
|
55
65
|
|
|
56
|
-
## Write path (`
|
|
66
|
+
## Write path (`ops.writes.put.call(key, ...)`)
|
|
57
67
|
|
|
58
|
-
1. CLI verb calls `
|
|
59
|
-
2. `Writes::Put#call` checks
|
|
60
|
-
3. Delegates
|
|
61
|
-
|
|
68
|
+
1. CLI verb calls `ops = Operations.for(store, role:)` then `ops.writes.put.call(key, meta:, body:, content:, if_etag:, suppress_events:)`.
|
|
69
|
+
2. `Writes::Put#call` validates the key, resolves the manifest entry, and checks `@ctx.can_write?(mentry.zone)` — raises `WriteForbidden` if denied.
|
|
70
|
+
3. Delegates raw I/O to `Store::Writer#write_envelope_to_disk(key, mentry:, payload:, ctx:, if_etag:)`, which:
|
|
71
|
+
- Resolves the path via `Manifest#resolve`
|
|
72
|
+
- Serializes via `Entry.for_format(...).serialize(...)`
|
|
73
|
+
- Validates against schema if declared
|
|
74
|
+
- Etag-checks if `if_etag:` provided (raises `EtagMismatch` on conflict)
|
|
75
|
+
- Writes to disk via `File.binwrite`
|
|
76
|
+
- Appends the audit row
|
|
77
|
+
4. On success, publishes `:entry_put` via the bus, with `store: @ctx.with_role(@ctx.role)`, `key:`, `envelope:`, `correlation_id:`.
|
|
62
78
|
|
|
63
|
-
The same pattern applies to `Writes::Delete
|
|
79
|
+
The same pattern applies to `Writes::{Delete,Mv,Accept,Reject,Build,Publish}`: each takes a `Context`, checks permissions at the use-case layer, delegates raw I/O to `Store::Writer` or `Infra::Publisher`, and fires the matching event.
|
|
64
80
|
|
|
65
|
-
## Refresh path (`
|
|
81
|
+
## Refresh path (`ops.refresh.worker.run(key)`)
|
|
66
82
|
|
|
67
|
-
1. CLI `Verb::Refresh`
|
|
68
|
-
2.
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
- Publishes `:refresh_began` via the injected `Infra::EventBus`.
|
|
83
|
+
1. CLI `Verb::Refresh` builds `ops = Operations.for(store, role: "runner")` then calls `ops.refresh.worker.run(key)`.
|
|
84
|
+
2. `Refresh::Worker#run(key)`:
|
|
85
|
+
- Resolves the manifest entry, looks up the intake handler via `store.registry.rpc_callable(:resolve_intake, mentry.intake_handler)`.
|
|
86
|
+
- Publishes `:refresh_started` via the bus.
|
|
72
87
|
- Invokes the handler under a 30s `Timeout.timeout` budget.
|
|
73
88
|
- On any error: publishes `:refresh_failed`, then re-raises (or wraps in `UsageError`).
|
|
74
|
-
- On success: normalizes the return shape, persists via `
|
|
89
|
+
- On success: normalizes the return shape, persists via `Application::Writes::Put` with `suppress_events: true`, publishes `:entry_refreshed` (unless the etag is unchanged).
|
|
90
|
+
3. The batch entry point is `Application::Refresh::All.call(ctx, prefix:, zone:)` which lists stale entries via `Application::Reads::Stale`, then runs `Worker#run` per entry, returning a summary envelope `{ refreshed: [...], failed: [...], skipped: [...] }`.
|
|
91
|
+
|
|
92
|
+
## Infrastructure-side hook dispatch
|
|
93
|
+
|
|
94
|
+
The hook bus needs an `Application::Context` even when fired from inside `Store#initialize` or other infrastructure-side code paths. `Application::Context.system(store)` returns a Context with `role: "human"` and a fresh correlation_id, designed for exactly this case — see `lib/textus/store.rb` (the `:store_loaded` publish), `lib/textus/doctor.rb` (the doctor-check view), and `lib/textus/projection.rb` (the transform_rows view).
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,220 @@ 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.14.0 — 2026-05-26
|
|
13
|
+
|
|
14
|
+
### Breaking (Ruby API only — CLI JSON output unchanged)
|
|
15
|
+
|
|
16
|
+
- `Operations.reads.get.call(...)` and every other use case that returned
|
|
17
|
+
an envelope Hash now returns `Textus::Envelope` (a `Data.define`
|
|
18
|
+
instance). Call `envelope.to_h_for_wire` to recover the previous Hash
|
|
19
|
+
shape for JSON serialization. The Hash shape itself is byte-identical.
|
|
20
|
+
- `Operations.writes.build.call(...)` return shape no longer includes
|
|
21
|
+
`published_leaves`. Call `Operations.writes.publish.call(...)`
|
|
22
|
+
separately for that. The CLI verb `textus build` runs both
|
|
23
|
+
automatically, so CLI users see no change.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `Textus::Application::Writes::Publish` — new use case that copies
|
|
28
|
+
nested-leaf files to their `publish_each` targets. Fires
|
|
29
|
+
`:file_published`.
|
|
30
|
+
- `Operations.writes.publish` — factory exposing the new use case.
|
|
31
|
+
- `Textus::Envelope` (now a class, was a module) — typed accessors for
|
|
32
|
+
`protocol`, `key`, `zone`, `owner`, `path`, `format`, `uid`, `etag`,
|
|
33
|
+
`schema_ref`, `meta`, `body`, `content`, `freshness`. Methods:
|
|
34
|
+
`to_h_for_wire`, `stale?`, `refreshing?`.
|
|
35
|
+
|
|
36
|
+
### Internal
|
|
37
|
+
|
|
38
|
+
- `Application::Writes::Build` trimmed from 116 LOC to ~50 LOC; now
|
|
39
|
+
only materializes generator-zone entries.
|
|
40
|
+
- All ~17 internal `env["..."]` call sites migrated to typed access.
|
|
41
|
+
- `Envelope.build` no longer carries `# rubocop:disable
|
|
42
|
+
Metrics/ParameterLists` (the `Data.define` member list serves the
|
|
43
|
+
same role more clearly).
|
|
44
|
+
|
|
45
|
+
### Reference
|
|
46
|
+
|
|
47
|
+
- See [ADR 0007](docs/architecture/decisions/0007-envelope-data-class.md).
|
|
48
|
+
|
|
49
|
+
## 0.13.1 — 2026-05-26
|
|
50
|
+
|
|
51
|
+
### Internal
|
|
52
|
+
|
|
53
|
+
- `Manifest::Entry` (260 LOC, 11 responsibilities) decomposed into:
|
|
54
|
+
- `Manifest::Entry::Parser` — raw hash → Entry value object.
|
|
55
|
+
- `Manifest::Entry::Validators::*` — one file per validation rule
|
|
56
|
+
(events, publish_each, inject_intro, index_filename, format_matrix).
|
|
57
|
+
- `Manifest::Entry` (~50 LOC) — value object with attr readers,
|
|
58
|
+
zone-kind predicates, and `publish_target_for`.
|
|
59
|
+
- Each validation rule is now independently testable. Adding a new
|
|
60
|
+
rule is one new file under `lib/textus/manifest/entry/validators/`
|
|
61
|
+
plus one line in `Validators::REGISTERED`.
|
|
62
|
+
- Pattern matches the existing `doctor/check/*` (~15 files, same
|
|
63
|
+
shape).
|
|
64
|
+
|
|
65
|
+
### Compatibility
|
|
66
|
+
|
|
67
|
+
- `Manifest::Entry` keeps all public attribute readers, predicates,
|
|
68
|
+
and `publish_target_for`. External callers consuming `Entry`
|
|
69
|
+
instances see no change. Embedders who subclassed `Entry` may
|
|
70
|
+
need adjustment.
|
|
71
|
+
|
|
72
|
+
## 0.13.0 — 2026-05-26
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- `Textus::Entry::Base` grows 7 abstract class methods that concrete
|
|
77
|
+
strategies must implement: `nested_glob`, `inject_uid`,
|
|
78
|
+
`enforce_name_match!`, `rewrite_name`, `serialize_for_put`,
|
|
79
|
+
`validate_path_extension`.
|
|
80
|
+
- `Textus::Entry.infer_from_extension(ext)` — registry method
|
|
81
|
+
replacing the deleted `Manifest::EXT_TO_FORMAT` constant.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- Format-specific branches in `Manifest#nested_glob`,
|
|
86
|
+
`Manifest::Entry#{validate_format_matrix!,resolve_format!,
|
|
87
|
+
validate_index_filename!}`, `Store::Writer#{ensure_uid,
|
|
88
|
+
enforce_name_match!,serialize_for_put}`, and
|
|
89
|
+
`Application::Writes::Mv#rewrite_name_for_mv!` all collapse to
|
|
90
|
+
single-line delegations to `Entry.for_format(fmt).<method>(...)`.
|
|
91
|
+
- The 3 rubocop `Metrics/*` disable comments in
|
|
92
|
+
`Manifest::Entry#validate_format_matrix!` are removed; the method
|
|
93
|
+
is now 3 lines.
|
|
94
|
+
|
|
95
|
+
### Removed
|
|
96
|
+
|
|
97
|
+
- `Textus::Manifest::EXT_TO_FORMAT` constant. Use
|
|
98
|
+
`Textus::Entry.infer_from_extension(ext)` instead.
|
|
99
|
+
|
|
100
|
+
### Reference
|
|
101
|
+
|
|
102
|
+
- See [ADR 0006](docs/architecture/decisions/0006-format-strategy-extraction.md).
|
|
103
|
+
|
|
104
|
+
## 0.12.6 — 2026-05-26
|
|
105
|
+
|
|
106
|
+
### Examples
|
|
107
|
+
|
|
108
|
+
- New `examples/project/` — demonstrates textus as the context store
|
|
109
|
+
for your own project (identity + runbooks + ADR proposal flow,
|
|
110
|
+
projecting `CLAUDE.md` and `AGENTS.md` at the repo root). Staged as
|
|
111
|
+
a fictional Rails service (`ledger`) so the entries read like a real
|
|
112
|
+
codebase, and the pre-staged ADR proposal is `accept`-runnable
|
|
113
|
+
end-to-end (carries a valid `frontmatter:` payload for its target).
|
|
114
|
+
- Refined `examples/claude-plugin/` — reduced to one entry of each kind
|
|
115
|
+
(one agent, one skill, one command, one identity entry, one output,
|
|
116
|
+
one intake recipe). Removed `bin/`, `Rakefile`, `lefthook.yml`, the
|
|
117
|
+
duplicate `github_folder.rb` recipe, and the per-recipe README.
|
|
118
|
+
- Fixed `examples/claude-plugin/recipes/skill_fanout.rb` — the recipe
|
|
119
|
+
routed inner writes through `store.list/put/delete`, which were
|
|
120
|
+
removed in v0.12.2. Now uses `Operations.writes.{put,delete}` against
|
|
121
|
+
the `Application::Context` that hooks actually receive.
|
|
122
|
+
- Updated `spec/examples/skill_fanout_hook_spec.rb` to test the recipe
|
|
123
|
+
against a Context-like duck type, matching the runtime contract.
|
|
124
|
+
|
|
125
|
+
## 0.12.5 — 2026-05-26
|
|
126
|
+
|
|
127
|
+
### Documentation
|
|
128
|
+
|
|
129
|
+
- Rewrote `docs/events.md` to use the textus/3 event names from
|
|
130
|
+
`Hooks::Registry::EVENTS` (`:entry_put`, `:entry_deleted`,
|
|
131
|
+
`:build_completed`, `:entry_renamed`, `:proposal_accepted`,
|
|
132
|
+
`:proposal_rejected`, `:file_published`, `:store_loaded`,
|
|
133
|
+
`:entry_refreshed`, `:refresh_started`, `:refresh_failed`,
|
|
134
|
+
`:refresh_backgrounded`). All hook examples now register against
|
|
135
|
+
events that actually exist.
|
|
136
|
+
- Rewrote `docs/zones.md` to use textus/3 vocabulary (`intake` not
|
|
137
|
+
`inbox`, `write_policy:` not `writable_by:`, `rules:` not
|
|
138
|
+
`policies:`, `runner` role not `script`). Manifest fixtures bump
|
|
139
|
+
to `version: textus/3`.
|
|
140
|
+
- Rewrote `ARCHITECTURE.md` to match the v0.12.4 layering:
|
|
141
|
+
`Operations` facade replaces `Composition`, `Application::Reads/Writes`
|
|
142
|
+
use cases replace direct `Store#get/#put` calls, `Store` is pure
|
|
143
|
+
infrastructure.
|
|
144
|
+
- `SPEC.md` Composition → Operations sweep; removed references to
|
|
145
|
+
deleted Store delegators.
|
|
146
|
+
- `docs/recipe-github-skill-bundle.md` updated to use the
|
|
147
|
+
`Operations.writes.{put,delete}` inner-write surface.
|
|
148
|
+
|
|
149
|
+
## [0.12.4] — 2026-05-26
|
|
150
|
+
|
|
151
|
+
### Breaking
|
|
152
|
+
|
|
153
|
+
- Removed `Textus::Store#list`, `#where`, `#deps`, `#rdeps`, `#published`,
|
|
154
|
+
`#stale`, `#validate_all`, `#uid`, `#schema_envelope`, and `#fire_event`.
|
|
155
|
+
Use `Textus::Operations.for(store).reads.<name>.call(...)` instead.
|
|
156
|
+
- Removed `Textus::Store::Writer#delete`, `#accept`, `#reject`. Use
|
|
157
|
+
`Textus::Operations.for(store, role:).writes.<verb>.call(...)`.
|
|
158
|
+
- Removed `Textus::Store::Mover`. The mv use case lives entirely in
|
|
159
|
+
`Textus::Application::Writes::Mv` now.
|
|
160
|
+
|
|
161
|
+
### Added
|
|
162
|
+
|
|
163
|
+
- `Textus::Application::Reads::{List,Where,Uid,SchemaEnvelope,Deps,Rdeps,Published,Stale,ValidateAll}`.
|
|
164
|
+
- `Textus::Application::Writes::Reject`.
|
|
165
|
+
- `Textus::Application::Context.system(store)` for infrastructure-side
|
|
166
|
+
hook dispatch.
|
|
167
|
+
|
|
168
|
+
### Internal
|
|
169
|
+
|
|
170
|
+
- Internal call sites (`Projection`, `Schema::Tools`, `Application::Refresh::All`,
|
|
171
|
+
`Doctor::Check::SchemaViolations`) now route reads through `Operations`.
|
|
172
|
+
|
|
173
|
+
See [ADR 0005](docs/architecture/decisions/0005-store-facade-final-removal.md).
|
|
174
|
+
|
|
175
|
+
## [0.12.3] — 2026-05-26
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
- `textus intro` output now includes an `agent_protocol` block: envelope shape, role-resolution rules, and four canonical recipes (`read`, `write`, `propose`, `refresh`). One `textus intro` call is sufficient orientation for a fresh AI agent to operate the store without consulting `SPEC.md`.
|
|
179
|
+
|
|
180
|
+
### Compatibility
|
|
181
|
+
- Fully additive. All pre-0.12.3 fields on `Textus::Intro.run` retain their existing keys, types, and shapes. The wire `"protocol"` field continues to hold the string `"textus/3"`.
|
|
182
|
+
|
|
183
|
+
### Examples
|
|
184
|
+
- `examples/claude-plugin/.textus/templates/claude-root.mustache` now projects the new `agent_protocol` recipes into the rendered `CLAUDE.md` via the existing `inject_intro:` mechanism. A plugin's CLAUDE.md auto-projects the four recipes alongside zone authority — agents reading CLAUDE.md get full orientation without a separate `textus intro` call. This is the canonical pattern for plugin authors who want recipes inline.
|
|
185
|
+
- `examples/claude-plugin/` trimmed to one of each surface (one agent, one skill, one command, one identity entry, one JSON output). Removed: the duplicate `fact-checker` variants, the `marketplace.json` projection (the JSON-output lesson is already shown by `plugin.json`), the degenerate `local_file` intake demo (pulled a file from the same store), and unused demo hooks (`rank_by_recency`, `build-stamp`). File count drops from 51 to 33; manifest from 142 to 99 lines.
|
|
186
|
+
|
|
187
|
+
## [0.12.2] — 2026-05-26
|
|
188
|
+
|
|
189
|
+
### Breaking changes
|
|
190
|
+
|
|
191
|
+
- **Removed `Textus::Composition`.** All call sites now go through
|
|
192
|
+
`Textus::Operations.for(store, role:)`. The new facade groups use-cases
|
|
193
|
+
by kind: `ops.writes.put`, `ops.reads.get`, `ops.refresh.worker`, etc.
|
|
194
|
+
No alias, no deprecation warning — internal callers update on upgrade.
|
|
195
|
+
- **Removed `Store#put / #get / #delete / #accept / #reject / #mv`.** These
|
|
196
|
+
were thin shims over `Composition`. Use `Operations` directly.
|
|
197
|
+
- **Removed `Writer#put`** (the explicit "Backward-compat shim").
|
|
198
|
+
|
|
199
|
+
### Internal
|
|
200
|
+
|
|
201
|
+
- Added `Application::Writes::Mv` use-case wrapping `Store::Mover`.
|
|
202
|
+
- Internal Application callers (Accept, Build, Refresh::Worker, Refresh::All,
|
|
203
|
+
Reads::Blame) no longer re-enter via the top-level facade.
|
|
204
|
+
- Audited `spec/` and removed redundant examples (~-48 LOC; most redundancy
|
|
205
|
+
was already absorbed by call-site migration in earlier tasks).
|
|
206
|
+
|
|
207
|
+
See [ADR 0004](docs/architecture/decisions/0004-operations-rename-and-store-facade-removal.md).
|
|
208
|
+
|
|
209
|
+
### Migration
|
|
210
|
+
|
|
211
|
+
There is no migration path for `Composition` and the Store facade methods —
|
|
212
|
+
they were internal. External consumers (hooks, custom verbs, gem embedders)
|
|
213
|
+
that referenced these symbols must update:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Before
|
|
217
|
+
ctx = Textus::Composition.context(store, role: "agent")
|
|
218
|
+
Textus::Composition.writes_put(ctx).call(key, body: "...")
|
|
219
|
+
# or:
|
|
220
|
+
store.put(key, body: "...", as: "agent")
|
|
221
|
+
|
|
222
|
+
# After
|
|
223
|
+
Textus::Operations.for(store, role: "agent").writes.put.call(key, body: "...")
|
|
224
|
+
```
|
|
225
|
+
|
|
12
226
|
## 0.12.1 — textus/2 hint fix (2026-05-26)
|
|
13
227
|
|
|
14
228
|
### Fixed
|
data/README.md
CHANGED
|
@@ -13,20 +13,12 @@ Reference implementation in Ruby. Wire format `textus/3`. SPEC: [`SPEC.md`](SPEC
|
|
|
13
13
|
|
|
14
14
|
Two versions, deliberately independent:
|
|
15
15
|
|
|
16
|
-
- **Protocol wire string:** `textus/3`.
|
|
17
|
-
- **Gem version:** semver, currently `0.
|
|
16
|
+
- **Protocol wire string:** `textus/3`. Breaking changes require `textus/4`.
|
|
17
|
+
- **Gem version:** semver, currently `0.14.0`. Decoupled from the protocol string — internal refactors bump the gem; only wire-format changes bump the protocol.
|
|
18
18
|
|
|
19
19
|
Envelope payloads carry the `protocol` field. The gem version is irrelevant to the wire format.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
textus 0.12.0 does not include a built-in migrator. If you are upgrading from
|
|
24
|
-
a textus/2 store (gem versions ≤ 0.10.x), first install textus 0.11.x and run:
|
|
25
|
-
|
|
26
|
-
textus migrate --to=textus/3
|
|
27
|
-
|
|
28
|
-
Then upgrade to 0.12.0. Pre-0.11.0 audit-log rows with `role: ai|script|build`
|
|
29
|
-
are tolerated verbatim by the reader — no rewrite step required.
|
|
21
|
+
See [CHANGELOG.md](CHANGELOG.md) for per-release notes.
|
|
30
22
|
|
|
31
23
|
## Install
|
|
32
24
|
|
|
@@ -87,6 +79,8 @@ For the full shape — Claude plugin with agents, skills, commands, pending walk
|
|
|
87
79
|
|
|
88
80
|
- **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/output/marketplace.json | jq .` works without going through textus — the in-store file *is* the consumer-shaped artifact. Structured outputs carry `_meta` at the top level (`generated_at`, `from`, `template`, `transform`).
|
|
89
81
|
- **Per-leaf publishing.** Nested entries declare `publish_each: "skills/{basename}/SKILL.md"`. Every leaf byte-copies to its consumer location on `textus build`. No more hand-mirrored `agents/` / `skills/` / `commands/` directories.
|
|
82
|
+
- **Split build/publish (v0.14.0).** `Application::Writes::Build` materializes generator-zone entries; `Application::Writes::Publish` copies nested leaves to `publish_each` targets. The `textus build` CLI verb calls both and merges results, so wire output is unchanged.
|
|
83
|
+
- **Typed envelopes (v0.14.0).** `Textus::Envelope` is a `Data.define` value object with typed accessors (`.meta`, `.body`, `.etag`, `.uid`, `.freshness`, …). Ruby API callers get IDE help and `NoMethodError` on typos. The CLI JSON wire format is preserved byte-for-byte via `envelope.to_h_for_wire`.
|
|
90
84
|
- **Stable identity (`uid:`).** 16-char hex, auto-minted on first `put`, preserved across writes and moves. `textus key mv old.key new.key` renames in place — uid survives, audit row records `from_key`, `to_key`, `uid`. Reorganising a tree no longer breaks references.
|
|
91
85
|
- **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus key normalize --dry-run|--write` rewrites existing stores with illegal segments deterministically.
|
|
92
86
|
- **`textus intro`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded hooks, write flows per role, the full CLI verb table. The boot signal for any agent — one tool call and it knows your store.
|
|
@@ -162,7 +156,7 @@ Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintai
|
|
|
162
156
|
bundle exec rspec
|
|
163
157
|
```
|
|
164
158
|
|
|
165
|
-
~
|
|
159
|
+
~637 examples; includes conformance fixtures A–I from SPEC §12.
|
|
166
160
|
|
|
167
161
|
## Code quality
|
|
168
162
|
|
data/SPEC.md
CHANGED
|
@@ -828,7 +828,10 @@ Both read and write paths flow through the application layer:
|
|
|
828
828
|
- **Reads** flow through `Application::Reads::Get`, which takes a `Context` and dispatches refresh via `Application::Refresh::Orchestrator`.
|
|
829
829
|
- **Writes** flow through `Application::Writes::{Put,Delete,Build,Accept,Publish}`, each taking a `Context`. Permission checks happen at the use-case layer (via `Context#can_write?`); I/O happens at `Store::Writer#write_envelope_to_disk` (pure).
|
|
830
830
|
- `Application::Context` is the universal request object: it carries `store`, `role`, `correlation_id`, `clock`, and `dry_run`. Use cases never thread these as separate kwargs.
|
|
831
|
-
- `Textus::
|
|
831
|
+
- `Textus::Operations` is the factory CLI verbs (and future MCP server / HTTP shim)
|
|
832
|
+
use to construct Contexts and use cases. `Operations.for(store, role:)` returns
|
|
833
|
+
a memoized facade with `.reads`, `.writes`, and `.refresh` namespaces mirroring the
|
|
834
|
+
files under `lib/textus/application/{reads,writes,refresh}/`.
|
|
832
835
|
|
|
833
836
|
See `ARCHITECTURE.md` for an ASCII diagram and the full read-path walkthrough.
|
|
834
837
|
|
data/docs/conventions.md
CHANGED
|
@@ -20,7 +20,7 @@ Recommended top-level layout — the spec allows alternatives, but this is what
|
|
|
20
20
|
zones/
|
|
21
21
|
identity/ # identity, voice, slow-changing facts — humans only
|
|
22
22
|
working/ # agent-writable working memory
|
|
23
|
-
|
|
23
|
+
intake/ # runner-fed external inputs
|
|
24
24
|
review/ # AI proposals awaiting accept
|
|
25
25
|
output/ # generated by build runners — never edit by hand
|
|
26
26
|
```
|
|
@@ -82,25 +82,25 @@ Full contract for both shapes is in [`../SPEC.md` §5.2 and §5.2.1](../SPEC.md)
|
|
|
82
82
|
|
|
83
83
|
## Intake and freshness
|
|
84
84
|
|
|
85
|
-
External inputs land via `:
|
|
85
|
+
External inputs land via `:resolve_intake` hooks, not shell commands. Each intake entry names a registered handler; refresh is on demand:
|
|
86
86
|
|
|
87
87
|
```sh
|
|
88
|
-
textus refresh
|
|
89
|
-
textus refresh-stale --zone=
|
|
88
|
+
textus refresh intake.notion.roadmap --as=runner
|
|
89
|
+
textus refresh-stale --zone=intake --as=runner # everything past its TTL
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Freshness budgets live in the top-level `
|
|
92
|
+
Freshness budgets live in the top-level `rules:` block, matched by glob:
|
|
93
93
|
|
|
94
94
|
```yaml
|
|
95
|
-
|
|
96
|
-
- match:
|
|
95
|
+
rules:
|
|
96
|
+
- match: intake.notion.**
|
|
97
97
|
refresh: { ttl: 6h, on_stale: warn } # warn | sync | timed_sync
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
A typical scheduled-refresh integration shells the `refresh-stale` sweep itself:
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
textus refresh-stale --zone=
|
|
103
|
+
textus refresh-stale --zone=intake --as=runner # in cron / CI
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
See [`./zones.md` §6](zones.md) for the full intake contract and [`./events.md`](events.md) for writing custom handlers.
|
|
@@ -5,6 +5,10 @@ module Textus
|
|
|
5
5
|
class Context
|
|
6
6
|
attr_reader :store, :role, :correlation_id
|
|
7
7
|
|
|
8
|
+
def self.system(store)
|
|
9
|
+
new(store: store, role: "human")
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
def initialize(store:, role:, correlation_id: nil, clock: Time, dry_run: false)
|
|
9
13
|
@store = store
|
|
10
14
|
@role = role.to_s
|
|
@@ -13,7 +13,7 @@ module Textus
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def call(key:, limit: nil)
|
|
16
|
-
audit_rows = Textus::
|
|
16
|
+
audit_rows = Textus::Application::Reads::Audit.new(ctx: @ctx).call(key: key, limit: limit)
|
|
17
17
|
path = resolve_path(key)
|
|
18
18
|
return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
|
|
19
19
|
|
|
@@ -30,12 +30,12 @@ module Textus
|
|
|
30
30
|
set = @ctx.store.manifest.rules_for(mentry.key)
|
|
31
31
|
refresh = set.refresh
|
|
32
32
|
envelope = safe_get(mentry.key)
|
|
33
|
-
last = envelope&.dig("
|
|
33
|
+
last = envelope&.meta&.dig("last_refreshed_at")
|
|
34
34
|
|
|
35
35
|
return base_row(mentry, last).merge(status: :no_policy) if refresh.nil?
|
|
36
36
|
|
|
37
37
|
fp = refresh.to_freshness_policy
|
|
38
|
-
verdict = @evaluator.call(fp, envelope
|
|
38
|
+
verdict = @evaluator.call(fp, envelope, now: @ctx.now)
|
|
39
39
|
status = if verdict.fresh? then :fresh
|
|
40
40
|
elsif last.nil? then :never_refreshed
|
|
41
41
|
else :stale
|
|
@@ -40,21 +40,18 @@ module Textus
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
42
|
def annotate(envelope, verdict, refreshing:, refresh_error: nil)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
fresh = {
|
|
44
|
+
"stale" => verdict.stale?,
|
|
45
|
+
"stale_reason" => verdict.reason,
|
|
46
|
+
"refreshing" => refreshing,
|
|
47
|
+
}
|
|
48
|
+
fresh["refresh_error"] = refresh_error if refresh_error
|
|
49
|
+
envelope.with(freshness: fresh)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# No refresh policy applies to this key — treat as fresh, skip evaluation/orchestration.
|
|
52
53
|
def annotate_fresh(envelope)
|
|
53
|
-
envelope
|
|
54
|
-
envelope["stale"] = false
|
|
55
|
-
envelope["stale_reason"] = nil
|
|
56
|
-
envelope["refreshing"] = false
|
|
57
|
-
envelope
|
|
54
|
+
envelope.with(freshness: { "stale" => false, "stale_reason" => nil, "refreshing" => false })
|
|
58
55
|
end
|
|
59
56
|
end
|
|
60
57
|
end
|