A git-backed document store for low-volume, high-touch, human-scale data. Library + CLI.
This project uses spec-driven development. specs/ is the source of truth for what
should be true; plans/ is the work-in-flight DAG that bridges specs to merged code.
The specops skill carries the full methodology — invoke it (the skill triggers on
"spec", "plan", starting a feature, etc.) before writing specs, planning, or building.
- Specs lead. Before changing behavior, change the spec; bring code into conformance after. Spec↔code drift is a bug, not debt. If the spec doesn't cover what you're about to do, update the spec first.
plans/is the planning system — not your built-in plan mode. Every chunk of work lands as a file inplans/that freezes todoneas the durable record of what got built. Don't let an ephemeral plan substitute for it, and don't skip it for "small" changes. (Classic trap: an ad-hoc plan of "write spec X, then build it" that ends with neither a reviewed spec nor a plan file — split those into the two real artifacts.)- When to author a plan depends on intent: mapping out a batch of specs → finish the batch first, then propose a set of plans; speccing one bounded feature in a mature project → draft the spec change and its plan in tandem; intent unclear → ask. The skill details each mode.
- A spec change ripples to its plans. After editing a spec, review the plans that
implement it (
grep -l '<spec-path>' plans/*.md) and offer to update them.
Query the DAG: .claude/skills/specops/scripts/specops next (what to work on next) and
.claude/skills/specops/scripts/specops dag (graph). Run /audit-spec-drift to compare
specs against the implementation (before major work, after large refactors, and as part
of the release checklist).
Start at specs/README.md. The spec index:
- specs/architecture.md — stack, packaging, foundational decisions
- specs/concepts.md — vocabulary: Repository, Sheet, Record, Transaction, Store, Index
- specs/deferred.md — features intentionally out of scope for v1.0 (do NOT silently implement these)
- specs/api/ — per-symbol API contracts (Repository, Sheet, Transaction, Store, Errors, CLI)
- specs/behaviors/ — cross-cutting rules (path templates, validation, normalization, transactions, indexing, push sync, attachments, patch semantics)
Plans live in plans/ — see plans/README.md.
The active work scope is the 1.0.0 milestone — see issues #128 through #141 (excluding the few backlog ones). The Rust-core migration is done: the engine lives in the Rust gitsheets-core crate (rust/gitsheets-core), the published gitsheets npm package is a thin marshalling shell over it (via the @gitsheets/core-napi addon), and a parallel Python binding (rust/gitsheets-py, pyo3) runs over the same core, byte-identical. Tree/blob/commit ops now run on holo-tree (gitoxide) inside the core — the direct @hologit/holo-tree JS dependency was dropped at the cutover (#216). The architecture is specced in specs/rust-core.md and was built out via the (now-done) plans/ DAG.
- Engine — Rust
gitsheets-corecrate (rust/gitsheets-core), the bytes-authority: TOML parse+serialize, canonical normalization/key-sort, path templates, JSON-Schema validation (strict), the embedded boa JS engine, native ICU collation, nativedprint-plugin-markdownnormalization, record CRUD, query/index, diff/patch, attachments, and theSheet/Transaction/Storestate machine. Tree/blob/commit viaholo-tree(gitoxide) inside the core. - Bindings — Node (
gitsheetsnpm package, a thin marshalling shell over the@gitsheets/core-napiaddon) and Python (rust/gitsheets-py, pyo3), byte-identical over the same core. - Language — TypeScript (strict). ESM-only.
- Runtime — Node.js ≥ 20 or Bun ≥ 1.
- Retained JS deps —
sort-keys(publicSheet.normalizeRecordarray-field sort),node:vm(exportedTemplaterender + raw-JS sort comparators),rfc6902(publicDiffChange.patchtype + markdowndiffFrom),yargs,csv-*. Dropped at the cutover:@iarna/toml,smol-toml,ajv,ajv-formats,markdownlint,@hologit/holo-tree. - Runtime consumer-validator interface — Standard Schema (any compliant validator: Zod, Valibot, ArkType, Effect Schema), run host-side in the binding.
- JSON Merge Patch — RFC 7396, inline in
packages/gitsheets/src/patch.ts. - Tests — Vitest.
See specs/architecture.md for the full stack rationale.
- TypeScript everywhere.
strict: true. No.jsinpackages/gitsheets/src/. - Field naming:
camelCasein code, in TOML records, and onSheet.<field>config — no casing translation. - IDs: consumer's choice (gitsheets doesn't impose UUID/string/numeric). Path templates render whatever the field holds.
- Timestamps: TOML datetime types are preserved by the Rust core across parse and serialize; the Node binding surfaces them as
instanceof Date, and consumers can also use ISO 8601 strings. - Use the typed error classes from specs/api/errors.md — never throw plain
Errorfrom public surfaces. - Mutations go through
repo.transactor via the permissive Sheet methods documented in specs/behaviors/transactions.md.
- Conventional commits —
type(scope): description(e.g.,feat(sheet): add patch method,fix(path-template): handle multi-variable segments,docs(specs): clarify trailer conventions). - Logical sets per commit — group related changes together; commit often. When multiple uncommitted change-sets exist, commit them separately in a logical order.
- Always
git statusbefore staging. Stage specific files or directories — nevergit add -Aorgit add .. - 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. - Don't commit suspected secrets —
.env, anything in*.local.*, credentials, private keys.
Three release tracks (two npm, one PyPI) ship from this repo on separate,
prefix-namespaced git-tag tracks — keep them distinct, and mind the ordering rule.
specs/behaviors/distribution.md is the source of truth for the tracks and their
deliberately-different version-source rules:
gitsheets(the JS package) — released onv*tags via the develop→main Release-PR flow (release-prepare/-validate/-publishworkflows; use therelease-flowskill). Merging theRelease: vX.Y.ZPR cuts the tag;publish-npm.ymlthen builds, tests, and publishes from that tag.@gitsheets/core-napi(+ its 6 platform packages) — released oncore-napi-v*tags viacore-napi.yml: native per-platform builds + npm trusted publishing (OIDC). Never tag the addon with a barev*— that namespace belongs to the JS package.gitsheetson PyPI (the Python binding) — released onpy-v*tags viapython-publish.yml: abi3 wheels + sdist, PyPI trusted publishing (OIDC). The version is COMMITTED inrust/gitsheets-py/Cargo.toml(maturin reads it) and the tag must match it exactly — the workflow's guard job fails the run on mismatch. This deliberately inverts the napi track's tag-stamped rule (seespecs/behaviors/distribution.md): bump the committed version on develop first, then tagpy-v<that-version>from a commit whererust-core.ymlis green (the cross-binding byte-parity gate). Never tag the binding with a barev*.
Rules learned the hard way (v2.3.0's failed publish — see
plans/core-napi-0.2.0-release.md):
- A
gitsheets-corebehavior change requires a core-napi release BEFORE the JS release.publish-npmruns the workspace napi tests against the published platform binaries (resolved via the workspace manifest'soptionalDependencies), so a JS release atop stale binaries fails mid-publish with test assertions mismatching old core behavior. Ifrust/gitsheets-corechanged since the lastcore-napi-v*tag, ship core-napi first. - napi versions are tag-stamped, never pre-committed.
core-napi.ymlrunsnpm version $VERSIONfrom the tag name at publish time and errors ("Version not changed") if the manifest already carries that version. Tagdevelopwith the un-bumped manifests; don't bumprust/gitsheets-napi/package.json(ornpm/*/package.json) ahead of the tag. - Sync manifests AFTER the core-napi publish. Once the new version is live, PR
the sync:
rust/gitsheets-napiversion + platformoptionalDependencies+npm/*/package.jsonto the released version,packages/gitsheetsdep range, and a lockfile refresh. Without this, workspace tests keep resolving the previous platform binaries. (Committing the just-released version is safe — the stamp step only fails when the committed version equals the tag being stamped.)
Sequence for a batch that touches core: merge the feature PRs → tag core-napi-v*
on develop → wait for the 7 packages to land on npm → PR the manifest/lockfile sync →
merge → run the release-flow skill on the auto-opened Release: v* PR. The py-v*
track is independent of this npm sequencing (it builds the core from source at the
tagged commit) but shares the parity gate: only tag rust-core-green commits.
gh-axi(orgh) for all GitHub operations.- GitHub Actions — when authoring or modifying a workflow, run
gh-axi repo view <owner>/<repo>on each action's repo to confirm the latest recommended version. jqfor processing JSON in any shell pipeline. No inline Python/Node to filter JSON.npmfor packages. Never hand-editpackage.jsonorpackage-lock.json— usenpm install <pkg>,npm uninstall <pkg>, etc. Commitpackage-lock.json.asdffor tool versions when available. Never edit.tool-versionsdirectly.
The repo is an npm workspaces monorepo — the published package lives at packages/gitsheets/. Run scripts from the repo root; root proxies (npm test, npm run build, npm run type-check) forward to the gitsheets workspace via -w gitsheets.
npm install
npm run build # tsc → packages/gitsheets/dist/
npm test # vitest
npm run type-checkSingle-workspace operations also work directly: npm install -w gitsheets <pkg>, npm test -w gitsheets.
CLI usage:
git sheet upsert <sheet> [file]
git sheet query <sheet> [--filter.<field>=<value>]
git sheet read <sheet> <path>
git sheet edit <sheet> <path>
git sheet normalize <sheet>
git sheet infer <sheet>
git sheet migrate-config <sheet>See specs/api/cli.md.
Pick the spec that mentions what you're working on. If multiple specs apply (e.g., Sheet.patch involves api/sheet.md, behaviors/patch-semantics.md, behaviors/validation.md), read each. If you can't find a spec, the answer is to write one — not to make up behavior.