Git
The sheet is the source and every value is derived, so a review shows the decision someone made — one changed number, not a re-exported binary. Diff it, recompute it, merge two people's edits, blame a formula, and gate it in CI.
Everything here runs against grades.tsvt from tsvsheet.examples:
curl -fsSO https://raw.githubusercontent.com/tsvsheet/tsvsheet.examples/main/grades.tsvt git init && git add grades.tsvt && git commit -m "grades: term one"
A change you can read
Dave’s reading score was transcribed wrong — 55 should have been 85. Fix that one cell and ask git what happened:
$ git diff -U1
diff --git a/grades.tsvt b/grades.tsvt
index 5104753caf83..675d9c649169 100644
--- a/grades.tsvt
+++ b/grades.tsvt
@@ -5,2 +5,2 @@ Bob 72 68 80 =round(avg(B3:D3), 1) =if(E3 >= 70, "Pass", "Fail")
Carol 95 88 91 =round(avg(B4:D4), 1) =if(E4 >= 70, "Pass", "Fail")
-Dave 60 55 70 =round(avg(B5:D5), 1) =if(E5 >= 70, "Pass", "Fail")
+Dave 60 85 70 =round(avg(B5:D5), 1) =if(E5 >= 70, "Pass", "Fail")One line, one field. The formula columns are byte-identical, because nothing about the calculation changed — only an input did. That is the entire review: a number was 55, and a person decided it should be 85.
The values are derived, never committed
A .tsvt stores =round(avg(B5:D5), 1). It never stores 71.7. So the diff above shows the decision, and the consequence is a separate question — one you can ask mechanically, by recomputing both sides of the commit and comparing:
$ diff <(git show HEAD:grades.tsvt | tsv render) <(tsv render grades.tsvt) 5c5 < Dave 60 55 70 61.7 Fail --- > Dave 60 85 70 71.7 Pass
Dave’s average moved and his result flipped from Fail to Pass. Nobody edited either of those cells; both fell out of the single change above. explain traces the last step:
$ tsv explain F5 grades.tsvt
F5 = Pass
formula: if(E5 >= 70,"Pass","Fail")
E5 = 71.7Those two diffs are the point of the format. The first is the change a reviewer approves; the second is its blast radius, computed rather than asserted. A spreadsheet that ships as a binary can give you neither — and one that commits its values hands you a diff full of recalculated cells with the actual edit buried somewhere inside it.
Two people, one sheet
Because rows are lines, ordinary text merging works. Two people edit different students on different branches:
git switch -c alice-retake # Alice's math retake: 85 → 91 git commit -am "grades: Alice math retake" git switch main # Carol's science regrade: 91 → 97 git commit -am "grades: Carol science regrade"
Then merge, with nothing to resolve:
$ git merge alice-retake
Auto-merging grades.tsvt
Merge made by the 'ort' strategy.
grades.tsvt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)Both edits are in, and the sheet recomputes around them:
$ tsv render grades.tsvt
Student Math Reading Science Average Result
Alice 91 90 78 86.3 Pass
Bob 72 68 80 73.3 Pass
Carol 95 88 97 93.3 Pass
Dave 60 85 70 71.7 PassTwo people changed the same spreadsheet at the same time and neither had to email the other a copy. Edits to the same row still conflict, as they should — and the conflict arrives as ordinary conflict markers you resolve in your editor, not as a choice between two whole files.
Who wrote this formula, and when
A formula is text on a line, so it has an author and a date like any other line of source:
$ git blame -L3,3 --date=short grades.tsvt
316f6ec4fabc (Ada 2026-08-08 3) Alice 91 90 78 =round(avg(B2:D2), 1) =if(E2 >= 70, "Pass", "Fail")“Why does this column round to one decimal?” has a commit behind it, and the commit has a message.
Gate it
check validates every formula and reference and reports through its exit status — 0 clean, 1 diagnostics, 2 a syntax error — so it drops straight into a hook or a CI job:
$ tsv check grades.tsvt $ echo $? 0
A diagnostic names the cell:
$ tsv check typo.tsvt E3: unknown function: avrage $ echo $? 1
Wire that into .githooks/pre-commit so a sheet that does not check never lands. check takes one sheet per invocation, so pass them through xargs -n1 rather than as a glob:
#!/bin/sh
# Refuse to commit a sheet that does not check.
sheets=$(git diff --cached --name-only --diff-filter=ACM | grep '\.tsvt$') || exit 0
[ -n "$sheets" ] || exit 0
echo "$sheets" | xargs -n1 tsv check$ chmod +x .githooks/pre-commit $ git config core.hooksPath .githooks $ git commit -am "grades: typo" E3: unknown function: avrage
The commit is refused and the working tree is untouched. The same line is the whole CI job — no spreadsheet application, no headless office suite, just a binary and a file.