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 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.