textus 0.2.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 +7 -0
- data/CHANGELOG.md +163 -0
- data/README.md +200 -0
- data/SPEC.md +720 -0
- data/docs/architecture.md +57 -0
- data/docs/conventions.md +85 -0
- data/exe/textus +4 -0
- data/lib/textus/audit_log.rb +32 -0
- data/lib/textus/builder.rb +191 -0
- data/lib/textus/builtin_fetchers.rb +63 -0
- data/lib/textus/cli.rb +394 -0
- data/lib/textus/dependencies.rb +23 -0
- data/lib/textus/doctor.rb +281 -0
- data/lib/textus/entry/json.rb +41 -0
- data/lib/textus/entry/markdown.rb +39 -0
- data/lib/textus/entry/text.rb +23 -0
- data/lib/textus/entry/yaml.rb +39 -0
- data/lib/textus/entry.rb +30 -0
- data/lib/textus/errors.rb +168 -0
- data/lib/textus/etag.rb +13 -0
- data/lib/textus/extension_registry.rb +48 -0
- data/lib/textus/extensions.rb +29 -0
- data/lib/textus/init.rb +51 -0
- data/lib/textus/intro.rb +104 -0
- data/lib/textus/key_distance.rb +53 -0
- data/lib/textus/manifest.rb +394 -0
- data/lib/textus/migrate_keys.rb +187 -0
- data/lib/textus/mustache.rb +117 -0
- data/lib/textus/projection.rb +80 -0
- data/lib/textus/proposal.rb +27 -0
- data/lib/textus/publisher.rb +71 -0
- data/lib/textus/refresh.rb +75 -0
- data/lib/textus/role.rb +20 -0
- data/lib/textus/schema.rb +90 -0
- data/lib/textus/schema_tools.rb +87 -0
- data/lib/textus/store.rb +607 -0
- data/lib/textus/store_view.rb +18 -0
- data/lib/textus/version.rb +4 -0
- data/lib/textus.rb +31 -0
- metadata +156 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '092917d22f25afea72145540f4dc8ce32e5a4f6c65889564f54e258e52eab7df'
|
|
4
|
+
data.tar.gz: 163d891ea50b7c651740f499ffe7ba5191aba0c2c327afbf058a1bf1fc019942
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1fa336c3387f503c7ac846fea1f7d1de92a9633ea0104d4c0882d48951bf6abbbb1366e0a9028abac1a138f048fe2b90c018228c7b9a19725eb251c835f43b6c
|
|
7
|
+
data.tar.gz: 2f99647d031c7f0004d4b0264c51a616f8da03bcb72a68f4e12c0e483b5865ee3d826786f7d519bb4d7aac2a8ec9d4f9a1092220ce376a226e1436e9c1e5affc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
8
|
+
(currently `textus/1`, embedded in every envelope as `protocol`). The protocol
|
|
9
|
+
is additive within a major; a new major would change the wire string.
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.2.0] — 2026-05-20 — Storage rewrite, agent surface, extension DSL (BREAKING)
|
|
14
|
+
|
|
15
|
+
This release reshapes textus from a markdown-only frontmatter store into a
|
|
16
|
+
multi-format, agent-introspectable context layer.
|
|
17
|
+
|
|
18
|
+
### Breaking changes
|
|
19
|
+
- **Per-entry formats.** Markdown is no longer the only storage shape. Manifest
|
|
20
|
+
entries declare `format: markdown|json|yaml|text`; format is inferred from
|
|
21
|
+
the path extension when omitted. JSON/YAML entries store `_meta` at the top
|
|
22
|
+
level (`generated_at`, `from`, `template`, `reducer`).
|
|
23
|
+
- **Strict key grammar.** Segments must match `/^[a-z0-9][a-z0-9-]*$/`. No
|
|
24
|
+
underscores, no uppercase, no dots-in-segments. Max 8 segments × 64 chars.
|
|
25
|
+
Enforced at manifest load, `put`, and `mv`. Existing stores with illegal
|
|
26
|
+
segments migrate via `textus migrate-keys --dry-run|--write`.
|
|
27
|
+
- **Publisher rename.** `Textus::Symlink` is now `Textus::Publisher`. Symlink
|
|
28
|
+
mode is gone; publish is `FileUtils.cp` + sentinel.
|
|
29
|
+
- **Sentinels relocated.** `.textus-managed.json` files move from beside the
|
|
30
|
+
published file to `<store_root>/sentinels/<target_rel>.textus-managed.json`.
|
|
31
|
+
Legacy sibling sentinels are auto-migrated on next publish.
|
|
32
|
+
- **Init profiles removed.** `textus init --profile=…` and
|
|
33
|
+
`lib/textus/profiles/*.yaml` are gone. `textus init` writes a single default
|
|
34
|
+
manifest declaring all five SPEC zones and pre-creates `zones/<name>/`
|
|
35
|
+
subdirectories.
|
|
36
|
+
- **Extension surface (carried over from earlier 0.2 work).** `.textus/parsers/`
|
|
37
|
+
and `.textus/calculators/` are gone — drop `.rb` files into
|
|
38
|
+
`.textus/extensions/`. `Textus::Parsers` and `Textus::Calculators` modules
|
|
39
|
+
removed. Manifest `source.parse`/`source.from` → `source.fetcher` +
|
|
40
|
+
`source.config`; projection `transform:` → `reducer:`; `hooks:` → `events:`
|
|
41
|
+
(no `on_` prefix; `on_stale` removed entirely). CLI: `--parse=NAME` removed;
|
|
42
|
+
`textus hooks list` removed (use `textus extensions list --kind=hook`).
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`textus intro`** — single-call orientation envelope (`zones`, `entries`,
|
|
46
|
+
`extensions`, `write_flows`, `cli_verbs`) for agents landing in a textus-
|
|
47
|
+
managed project.
|
|
48
|
+
- **`inject_intro: true`** flag on derived markdown/text entries — merges the
|
|
49
|
+
intro envelope into the template data so `CLAUDE.md` (or any boot doc) can
|
|
50
|
+
render an orientation preamble.
|
|
51
|
+
- **`textus doctor`** — health check with 8 categories (missing schemas /
|
|
52
|
+
templates / extensions, illegal nested keys, sentinel orphan/drift, audit log
|
|
53
|
+
readability, unowned schema fields). `ok: true` only when zero error-level
|
|
54
|
+
issues; warnings/info don't flip the bit.
|
|
55
|
+
- **`textus mv <old> <new>`** — same-zone, same-format rename. Preserves uid,
|
|
56
|
+
writes an `mv` audit row with `from_key`, `to_key`, `from_path`, `to_path`,
|
|
57
|
+
`uid`. `--dry-run` plans without writing.
|
|
58
|
+
- **`uid:`** field — 16-char hex stable identity (`SecureRandom.hex(8)`),
|
|
59
|
+
auto-minted on first `Store#put`, preserved across writes and moves. Lives
|
|
60
|
+
in frontmatter for markdown, `_meta.uid` for json/yaml. Surfaced on the
|
|
61
|
+
envelope.
|
|
62
|
+
- **`publish_each:`** template on nested entries — each leaf byte-copies to a
|
|
63
|
+
per-leaf target derived from `{leaf}`, `{basename}`, `{key}`, `{ext}`. Closes
|
|
64
|
+
the per-file publish loop for plugins that mirror `working.*` into
|
|
65
|
+
`agents/`, `skills/<name>/SKILL.md`, `commands/`.
|
|
66
|
+
- **`textus migrate-keys`** — run-once helper renaming files whose basenames
|
|
67
|
+
violate the new strict key grammar. `--dry-run` reports proposed renames and
|
|
68
|
+
collisions; `--write` applies them bottom-up and writes `migrate-keys` audit
|
|
69
|
+
rows.
|
|
70
|
+
- **Per-format strategies** under `lib/textus/entry/{markdown,json,yaml,text}.rb`
|
|
71
|
+
with a uniform parse/serialize contract. `Entry.for_format(name)` dispatcher.
|
|
72
|
+
- **`textus.intro` + CLAUDE.md preamble** — the example's CLAUDE.md now opens
|
|
73
|
+
with auto-generated zone-and-write-flow orientation for the agent.
|
|
74
|
+
- **Actionable error hints.** Every `Textus::Error` exposes `code`, `message`,
|
|
75
|
+
and `hint`. `UnknownKey` carries up to 5 ranked "did you mean" suggestions
|
|
76
|
+
(shared-prefix + bounded Levenshtein). The CLI prints
|
|
77
|
+
`code: msg\n → hint` to stderr alongside the JSON envelope on stdout.
|
|
78
|
+
- **Extension surface (carried over from earlier 0.2 work).**
|
|
79
|
+
`Textus.fetcher`, `Textus.reducer`, `Textus.hook` DSL verbs. Per-`Store`
|
|
80
|
+
`ExtensionRegistry` (no global state). `textus refresh KEY --as=script`.
|
|
81
|
+
`textus extensions list [--kind=fetcher|reducer|hook]`. In-process lifecycle
|
|
82
|
+
events (`:put`, `:delete`, `:refresh`, `:build`, `:accept`). `Textus::Refresh`
|
|
83
|
+
driver with 2 s timeout. `Textus::StoreView` read-only proxy. Audit log gains
|
|
84
|
+
a 7th JSON-extras column. `Init.run` scaffolds `.textus/extensions/` with a
|
|
85
|
+
README stub.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
- `Projection#run` no longer stamps `generated_at` onto Hash reducer results —
|
|
89
|
+
`_meta.generated_at` is the single source of truth for structured outputs
|
|
90
|
+
(avoids duplicate timestamps in `marketplace.json`-style files).
|
|
91
|
+
- `UnknownKey` raised from `Store#get`/`put`/`mv` (nested-tree file misses) now
|
|
92
|
+
carries suggestions, matching `Manifest#resolve`.
|
|
93
|
+
|
|
94
|
+
### Example
|
|
95
|
+
- `examples/claude-plugin/` rewritten as a real Claude Code plugin
|
|
96
|
+
(`voice-tools`) entirely managed by textus: `.claude-plugin/{plugin,
|
|
97
|
+
marketplace}.json` and `CLAUDE.md` derived; `agents/`, `skills/<name>/
|
|
98
|
+
SKILL.md`, `commands/` mirrored via `publish_each:`; pending-zone walkthrough
|
|
99
|
+
exercising the AI propose → human accept loop; intake fetcher, in-process
|
|
100
|
+
reducer / hook, deep-nested key demos.
|
|
101
|
+
|
|
102
|
+
### SPEC
|
|
103
|
+
- Rewritten for §1–§5 (layers, manifest, formats, publish), §10 (envelope adds
|
|
104
|
+
`format`, `content`, `uid`), audit verb table (`mv`, `migrate-keys`), CLI
|
|
105
|
+
verb table, Fixture G.
|
|
106
|
+
|
|
107
|
+
## [0.1.0] — 2026-05-19
|
|
108
|
+
|
|
109
|
+
First public release. Implements protocol `textus/1`.
|
|
110
|
+
|
|
111
|
+
### Added — storage and model
|
|
112
|
+
- Zone-based storage layout under `.textus/zones/`: `canon`, `working`, `intake`,
|
|
113
|
+
`pending`, `derived`. Each zone declares `writable_by` roles in the manifest.
|
|
114
|
+
- Role resolution order: `--as` flag → `TEXTUS_ROLE` env → `.textus/role` file →
|
|
115
|
+
default `human`. Recognized roles: `human`, `ai`, `script`, `build`.
|
|
116
|
+
- Append-only TSV audit log at `.textus/audit.log`, file-locked on every write.
|
|
117
|
+
- Schemas with required/optional fields, type checking, and per-field
|
|
118
|
+
`maintained_by` ownership plus `evolution` metadata (`added_in`,
|
|
119
|
+
`migrate_from`).
|
|
120
|
+
- Manifest backwards compatibility: a manifest without `zones:` synthesizes the
|
|
121
|
+
legacy `fixed` / `state` / `derived` zones.
|
|
122
|
+
|
|
123
|
+
### Added — compute and publish
|
|
124
|
+
- Vendored Mustache renderer (~120 LOC, depth-bounded at 8).
|
|
125
|
+
- Projection engine: `select` / `pluck` / `sort_by` / `limit` / `transform`
|
|
126
|
+
(1000-row cap, single 2 s timeout on transforms).
|
|
127
|
+
- `textus build` materializes derived entries from `projection:` + `template:`
|
|
128
|
+
declarations.
|
|
129
|
+
- Atomic symlink publish via `publish_to:`, with copy-mode fallback and a
|
|
130
|
+
`.textus-managed.json` sentinel for filesystems without symlinks.
|
|
131
|
+
|
|
132
|
+
### Added — extension points
|
|
133
|
+
- **Parsers** — built-ins for `json`, `csv`, `markdown-links`, `ical-events`,
|
|
134
|
+
`rss`. Auto-load from `.textus/parsers/<name>.rb`, 2 s timeout.
|
|
135
|
+
- **Calculators** — pure projection-row transforms registered via
|
|
136
|
+
`Textus::Calculators.register`. Auto-load from `.textus/calculators/<name>.rb`,
|
|
137
|
+
2 s timeout.
|
|
138
|
+
- **Hooks** — manifest entries declare `hooks:` keyed by lifecycle event
|
|
139
|
+
(`on_put`, `on_delete`, `on_refresh`, `on_stale`, `on_accept`, `on_build`).
|
|
140
|
+
textus enumerates hooks via `textus hooks list`; external runners execute.
|
|
141
|
+
|
|
142
|
+
### Added — CLI verbs
|
|
143
|
+
- Read: `list`, `where`, `get`, `schema`, `stale`, `deps`, `rdeps`, `published`,
|
|
144
|
+
`validate-all`, `hooks list`.
|
|
145
|
+
- Write: `put`, `delete`, `build`, `accept`.
|
|
146
|
+
- Scaffolding: `init` (profiles: `personal`, `claude-plugin`), `schema-init`,
|
|
147
|
+
`schema-diff`, `schema-migrate`.
|
|
148
|
+
- Flags: `--as=ROLE`, `--zone=Z`, `--prefix=KEY`, `--parse=NAME`, `--if-etag=E`,
|
|
149
|
+
`--stale`, `--strict`, `--format=json`.
|
|
150
|
+
|
|
151
|
+
### Added — examples
|
|
152
|
+
- `examples/claude-plugin/` — a working `.textus/` tree that publishes a
|
|
153
|
+
Claude Code `CLAUDE.md` and a `marketplace.json` via projection + Mustache.
|
|
154
|
+
Demonstrates intake, parsers, calculators, hooks, and schema field ownership.
|
|
155
|
+
- `examples/mcp-server/` — 50-line MCP server wrapping `textus get` / `list` /
|
|
156
|
+
`put` as tools.
|
|
157
|
+
|
|
158
|
+
### Added — quality tooling
|
|
159
|
+
- RuboCop config with rubocop-rspec; codebase passes with zero offenses.
|
|
160
|
+
- Lefthook hooks (`pre-commit` runs rubocop on staged Ruby; `pre-push` runs
|
|
161
|
+
rspec + rubocop). Install via `brew bundle install`.
|
|
162
|
+
- `gemspec` packages only `lib/`, `exe/`, `README.md`, `SPEC.md`, and two
|
|
163
|
+
curated `docs/` files. Internal plan documents stay out of the gem.
|
data/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# textus
|
|
2
|
+
|
|
3
|
+
Reference Ruby implementation of the **textus/1** protocol — a storage convention and JSON wire protocol for agent-readable project memory: addressable dotted keys, schema-validated entries (markdown, JSON, YAML, or text per entry), role-gated writes, declarative compute, and copy-based publish targets.
|
|
4
|
+
|
|
5
|
+
See [`SPEC.md`](SPEC.md) for the protocol. Implementation notes live in [`docs/`](docs/).
|
|
6
|
+
|
|
7
|
+
## Versioning
|
|
8
|
+
|
|
9
|
+
Two versions, deliberately independent:
|
|
10
|
+
|
|
11
|
+
- **Protocol wire string:** `textus/1`. Stable; breaking changes require `textus/2`.
|
|
12
|
+
- **Gem version:** semver, currently `0.2.0`. Gem `0.x.y` and `1.x` both speak `textus/1`.
|
|
13
|
+
|
|
14
|
+
Envelope payloads carry the `protocol` field; the gem version is irrelevant to the wire format.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
gem install textus # when published
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or from this repo:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bundle install
|
|
26
|
+
bundle exec exe/textus --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
Bootstrap a fresh tree:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle exec exe/textus init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This scaffolds `.textus/` with a starter manifest, the five zone directories, baseline schemas, and an empty audit log. The resulting layout:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
.textus/
|
|
41
|
+
manifest.yaml
|
|
42
|
+
audit.log
|
|
43
|
+
role
|
|
44
|
+
schemas/
|
|
45
|
+
templates/
|
|
46
|
+
extensions/
|
|
47
|
+
zones/
|
|
48
|
+
canon/ # human-only
|
|
49
|
+
working/ # human, ai, script
|
|
50
|
+
intake/ # script (declared external inputs)
|
|
51
|
+
pending/ # ai (proposals awaiting accept)
|
|
52
|
+
derived/ # build only (computed outputs)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A minimal `manifest.yaml`:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
version: textus/1
|
|
59
|
+
|
|
60
|
+
zones:
|
|
61
|
+
- { name: canon, writable_by: [human] }
|
|
62
|
+
- { name: working, writable_by: [human, ai, script] }
|
|
63
|
+
- { name: intake, writable_by: [script] }
|
|
64
|
+
- { name: pending, writable_by: [ai] }
|
|
65
|
+
- { name: derived, writable_by: [build] }
|
|
66
|
+
|
|
67
|
+
entries:
|
|
68
|
+
- key: canon.identity
|
|
69
|
+
path: canon/identity.md
|
|
70
|
+
zone: canon
|
|
71
|
+
schema: identity
|
|
72
|
+
|
|
73
|
+
- key: working.network.org
|
|
74
|
+
path: working/network/org
|
|
75
|
+
zone: working
|
|
76
|
+
schema: person
|
|
77
|
+
owner: textus:network
|
|
78
|
+
nested: true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Manifest `path:` fields are relative to `.textus/zones/` — implementations prepend `zones/` when resolving. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
|
|
82
|
+
|
|
83
|
+
Read and write:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
textus get working.network.org.jane --format=json
|
|
87
|
+
textus list --zone=working --format=json
|
|
88
|
+
echo '{"frontmatter":{"name":"bob","relationship":"peer","org":"acme"},"body":"hi\n"}' \
|
|
89
|
+
| textus put working.network.org.bob --as=human --stdin --format=json
|
|
90
|
+
textus stale --zone=derived --format=json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## CLI verbs
|
|
94
|
+
|
|
95
|
+
All verbs accept `--format=json` and emit the envelope defined in SPEC §8. Write verbs require `--as=<role>` (subject to role-resolution order, §5.1).
|
|
96
|
+
|
|
97
|
+
**Read verbs (no role required):**
|
|
98
|
+
|
|
99
|
+
| Verb | Purpose |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `list [--prefix=K] [--zone=Z] [--stale]` | Enumerate keys, optionally filtered |
|
|
102
|
+
| `where K` | Resolve a key to its filesystem path |
|
|
103
|
+
| `get K` | Return the full envelope |
|
|
104
|
+
| `schema K` | Return the schema bound to an entry |
|
|
105
|
+
| `stale [--prefix=K] [--zone=Z] [--strict]` | List stale derived/intake entries |
|
|
106
|
+
| `deps K` / `rdeps K` | Forward/reverse projection dependencies |
|
|
107
|
+
| `published` | List `publish_to:` targets and their backing keys |
|
|
108
|
+
| `validate-all` | Validate every entry against its schema (incl. `maintained_by`) |
|
|
109
|
+
| `extensions list [--kind=K]` | Enumerate registered fetchers, reducers, and declared hooks |
|
|
110
|
+
|
|
111
|
+
**Write verbs (role-gated per zone):**
|
|
112
|
+
|
|
113
|
+
| Verb | Role |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `put K --stdin --as=R [--fetcher=NAME]` | per zone |
|
|
116
|
+
| `delete K --if-etag=E --as=R` | per zone |
|
|
117
|
+
| `refresh K --as=script` | per zone (typically `script`) |
|
|
118
|
+
| `build [--prefix=K] [--dry-run]` | `build` |
|
|
119
|
+
| `accept K --as=human` | `human` only |
|
|
120
|
+
|
|
121
|
+
**Scaffolding (human-only):**
|
|
122
|
+
|
|
123
|
+
| Verb | Purpose |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `init` | Scaffold a fresh `.textus/` tree |
|
|
126
|
+
| `schema-init NAME` | Write a stub schema |
|
|
127
|
+
| `schema-diff NAME` | Compare on-disk schema against entries claiming it |
|
|
128
|
+
| `schema-migrate NAME [--rename=OLD:NEW]` | Rewrite frontmatter keys across affected entries |
|
|
129
|
+
|
|
130
|
+
## Zones and roles
|
|
131
|
+
|
|
132
|
+
| Zone | `writable_by` | Purpose |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `canon` | `[human]` | Identity, voice, immutable principles |
|
|
135
|
+
| `working` | `[human, ai, script]` | Active project state — notes, decisions, network |
|
|
136
|
+
| `intake` | `[script]` | Declared external inputs (calendar, feeds, scraped pages) |
|
|
137
|
+
| `pending` | `[ai]` | AI proposals awaiting `textus accept` |
|
|
138
|
+
| `derived` | `[build]` | Computed outputs from `textus build` |
|
|
139
|
+
|
|
140
|
+
The effective role for any CLI call is resolved in order: `--as` flag, then `TEXTUS_ROLE` env, then `.textus/role`, then default `human`. Mismatches return `write_forbidden`. Every write records the resolved role in `.textus/audit.log`.
|
|
141
|
+
|
|
142
|
+
## Compute layer
|
|
143
|
+
|
|
144
|
+
Derived entries are not authored by hand. Each declares a `projection:` block (select prefixes, pluck fields, optional sort/limit/transform) and optionally a Mustache template under `.textus/templates/`. textus implements a deliberately restricted Mustache subset (variables, sections, inverted sections, comments — no partials, no lambdas, no HTML escaping). Results are bounded at 1000 rows; template recursion at depth 8.
|
|
145
|
+
|
|
146
|
+
Derived entries may declare `format:` to be `markdown` (default), `json`, `yaml`, or `text`. The in-store file is the consumer-shaped artifact — `cat .textus/zones/derived/marketplace.json` returns valid JSON without going through textus. `publish_to:` then performs a byte-for-byte file copy of that artifact to each destination, alongside a `.textus-managed.json` sentinel. See SPEC §5.2, §5.3, and §5.12.
|
|
147
|
+
|
|
148
|
+
## Extension points
|
|
149
|
+
|
|
150
|
+
Three DSL verbs:
|
|
151
|
+
|
|
152
|
+
- **`Textus.fetcher(:name) do |config:, store:|`** — pulls data into an intake entry. Returns one of `{ frontmatter:, body: }`, `{ content: }` (for `format: json|yaml` entries), or `{ body: }` (raw bytes); the store normalizes all three. Configured via `source.fetcher` in the manifest. Five built-ins ship out of the box: `json`, `csv`, `markdown-links`, `ical-events`, `rss`.
|
|
153
|
+
- **`Textus.reducer(:name) do |rows:, config:|`** — shapes rows in a derived projection. Pure function. Configured via `projection.reducer`.
|
|
154
|
+
- **`Textus.hook(:event, :name) do |kwargs|`** — reacts to a lifecycle event. Five events: `:put`, `:delete`, `:refresh`, `:build`, `:accept`.
|
|
155
|
+
|
|
156
|
+
Extension files live in `.textus/extensions/*.rb` (one per registration, by convention). Each Store instance gets its own registry; no global state.
|
|
157
|
+
|
|
158
|
+
See SPEC.md §5.11 for the full contract.
|
|
159
|
+
|
|
160
|
+
Schema fields may also declare `maintained_by:` and a top-level `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). SPEC §5.8.
|
|
161
|
+
|
|
162
|
+
## Examples
|
|
163
|
+
|
|
164
|
+
- [`examples/claude-plugin/`](examples/claude-plugin/) — full tour: fetcher, reducer, lifecycle events, schema ownership, and a `derived.claude.root` entry published to `CLAUDE.md`.
|
|
165
|
+
- [`examples/mcp-server/`](examples/mcp-server/) — 50-line MCP server wrapping `textus get/put` as tools.
|
|
166
|
+
|
|
167
|
+
## Tests
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
bundle exec rspec
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Runs the full suite, including conformance fixtures A–I from SPEC §12.
|
|
174
|
+
|
|
175
|
+
## Code quality
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
bundle exec rubocop # lint
|
|
179
|
+
bundle exec rubocop -A # lint + autocorrect
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Git hooks via [Lefthook](https://github.com/evilmartians/lefthook):
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
brew bundle install # installs lefthook (see Brewfile)
|
|
186
|
+
lefthook install # writes .git/hooks/{pre-commit,pre-push}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Git hooks (defined in `lefthook.yml`):
|
|
190
|
+
- `pre-commit` — runs `rubocop` on staged Ruby files.
|
|
191
|
+
- `pre-push` — runs the full `rspec` suite and `rubocop` over the tree.
|
|
192
|
+
|
|
193
|
+
Bypass with `LEFTHOOK=0 git commit ...` when needed.
|
|
194
|
+
|
|
195
|
+
CI runs `rspec` (Ruby 3.3 / 3.4) and `rubocop` via GitHub Actions
|
|
196
|
+
([`.github/workflows/ci.yml`](.github/workflows/ci.yml)).
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT.
|