tidemark 0.1.0__tar.gz

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.
@@ -0,0 +1,67 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ lint:
15
+ name: Lint
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: dtolnay/rust-toolchain@stable
21
+ with:
22
+ components: rustfmt, clippy
23
+
24
+ - name: Run linting
25
+ run: make lint
26
+
27
+ test:
28
+ name: Test
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - uses: dtolnay/rust-toolchain@stable
34
+
35
+ - uses: taiki-e/install-action@nextest
36
+
37
+ - name: Run tests
38
+ run: make test
39
+
40
+ coverage:
41
+ name: Coverage
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: dtolnay/rust-toolchain@stable
46
+ - uses: taiki-e/install-action@cargo-tarpaulin
47
+ - name: Generate coverage
48
+ run: cargo tarpaulin --out xml
49
+ - name: Upload to Codecov
50
+ uses: codecov/codecov-action@v5
51
+ with:
52
+ files: cobertura.xml
53
+ token: ${{ secrets.CODECOV_TOKEN }}
54
+
55
+ all-checks-passed:
56
+ name: All checks passed
57
+ runs-on: ubuntu-latest
58
+ needs: [lint, test]
59
+ if: always()
60
+ steps:
61
+ - name: Verify all checks passed
62
+ run: |
63
+ if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]; then
64
+ echo "Some checks failed or were cancelled"
65
+ exit 1
66
+ fi
67
+ echo "All checks passed"
@@ -0,0 +1,393 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]*.[0-9]*.[0-9]*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ dry_run:
10
+ description: 'Dry run mode (skip actual publishing)'
11
+ required: false
12
+ default: true
13
+ type: boolean
14
+ skip_crates_io:
15
+ description: 'Skip publishing to crates.io (if already published)'
16
+ required: false
17
+ default: false
18
+ type: boolean
19
+ skip_pypi:
20
+ description: 'Skip publishing to PyPI (if already published)'
21
+ required: false
22
+ default: false
23
+ type: boolean
24
+
25
+ permissions:
26
+ contents: write
27
+
28
+ env:
29
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30
+
31
+ jobs:
32
+ test:
33
+ name: Run tests
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+
38
+ - uses: dtolnay/rust-toolchain@stable
39
+
40
+ - uses: taiki-e/install-action@nextest
41
+
42
+ - name: Verify Cargo.lock is up to date
43
+ run: cargo check --locked
44
+
45
+ - name: Run tests
46
+ run: make test
47
+
48
+ build:
49
+ name: Build ${{ matrix.target }}
50
+ needs: test
51
+ timeout-minutes: 30
52
+ # Guard: self-hosted runners only execute on trusted events (tag push,
53
+ # workflow_dispatch). Fork PRs must never dispatch jobs to self-hosted.
54
+ if: github.event_name != 'pull_request'
55
+ strategy:
56
+ matrix:
57
+ include:
58
+ - os: ubuntu-latest
59
+ target: x86_64-unknown-linux-gnu
60
+ - os: [self-hosted, Linux, ARM64]
61
+ target: aarch64-unknown-linux-gnu
62
+ - os: windows-latest
63
+ target: x86_64-pc-windows-msvc
64
+ - os: [self-hosted, macOS, ARM64]
65
+ target: x86_64-apple-darwin
66
+ - os: [self-hosted, macOS, ARM64]
67
+ target: aarch64-apple-darwin
68
+ runs-on: ${{ matrix.os }}
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ # Preserve target/ between runs on self-hosted runners for cargo
73
+ # incremental rebuild. GitHub-hosted runners start clean each job
74
+ # regardless of this setting, so this only affects self-hosted.
75
+ clean: false
76
+
77
+ - uses: dtolnay/rust-toolchain@stable
78
+ with:
79
+ targets: ${{ matrix.target }}
80
+
81
+ - name: Prepare cargo cache dirs (arm64 linux)
82
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
83
+ run: mkdir -p "${RUNNER_TOOL_CACHE}/cargo-cache/registry" "${RUNNER_TOOL_CACHE}/cargo-cache/git"
84
+
85
+ - name: Clean stale dist
86
+ # Keep dist/ fresh each run so previous builds cannot leak artifacts
87
+ # into the upload. Self-hosted runners do not clean automatically.
88
+ # On the arm64 linux runner, maturin-action runs as root inside the
89
+ # manylinux container and leaves root-owned files in dist/; clean
90
+ # via docker so the unprivileged runner user can proceed.
91
+ shell: bash
92
+ run: |
93
+ if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
94
+ docker run --rm -v "${GITHUB_WORKSPACE}:/io" alpine rm -rf /io/dist /io/target/wheels
95
+ else
96
+ rm -rf dist target/wheels
97
+ fi
98
+
99
+ - name: Install cross-compilation tools
100
+ # Only needed on GitHub-hosted linux where the arm64 toolchain must
101
+ # be cross-installed. The self-hosted arm64 runner is native.
102
+ if: matrix.target == 'aarch64-unknown-linux-gnu' && runner.arch != 'ARM64'
103
+ run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
104
+
105
+ - name: Install uv
106
+ if: matrix.target != 'aarch64-unknown-linux-gnu'
107
+ uses: astral-sh/setup-uv@v6
108
+
109
+ - name: Install maturin and zig
110
+ # maturin-action provides its own maturin inside the manylinux
111
+ # container for the arm64 linux build, so skip the host install
112
+ # on the self-hosted runner.
113
+ if: matrix.target != 'aarch64-unknown-linux-gnu'
114
+ shell: bash
115
+ run: |
116
+ uv venv "${RUNNER_TEMP}/build-venv"
117
+ VENV_BIN="${RUNNER_TEMP}/build-venv/bin"
118
+ [ -d "$VENV_BIN" ] || VENV_BIN="${RUNNER_TEMP}/build-venv/Scripts"
119
+ uv pip install --python "$VENV_BIN/python" maturin ziglang
120
+ echo "$VENV_BIN" >> "$GITHUB_PATH"
121
+
122
+ - name: Build wheel (arm64 linux via maturin-action)
123
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
124
+ uses: PyO3/maturin-action@v1
125
+ with:
126
+ target: aarch64-unknown-linux-gnu
127
+ args: --release --out dist
128
+ manylinux: manylinux_2_28
129
+ # Mount persistent cargo cache into the manylinux container so
130
+ # dependency downloads survive between jobs on the self-hosted
131
+ # runner.
132
+ docker-options: -v ${{ runner.tool_cache }}/cargo-cache/registry:/root/.cargo/registry -v ${{ runner.tool_cache }}/cargo-cache/git:/root/.cargo/git
133
+
134
+ - name: Build wheel
135
+ if: matrix.target != 'aarch64-unknown-linux-gnu'
136
+ shell: bash
137
+ run: |
138
+ case "${{ matrix.target }}" in
139
+ *-gnu)
140
+ maturin build --release --target ${{ matrix.target }} --compatibility manylinux_2_28 --zig --out dist
141
+ ;;
142
+ *)
143
+ maturin build --release --target ${{ matrix.target }} --out dist
144
+ ;;
145
+ esac
146
+
147
+ - name: Build binary (arm64 linux via cargo-zigbuild)
148
+ # On the Pi runner build natively with cargo-zigbuild targeting
149
+ # glibc 2.28 for wide distro compatibility. Use a separate target
150
+ # directory so host cargo does not conflict with root-owned files
151
+ # left behind by the maturin-action container build in target/.
152
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
153
+ env:
154
+ CARGO_TARGET_DIR: target-host
155
+ run: cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28
156
+
157
+ - name: Build binary
158
+ if: matrix.target != 'aarch64-unknown-linux-gnu'
159
+ env:
160
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
161
+ run: cargo build --release --target ${{ matrix.target }}
162
+
163
+ - name: Verify binary
164
+ if: ${{ !contains(matrix.target, 'aarch64-unknown-linux') }}
165
+ shell: bash
166
+ run: |
167
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
168
+ ./target/${{ matrix.target }}/release/tidemark.exe --version
169
+ else
170
+ ./target/${{ matrix.target }}/release/tidemark --version
171
+ fi
172
+
173
+ - name: Create release archive
174
+ shell: bash
175
+ run: |
176
+ VERSION=${GITHUB_REF#refs/tags/}
177
+ ARCHIVE_NAME="tidemark-${VERSION}-${{ matrix.target }}"
178
+
179
+ mkdir -p release-package
180
+
181
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
182
+ cp "target/${{ matrix.target }}/release/tidemark.exe" release-package/tidemark.exe
183
+ cd release-package
184
+ powershell -command "Compress-Archive -Path tidemark.exe -DestinationPath ../${ARCHIVE_NAME}.zip"
185
+ cd ..
186
+ powershell -command "Get-FileHash -Path '${ARCHIVE_NAME}.zip' -Algorithm SHA256 | Select-Object -ExpandProperty Hash" > "${ARCHIVE_NAME}.zip.sha256"
187
+ else
188
+ if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
189
+ BIN_PATH="target-host/aarch64-unknown-linux-gnu/release/tidemark"
190
+ else
191
+ BIN_PATH="target/${{ matrix.target }}/release/tidemark"
192
+ fi
193
+ cp "$BIN_PATH" release-package/tidemark
194
+ tar -czf "${ARCHIVE_NAME}.tar.gz" -C release-package tidemark
195
+
196
+ if [[ "${{ runner.os }}" == "macOS" ]]; then
197
+ shasum -a 256 "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
198
+ else
199
+ sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
200
+ fi
201
+ fi
202
+
203
+ rm -rf release-package
204
+
205
+ - name: Upload wheel
206
+ uses: actions/upload-artifact@v4
207
+ with:
208
+ path: dist/*.whl
209
+ name: wheel-${{ matrix.target }}
210
+
211
+ - name: Upload release archives
212
+ uses: actions/upload-artifact@v4
213
+ with:
214
+ path: |
215
+ tidemark-*-${{ matrix.target }}.tar.gz*
216
+ tidemark-*-${{ matrix.target }}.zip*
217
+ name: release-${{ matrix.target }}
218
+
219
+ sdist:
220
+ name: Build source distribution
221
+ runs-on: ubuntu-latest
222
+ needs: test
223
+ steps:
224
+ - uses: actions/checkout@v4
225
+
226
+ - uses: astral-sh/setup-uv@v6
227
+
228
+ - name: Install maturin
229
+ shell: bash
230
+ run: |
231
+ uv venv "${RUNNER_TEMP}/build-venv"
232
+ uv pip install --python "${RUNNER_TEMP}/build-venv/bin/python" maturin
233
+ echo "${RUNNER_TEMP}/build-venv/bin" >> "$GITHUB_PATH"
234
+
235
+ - name: Build sdist
236
+ run: maturin sdist
237
+
238
+ - uses: actions/upload-artifact@v4
239
+ with:
240
+ path: target/wheels/*.tar.gz
241
+ name: sdist
242
+
243
+ release:
244
+ runs-on: ubuntu-latest
245
+ needs: [build, sdist]
246
+ steps:
247
+ - uses: actions/checkout@v4
248
+
249
+ - uses: dtolnay/rust-toolchain@stable
250
+
251
+ - name: Install uv
252
+ uses: astral-sh/setup-uv@v5
253
+ with:
254
+ enable-cache: false
255
+
256
+ - name: Publish to crates.io
257
+ if: ${{ inputs.dry_run != true && inputs.skip_crates_io != true }}
258
+ env:
259
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
260
+ run: cargo publish --locked
261
+
262
+ - name: Test crates.io publish (dry run)
263
+ if: ${{ inputs.dry_run == true && inputs.skip_crates_io != true }}
264
+ run: |
265
+ echo "DRY RUN: Would publish to crates.io"
266
+ cargo publish --dry-run --locked
267
+
268
+ - name: Skip crates.io publishing
269
+ if: ${{ inputs.skip_crates_io == true }}
270
+ run: echo "Skipping crates.io publishing as requested"
271
+
272
+ - name: Download all artifacts
273
+ uses: actions/download-artifact@v4
274
+ with:
275
+ path: artifacts
276
+
277
+ - name: Publish to PyPI
278
+ if: ${{ inputs.dry_run != true && inputs.skip_pypi != true }}
279
+ env:
280
+ TWINE_USERNAME: __token__
281
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
282
+ run: |
283
+ uv tool run twine upload artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz
284
+
285
+ - name: Test PyPI upload (dry run)
286
+ if: ${{ inputs.dry_run == true && inputs.skip_pypi != true }}
287
+ run: |
288
+ echo "DRY RUN: Would upload to PyPI:"
289
+ find artifacts/wheel-* -name "*.whl" -type f | sort
290
+ find artifacts/sdist -name "*.tar.gz" -type f | sort
291
+ uv tool run twine check artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz
292
+
293
+ - name: Skip PyPI publishing
294
+ if: ${{ inputs.skip_pypi == true }}
295
+ run: echo "Skipping PyPI publishing as requested"
296
+
297
+ - name: Create Release
298
+ if: ${{ inputs.dry_run != true }}
299
+ uses: softprops/action-gh-release@v2
300
+ with:
301
+ generate_release_notes: true
302
+ files: |
303
+ artifacts/release-*/tidemark-*.tar.gz
304
+ artifacts/release-*/tidemark-*.tar.gz.sha256
305
+ artifacts/release-*/tidemark-*.zip
306
+ artifacts/release-*/tidemark-*.zip.sha256
307
+
308
+ - name: Dry Run Summary
309
+ if: ${{ inputs.dry_run == true }}
310
+ run: |
311
+ echo "Dry run complete. Artifacts built but nothing published."
312
+ echo "Archives:"
313
+ find artifacts/release-* -type f | sort
314
+
315
+ update-homebrew:
316
+ needs: release
317
+ if: ${{ !inputs.dry_run }}
318
+ runs-on: ubuntu-latest
319
+ steps:
320
+ - uses: actions/download-artifact@v4
321
+ with:
322
+ path: /tmp/artifacts
323
+
324
+ - name: Compute SHA256 hashes
325
+ id: hashes
326
+ run: |
327
+ for target in x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
328
+ sha=$(cat /tmp/artifacts/release-${target}/tidemark-${{ github.ref_name }}-${target}.tar.gz.sha256 | awk '{print $1}')
329
+ key=$(echo "${target}" | tr '-' '_')
330
+ echo "${key}=${sha}" >> "$GITHUB_OUTPUT"
331
+ done
332
+
333
+ - name: Update Homebrew formula
334
+ env:
335
+ GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
336
+ run: |
337
+ VERSION="${{ github.ref_name }}"
338
+ VERSION_NUM="${VERSION#v}"
339
+
340
+ cat > /tmp/tidemark.rb << 'FORMULA'
341
+ class Tidemark < Formula
342
+ desc "Snapshot a directory tree and diff what changed - no git required"
343
+ homepage "https://github.com/rvben/tidemark"
344
+ version "VERSION_NUM"
345
+ license "MIT"
346
+
347
+ on_macos do
348
+ if Hardware::CPU.arm?
349
+ url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-aarch64-apple-darwin.tar.gz"
350
+ sha256 "SHA_AARCH64_APPLE_DARWIN"
351
+ else
352
+ url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-x86_64-apple-darwin.tar.gz"
353
+ sha256 "SHA_X86_64_APPLE_DARWIN"
354
+ end
355
+ end
356
+
357
+ on_linux do
358
+ if Hardware::CPU.arm?
359
+ url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-aarch64-unknown-linux-gnu.tar.gz"
360
+ sha256 "SHA_AARCH64_UNKNOWN_LINUX_GNU"
361
+ else
362
+ url "https://github.com/rvben/tidemark/releases/download/VERSION/tidemark-VERSION-x86_64-unknown-linux-gnu.tar.gz"
363
+ sha256 "SHA_X86_64_UNKNOWN_LINUX_GNU"
364
+ end
365
+ end
366
+
367
+ def install
368
+ bin.install "tidemark"
369
+ end
370
+
371
+ test do
372
+ system "#{bin}/tidemark", "--version"
373
+ end
374
+ end
375
+ FORMULA
376
+
377
+ sed -i "s/VERSION_NUM/${VERSION_NUM}/g" /tmp/tidemark.rb
378
+ sed -i "s/VERSION/${VERSION}/g" /tmp/tidemark.rb
379
+ sed -i "s/SHA_AARCH64_APPLE_DARWIN/${{ steps.hashes.outputs.aarch64_apple_darwin }}/g" /tmp/tidemark.rb
380
+ sed -i "s/SHA_X86_64_APPLE_DARWIN/${{ steps.hashes.outputs.x86_64_apple_darwin }}/g" /tmp/tidemark.rb
381
+ sed -i "s/SHA_AARCH64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.aarch64_unknown_linux_gnu }}/g" /tmp/tidemark.rb
382
+ sed -i "s/SHA_X86_64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.x86_64_unknown_linux_gnu }}/g" /tmp/tidemark.rb
383
+
384
+ # Clone tap repo, update formula, push
385
+ git clone https://x-access-token:${GH_TOKEN}@github.com/rvben/homebrew-tap.git /tmp/tap
386
+ mkdir -p /tmp/tap/Formula
387
+ cp /tmp/tidemark.rb /tmp/tap/Formula/tidemark.rb
388
+ cd /tmp/tap
389
+ git config user.name "github-actions[bot]"
390
+ git config user.email "github-actions[bot]@users.noreply.github.com"
391
+ git add Formula/tidemark.rb
392
+ git diff --cached --quiet || git commit -m "Update tidemark to ${VERSION}"
393
+ git push
@@ -0,0 +1,6 @@
1
+ /target
2
+ /CLAUDE.local.md
3
+ /dist
4
+ /target-host
5
+ *.whl
6
+ /docs/superpowers/
@@ -0,0 +1,94 @@
1
+ # tidemark for agents
2
+
3
+ `tidemark` is a deterministic filesystem snapshot/diff tool designed to be driven
4
+ by AI agents. This document is the operational contract.
5
+
6
+ ## Discover capabilities
7
+
8
+ Run `tidemark schema` first. It returns a clispec v0.1 document listing every
9
+ command, its arguments (name, type, required, default), output fields, error
10
+ kinds (each with a `retryable` flag), exit-code semantics, and which commands
11
+ mutate state (`mutating: true`). The schema is the source of truth; prefer it
12
+ over parsing help text.
13
+
14
+ ## Output contract
15
+
16
+ - Output is **JSON by default when stdout is not a TTY**. You never need a flag,
17
+ but you may pass `--json` (or `--output json`) to be explicit.
18
+ - **stdout carries data only.** All diagnostics and the human summary line go to
19
+ stderr. Piping stdout to a JSON parser is always safe.
20
+ - `diff` returns a flat object:
21
+
22
+ ```json
23
+ {
24
+ "changes": [ { "kind": "modified", "path": "src/a.rs",
25
+ "old_hash": "blake3:...", "new_hash": "blake3:...",
26
+ "size_delta": 12 } ],
27
+ "added": 0, "modified": 1, "deleted": 0, "renamed": 0,
28
+ "total": 1, "limit": null, "offset": 0
29
+ }
30
+ ```
31
+
32
+ - `list` uses the envelope `{"items": [...], "total": N, "limit": L, "offset": O}`.
33
+ - Bound large results with `--limit` / `--offset`, and trim fields with
34
+ `--fields path,kind`. These flags are global (accepted on any command).
35
+ - Running `tidemark` with no command lists stored snapshots.
36
+
37
+ ## Recommended pattern
38
+
39
+ ```
40
+ tidemark snap -o pre.tidemark # portable manifest (or: tidemark snap before)
41
+ <run the operation under test>
42
+ tidemark diff pre.tidemark @ --json # @ means "the current tree"
43
+ ```
44
+
45
+ A ref (the `A`/`B` of `diff`) is one of: a store label, a path to a manifest
46
+ file, or `@` for the live tree. `tidemark diff before` is shorthand for
47
+ `tidemark diff before @`. A bare ref resolves to a store label first, falling
48
+ back to a same-named file only if no such label exists. `tidemark init` creates
49
+ the `.tidemark/` store explicitly (idempotent).
50
+
51
+ ## Determinism and idempotency
52
+
53
+ - `tree_digest` is a BLAKE3 Merkle root over the sorted entries. Two snapshots of
54
+ an unchanged tree are byte-for-byte identical in digest.
55
+ - `mtime` never affects change detection, so rebuilds and `touch` do not produce
56
+ spurious diffs.
57
+ - `tidemark snap LABEL` on an unchanged tree is a success no-op (exit 0).
58
+ Re-using a label for a *different* tree returns the `conflict` error kind
59
+ (exit 2) unless you pass `--force`.
60
+ - A file that vanishes between the directory walk and its read is skipped, so a
61
+ concurrent deletion mid-snapshot does not abort the whole snapshot.
62
+
63
+ ## Exit codes
64
+
65
+ - Default: `0` success, `2` error.
66
+ - `diff --exit-code`: `0` no changes, `1` changes found, `2` error. Use this when
67
+ you only need a yes/no "did anything change" signal.
68
+
69
+ ## Errors
70
+
71
+ Always to stderr, shape:
72
+
73
+ ```json
74
+ {"error": {"kind": "not_found", "message": "...", "retryable": false}}
75
+ ```
76
+
77
+ Kinds: `not_found`, `conflict`, `invalid_input`, `unsupported`, and `io`
78
+ (the only `retryable: true` kind - safe to retry transient filesystem errors).
79
+ Argument-parse failures are also reported as `invalid_input` JSON on stderr.
80
+
81
+ ## Content diffs
82
+
83
+ `diff --content` emits a real unified line diff in each modified change's
84
+ `content_preview` field, reconstructed from text content stored in both
85
+ manifests. Because every snapshot stores inline text content (UTF-8 files under
86
+ 256 KiB), this works even between two stored manifests, not just against the live
87
+ tree. Binary, oversized, or `--no-content` files report that the content was
88
+ unavailable instead.
89
+
90
+ ## Safety
91
+
92
+ - Destructive `rm` refuses to run without `--yes` when stdin is not a TTY.
93
+ - tidemark never records its own `.tidemark/` store directory in a snapshot.
94
+ - Labels are validated against path traversal and control characters.
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
@@ -0,0 +1,73 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build & Test Commands
6
+
7
+ ```bash
8
+ make check # lint + test (CI runs this)
9
+ make build # cargo build --release
10
+ make test # cargo nextest run + cargo test --doc
11
+ make lint # cargo fmt --check + cargo clippy -D warnings
12
+ make fmt # auto-format
13
+ make score # build release + clispec score ./target/release/tidemark
14
+ make ci # check + score
15
+ make install # cargo install --path .
16
+ cargo nextest run <name> # run a single test by name
17
+ ```
18
+
19
+ ## Architecture
20
+
21
+ tidemark is a Rust CLI that snapshots a directory tree into a deterministic
22
+ BLAKE3 manifest and diffs two snapshots (added/modified/deleted/renamed) with no
23
+ git required. Binary name: `tidemark`. Also published to PyPI (`uvx tidemark`)
24
+ and Homebrew (`brew install rvben/tap/tidemark`). Scores 100/100 against
25
+ [The CLI Spec](https://clispec.dev).
26
+
27
+ ### Pure-core + thin-shell layout
28
+
29
+ - **Pure (no I/O, unit-tested directly):** `manifest` (`Manifest`/`Entry`, Merkle
30
+ `tree_digest`), `diff` (classification, rename detection, `unified_diff`).
31
+ - **I/O wrappers:** `walk` (directory traversal via the `ignore` crate,
32
+ `require_git(false)`), `hash` (`hash_bytes`), `builder` (`build_manifest` +
33
+ `SnapOptions`), `store` (`.tidemark/` labeled snapshot store), `refs` (resolve a
34
+ ref: store label | manifest file | `@` current tree).
35
+ - **Shell:** `output` (JSON/table, pagination, field selection), `schema` (the
36
+ clispec v0.1 document), `cli` (clap tree, dispatch, exit codes), `error`
37
+ (`TidemarkError` + clispec error kinds).
38
+
39
+ ### Key patterns
40
+
41
+ - **clispec scorer probes the first non-mutating command** for structured-output
42
+ and stream checks, so `list` MUST stay ordered before `diff` in `schema.rs`
43
+ (`tidemark diff` errors with no snapshot; `list` always exits 0 with an
44
+ envelope). Reordering drops the score.
45
+ - **Schema matches `clispec.dev/schema/v0.1.json`:** top-level
46
+ `name`/`version`/`commands`/`errors`; per-command `mutating` boolean +
47
+ `output_fields`; `errors` is a top-level array of `{kind, retryable}`.
48
+ - **`mtime` is informational only** - excluded from `tree_digest` and change
49
+ detection, so `touch`/rebuilds never create false positives.
50
+ - **Rename detection only on unambiguous `(hash, mode, kind)` signatures**
51
+ (exactly one deleted + one added share it); duplicate-content files stay as
52
+ add/delete.
53
+ - **Inline content** stored for UTF-8 files <= 256 KiB (`CONTENT_CAP_BYTES`)
54
+ enables real two-sided `diff --content`, even between two stored manifests.
55
+ - **stdout/stderr flushed before `process::exit`** in `main.rs` (exit skips
56
+ destructors, so a buffered pipe could otherwise truncate).
57
+ - Destructive `rm` requires `--yes` when stdin is not a TTY. tidemark never
58
+ records its own `.tidemark/` store in a snapshot. Labels are validated against
59
+ path traversal and control characters.
60
+
61
+ ### CI / release
62
+
63
+ All CI steps are make targets; the pipeline only runs make. `ci.yml` runs
64
+ lint/test/coverage on push and PR. `release.yml` triggers on a `v*` tag (or
65
+ manual dispatch, dry-run by default) and publishes to crates.io + PyPI + a
66
+ GitHub release, then updates the Homebrew tap formula. Release is driven by
67
+ `make release-patch|minor|major` (vership).
68
+
69
+ ## Documentation
70
+
71
+ - `README.md` - human-facing usage.
72
+ - `AGENTS.md` - the agent-facing operational contract.
73
+ - `docs/superpowers/` - design spec and implementation plan (gitignored).