Skip to content

Commit 0d106d9

Browse files
Merge pull request #250 from JarvusInnovations/develop
Release: v2.4.0
2 parents 6acd703 + 817afd2 commit 0d106d9

30 files changed

Lines changed: 2080 additions & 110 deletions

CLAUDE.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,45 @@ See [specs/architecture.md](specs/architecture.md) for the full stack rationale.
7373
- **Generated changes commit first.** When a command modifies files (`npm install`, codegen), commit those in a dedicated commit with the exact command in the body. Then make manual edits in a separate commit.
7474
- **Don't commit suspected secrets**`.env`, anything in `*.local.*`, credentials, private keys.
7575

76+
## Releases
77+
78+
Two npm release tracks ship from this repo on **separate, prefix-namespaced git-tag
79+
tracks** — keep them distinct, and mind the ordering rule:
80+
81+
- **`gitsheets`** (the JS package) — released on **`v*`** tags via the develop→main
82+
Release-PR flow (`release-prepare`/`-validate`/`-publish` workflows; use the
83+
**`release-flow`** skill). Merging the `Release: vX.Y.Z` PR cuts the tag;
84+
`publish-npm.yml` then builds, tests, and publishes from that tag.
85+
- **`@gitsheets/core-napi`** (+ its 6 platform packages) — released on
86+
**`core-napi-v*`** tags via `core-napi.yml`: native per-platform builds + npm
87+
trusted publishing (OIDC). Never tag the addon with a bare `v*` — that namespace
88+
belongs to the JS package.
89+
90+
Rules learned the hard way (v2.3.0's failed publish — see
91+
`plans/core-napi-0.2.0-release.md`):
92+
93+
1. **A `gitsheets-core` behavior change requires a core-napi release BEFORE the JS
94+
release.** `publish-npm` runs the workspace napi tests against the *published*
95+
platform binaries (resolved via the workspace manifest's `optionalDependencies`),
96+
so a JS release atop stale binaries fails mid-publish with test assertions
97+
mismatching old core behavior. If `rust/gitsheets-core` changed since the last
98+
`core-napi-v*` tag, ship core-napi first.
99+
2. **napi versions are tag-stamped, never pre-committed.** `core-napi.yml` runs
100+
`npm version $VERSION` from the tag name at publish time and errors ("Version not
101+
changed") if the manifest already carries that version. Tag `develop` with the
102+
**un-bumped** manifests; don't bump `rust/gitsheets-napi/package.json` (or
103+
`npm/*/package.json`) ahead of the tag.
104+
3. **Sync manifests AFTER the core-napi publish.** Once the new version is live, PR
105+
the sync: `rust/gitsheets-napi` version + platform `optionalDependencies` +
106+
`npm/*/package.json` to the released version, `packages/gitsheets` dep range,
107+
and a lockfile refresh. Without this, workspace tests keep resolving the previous
108+
platform binaries. (Committing the just-released version is safe — the stamp step
109+
only fails when the committed version equals the tag being stamped.)
110+
111+
Sequence for a batch that touches core: merge the feature PRs → tag `core-napi-v*`
112+
on develop → wait for the 7 packages to land on npm → PR the manifest/lockfile sync →
113+
merge → run the release-flow skill on the auto-opened `Release: v*` PR.
114+
76115
## Tooling
77116

78117
- **`gh-axi`** (or `gh`) for all GitHub operations.

README.md

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,134 @@
33
A git-backed document store for low-volume, high-touch, human-scale data.
44

55
Records are TOML (or markdown-with-frontmatter) files in a git repository, laid
6-
out by a per-sheet path template. Every mutation is a commit, so the log is the
7-
audit trail. The engine is a shared **Rust core** with thin per-language
8-
bindings: this repo ships the Node binding (`gitsheets` on npm) and a Python
9-
binding, both running on the same core, so a write from either language produces
10-
byte-identical commits.
6+
out by a per-sheet path template. Every write is validated against your schema,
7+
serialized to a canonical form (the same data always produces the same bytes),
8+
and lands as a commit. The log is your audit trail, the diff is your review
9+
tool, and a `git clone` is a complete copy of everything. The engine is a
10+
shared Rust core with thin per-language bindings: the `gitsheets` npm package
11+
and a Python binding both produce byte-identical commits.
1112

12-
The library and CLI live in [`packages/gitsheets`](packages/gitsheets); start
13-
there. The Rust core and its bindings live under [`rust/`](rust/).
13+
## The whole idea in one terminal
1414

15-
## Spec-driven
15+
```console
16+
$ cat .gitsheets/contacts.toml
17+
[gitsheet]
18+
root = 'contacts'
19+
path = '${{ handle }}'
20+
21+
[gitsheet.schema]
22+
type = 'object'
23+
required = ['handle', 'name']
24+
25+
$ npx -y gitsheets upsert contacts '{"handle": "ada", "name": "Ada Lovelace"}'
26+
27+
$ git show HEAD --stat --oneline
28+
f3a91c2 contacts upsert ada
29+
contacts/ada.toml | 2 ++
30+
```
31+
32+
A record is a file. A change is a commit. Everything git can do with commits
33+
(blame, revert, branch, push, diff, sign) now applies to your data.
34+
35+
## What people run on it
36+
37+
- **An application datastore.** [codeforphilly.org's rebuild](https://github.com/CodeForPhilly/codeforphilly-ng)
38+
uses gitsheets as its only public datastore: about 32,000 person records,
39+
where every mutating HTTP request becomes one commit authored as the acting
40+
user. The audit system is `git log`.
41+
- **Operational databanks.** Inventories and worklists that used to rot in
42+
spreadsheets, tracked as schema-validated records where every decision is a
43+
reviewable commit.
44+
- **AI evaluation corpora.** Agents and models write verdicts as records
45+
(keyed by subject and evaluator), one commit per pass, and nothing acts on a
46+
verdict until a human has read the diff. The schema rejects malformed model
47+
output at write time.
48+
- **Mirrors of external systems.** Extract an API into records, fix and merge
49+
data in git where diffs and review are free, then load only the deltas back.
50+
51+
## Getting started
52+
53+
### The library
54+
55+
```console
56+
npm install gitsheets # Node >= 20; prebuilt engine, no Rust toolchain
57+
```
58+
59+
```typescript
60+
import { openRepo } from 'gitsheets';
61+
62+
const repo = await openRepo({ gitDir: 'data/.git' });
63+
64+
await repo.transact(
65+
{ message: 'contacts upsert ada', author: { name: 'Ada', email: 'ada@example.org' } },
66+
async (tx) => {
67+
await tx.sheet('contacts').upsert({ handle: 'ada', name: 'Ada Lovelace' });
68+
},
69+
);
70+
```
71+
72+
One handler, one commit, committed only on success. `openStore` layers typed
73+
sheets on top with Standard Schema validators (Zod, Valibot, ArkType), and a
74+
successful transaction auto-refreshes reads. See
75+
[the docs](https://jarvusinnovations.github.io/gitsheets/) for the API guide.
76+
77+
### The CLI
1678

17-
[`specs/`](specs/) is the source of truth. Start at [`specs/README.md`](specs/README.md).
79+
Works in any repo with a `.gitsheets/` config, no install required:
80+
81+
```console
82+
npx -y gitsheets query contacts --filter name='Ada Lovelace'
83+
npx -y gitsheets read contacts ada
84+
npx -y gitsheets normalize contacts
85+
```
86+
87+
Installed globally it also mounts as a git subcommand: `git sheet query …`.
88+
89+
### For AI agents: gitsheets-axi
90+
91+
`gitsheets-axi` is the same store behind a CLI designed for agents: compact
92+
token-efficient output, schemas surfaced with every listing, and bulk
93+
operations (upsert, patch, delete) that land as single reviewable commits.
94+
95+
```console
96+
npx -y gitsheets-axi sheets # what's here, with schemas
97+
npx -y gitsheets-axi query contacts --group-by name
98+
jq -c '.[]' import.json | npx -y gitsheets-axi upsert contacts # one commit
99+
```
100+
101+
An agent working in a gitsheets repo can be given real write access: the
102+
schema gates every write and the history makes every action reversible.
103+
104+
### The agent skill
105+
106+
For Claude Code and compatible harnesses, an installable skill teaches the
107+
agent the config grammar, the API, and the axi tool, with session hooks that
108+
surface your sheets at startup:
109+
110+
```console
111+
npx skills add JarvusInnovations/gitsheets -y --skill gitsheets
112+
```
113+
114+
### Python
115+
116+
A Python binding ([`rust/gitsheets-py`](rust/gitsheets-py), pyo3) runs on the
117+
same core — a write from Python and a write from Node produce byte-identical
118+
commits, proven by cross-binding tests in CI. It builds from the repo today;
119+
PyPI publication is on the roadmap.
120+
121+
## What it is not
122+
123+
Every write is a commit, so sustained high write rates are the wrong fit. One
124+
process writes to a repo at a time. Queries are index-assisted scans with
125+
filters, grouping, and counts, not SQL joins. Large media belongs in object
126+
storage, not records. If a conventional database is serving you well, keep it.
127+
128+
## Spec-driven
18129

19-
- [`specs/architecture.md`](specs/architecture.md) — stack and packaging
20-
- [`specs/rust-core.md`](specs/rust-core.md) — the Rust core + thin-binding architecture
21-
- [`specs/concepts.md`](specs/concepts.md) — Repository, Sheet, Record, Transaction, Store, Index
22-
- [`specs/api/`](specs/api/) — per-symbol API contracts
23-
- [`specs/behaviors/`](specs/behaviors/) — cross-cutting rules
130+
[`specs/`](specs/) is the source of truth for behavior; start at
131+
[`specs/README.md`](specs/README.md). The library and CLI live in
132+
[`packages/gitsheets`](packages/gitsheets); the Rust core and bindings live
133+
under [`rust/`](rust/).
24134

25135
## License
26136

docs/path-templates.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ For the full spec see
1212
| --- | --- | --- |
1313
| Literal | `users/by-domain/` | Static path text |
1414
| Field reference | `${{ slug }}` | The value of `record.slug` |
15+
| Date bucket | `${{ publishedAt: YYYY/MM/DD }}` | UTC date partitions of the field — one directory level per format part |
1516
| Expression | `${{ slug.toLowerCase() }}` | Arbitrary JS expression with record fields in scope |
1617
| Recursive | `${{ path/** }}` | Field's value may contain `/` — for nested-path fields |
1718
| Prefix/suffix | `user-${{ id }}.draft` | Literal text attached to an expression |
@@ -26,13 +27,46 @@ path = "${{ slug }}"
2627
# Composite key (two-level directory)
2728
path = "${{ domain }}/${{ username }}"
2829

29-
# Sharded by date
30-
path = "${{ publishedAt.getFullYear() }}/${{ publishedAt.getMonth() }}/${{ slug }}"
30+
# Date-bucketed (2026/03/09/my-post.toml)
31+
path = "${{ publishedAt: YYYY/MM/DD }}/${{ slug }}"
3132

3233
# Nested path field
3334
path = "${{ contentPath/** }}"
3435
```
3536

37+
## Date buckets
38+
39+
A date-bucket reference partitions records by a date field — by day, month,
40+
year, or ISO week:
41+
42+
```toml
43+
path = "${{ publishedAt: YYYY/MM/DD }}/${{ slug }}" # 2026/03/09/my-post.toml
44+
path = "${{ publishedAt: YYYY/MM }}/${{ slug }}" # 2026/03/my-post.toml
45+
path = "${{ publishedAt: YYYY/WW }}/${{ slug }}" # 2026/11/my-post.toml (ISO week)
46+
path = "${{ day: YYYY/MM/DD }}" # daily rollup: the bucket IS the key
47+
```
48+
49+
The format is a **closed set**`YYYY`, `YYYY/MM`, `YYYY/MM/DD`, `YYYY/WW`;
50+
anything else throws `ConfigError(config_invalid)` when the sheet is opened.
51+
Fancier partitioning falls back to the expression form.
52+
53+
Semantics worth knowing:
54+
55+
- **UTC always.** Buckets never consult the host timezone, so the same record
56+
produces the same path on every machine. (The expression spelling
57+
`getFullYear()` doesn't have this guarantee — it renders host-local dates.)
58+
- **`YYYY/WW` is ISO-8601 week numbering**, and its year part is the ISO
59+
*week-based* year: 2027-01-01 belongs to ISO week 2026-W53 and renders as
60+
`2026/53`.
61+
- **`MM`, `DD`, `WW` are always two digits** — partitions sort correctly.
62+
- The field may hold a datetime/date value or an ISO 8601 string. One bucket
63+
token expands to multiple real directory levels, and must stand alone
64+
between slashes.
65+
- Queries that supply the bucketed field (e.g.
66+
`posts.queryAll({ publishedAt })`) descend the exact partition instead of
67+
scanning every record; without it, the partition levels are walked like any
68+
other unconstrained segment.
69+
3670
## How queries use the template
3771

3872
When a query specifies fields the template uses, gitsheets prunes the tree

package-lock.json

Lines changed: 26 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/gitsheets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"license": "Apache-2.0",
4242
"author": "Jarvus Innovations",
4343
"dependencies": {
44-
"@gitsheets/core-napi": "^0.2.0",
44+
"@gitsheets/core-napi": "^0.3.0",
4545
"csv-parse": "^6.2.1",
4646
"csv-stringify": "^6.7.0",
4747
"rfc6902": "^5.2.0",

0 commit comments

Comments
 (0)