A CLI that polls GitHub star counts and reports per-run deltas, with the
diff arithmetic and its sign-classified decomposition verified in Dafny
via LemmaScript. The
TypeScript in src/domain.ts is the source of truth; the .dfy files
are a verification side-car.
Layered from primitive to high-level, all in src/domain.ts:
Pure recursive helpers (emit as Dafny functions):
sumDiffs(rows)— tail-recursive sum overDiffRow[].sumDiffsUpTo(rows, n)— prefix-indexed sum, bridges to theupTopartition family.sumPositiveUpTo(rows, n)/sumNegativeUpTo(rows, n)— signed partial sums.countPositiveUpTo / countNegativeUpTo / countZeroUpTo— signed partial counts.
Core method computeDiff(repos, oldCounts, newCounts) — builds the per-repo
diff report. A single //@ assert repo in newCounts bridges from the universal
requires to the concrete narrowing at the access site.
Sign-classified extractors:
extractIncreases(report)— rows withdiff > 0.extractDecreases(report)— rows withdiff < 0.extractUnchanged(report)— rows withdiff === 0.
Each carries six theorems (see "What's verified"). extractIncreases is what
the watch-mode notification uses — "foo/bar +1, baz/qux +3" is the verified
prefix of gainers in input order.
Conservation theorem decompose(report) — returns { increases, decreases, same } and, by invoking the partition lemmas in its body, proves that the
three splits partition the report without loss.
computeDiff (6 ensures):
- Row count matches input repo count.
- Each row's
repo,newCount,oldCount,diffare correct per the inputs. totalDiff === sumDiffs(rows)(non-trivial inductive step viaSumDiffs_append).
Each extractor (6 ensures × 3 extractors):
- Length bound
|res| ≤ |rows|. - Count equality
|res| === countXUpTo(rows, |rows|). - Sum equality
sumDiffs(res) === sumXUpTo(rows, |rows|)(for ±; unchanged's sum is 0 by soundness). - Soundness every output row has the expected sign.
- Completeness every input row with the expected sign appears in the output.
- Ordered completeness same-sign rows appear in the output in the same order they were listed on the command line — pairs
k1 < k2with matching signs witness orderedj1 < j2in the output.
decompose (conservation, 3 ensures):
|increases| + |decreases| + |same| === |report.rows|— every row in exactly one class.sumDiffs(increases) + sumDiffs(decreases) === report.totalDiff— the non-trivial sums account for the whole delta (the zero sum forsamefollows by soundness).
33 Dafny verification conditions, 0 errors.
src/
domain.ts annotated TS — source of truth
domain.dfy.gen generated by `lsc gen --backend=dafny`
domain.dfy .dfy.gen + proof additions
cli.ts trusted boundary (fetch, state, output, notification)
bin/
stars shell entry — `stars owner/repo [...]`
On top of the generated .dfy.gen, the following are hand-written:
SumDiffs_appendlemma —sumDiffs(rows + [row]) === sumDiffs(rows) + row.diff, by induction on|rows|. Used bycomputeDiff's loop body and in each extractor's per-iteration step to carry thesumDiffs(out) === sumXUpToinvariant.- Partition lemmas:
CountPartitionUpTo(rows, n)— positive + negative + zero counts equaln. Induction onn.SumPartitionUpTo(rows, n)— positive + negative sums equalsumDiffsUpTo. Induction onn.
- Bridge lemmas:
SumDiffs_eq_UpTo(rows)—sumDiffs(rows) === sumDiffsUpTo(rows, |rows|). Induction on|rows|, via…SumDiffsUpTo_shift(rows)—sumDiffsUpTo(rows, |rows|) === rows[0].diff + sumDiffsUpTo(rows[1..], |rows| - 1). The head/tail equivalence used to close the induction. Via…SumDiffsUpTo_prefix_agrees(rs, rt, n)— the technical helper showing thatsumDiffsUpToonly inspects a prefix and agrees across a shift.
- Witness hints in each extractor's loop body:
ghost var out_old := outsnapshot, plus asserts thatout[|out_old|]matches the appended row (Some-branch) and that the prefix is preserved across append (both branches). The same pattern carries the plain-completeness, ordered-completeness, sum-equality, and count-equality invariants. - Proof lemma calls in
decompose's body:CountPartitionUpTo,SumPartitionUpTo,SumDiffs_eq_UpToinvoked in sequence so Dafny can combine the extractors' upTo-indexed ensures into the top-level conservation claims.
Prerequisites: Node.js ≥ 18, Dafny ≥ 4.x.
Assumes LemmaScript is cloned as a sibling (../LemmaScript).
cd ../LemmaScript/tools && npm install
cd - && npm installnpm run stars -- owner/repo1 owner/repo2 ...
npm run stars -- --watch --every 10 owner/repo1 ...npm link once and stars is on PATH.
npm run check # lsc check --backend=dafny src/domain.ts
npm run gen # regen domain.dfy.gen
npm run regen # three-way merge .dfy with new .dfy.gen