Pinning
Where each dataset stands on tags and releases, how to pin one, and how to prove the bytes you hold are the bytes the annotations were computed against.
For what you may do with those bytes, see Licensing.
The state today
Checked against the repositories on 11 September 2026. This table will age; treat it as a snapshot, not a contract.
| Package | Tags | Releases | How you pin today |
|---|---|---|---|
| Quran Text | none | none | commit SHA |
| Qiraat Ayah Map | none | none | commit SHA |
| Quran Assets | none | none | commit SHA |
| QuranTech skill | none | none | commit SHA |
| Quran SVG | v1.0.0 | v1.0.0, no attached files | tag or commit SHA |
| Quran SVG Elements | v1.0.0 | v1.0.0 + a 108 MB tarball with its SHA-256 | release tag, then the digest |
| Quran Engine | v0.1.0 | v0.1.0 + a page-data tarball with its SHA-256 | release tag, then the digest |
| Quran Tajweed | v0.2.0, v0.3.0, v0.4.0 | three, each with versioned JSON files | release tag, then the edition digest |
| Quran PNG | artwork-v1.0.0 | one artwork bundle | not pinnable — see below |
Quran PNG is a hosted service rather than a dataset, and its v query parameter does not select a
renderer version: ?v=1 and ?v=9 both answer with x-render-version: 3 today. It is a cache key,
not a pin, so a URL you froze a year ago serves whatever the renderer produces now. Watch
x-render-version in the response if that matters to you.
Nothing is published to npm, PyPI, Packagist, pub.dev or Maven Central. Quran Text's own changelog says it: "This project has not had a tagged release yet; everything below is unreleased."
The QuranTech skill’s plugin.json declares "version": "1.0.0", and Qiraat Ayah Map stamps
_version: "0.1.0" into every generated file from a package.json marked "private": true. Neither
number is attached to anything published. Two people who both say they are on 1.0.0 can be holding
different files, and nothing in either repository will tell them so. Until a tag exists,
the commit is the version (Versioning and corrections, section 8).
Three things you can pin
| Pin | Proves | Where you get it |
|---|---|---|
| a commit SHA | the repository tree you read | gh api repos/OWNER/REPO/commits/HEAD --jq .sha |
| a release asset digest | the exact archive you downloaded | the .sha256 published beside the tarball |
| a content digest inside the data | the exact text an annotation set was computed against | a field in the data — see what a digest proves |
They answer different questions; the third is the one that catches the bug below.
Pin a dataset
This site does this in scripts/sync-demo-data.mjs; the record it keeps is the worked example.
Resolve the commit once
gh api repos/quran-ws/quran-text/commits/HEAD --jq .shaDo this once, by hand, when you adopt the dataset — not on every build. A script that resolves
HEADat build time is not pinned to anything.Read every file at that ref
gh api "repos/quran-ws/quran-text/contents/data/catalog.json?ref=$SHA" --jq .content | base64 -dQuote the URL. In zsh the
?is a glob character and the command fails without quotes. For files over 1 MB the API returns an emptycontent; read.download_urland fetch that instead.Commit the SHA to your repository
Put it in a file your reviewers see, so that changing the data is a diff someone approves. Ours lives in
public/demo/SOURCES.json:Payload built from Repository read Commit pinned quran-svg quran-ws/quran-svg 9fc0c87aebd7 quran-text quran-ws/quran-text 2aac0b1feefb qiraat-ayah-map quran-ws/qiraat-ayah-map 7f38fc0e3c49 quran-tajweed quran-ws/quran-tajweed 992d1484a41e quran-svg-elements quran-ws/quran-svg-elements de4a3b8c6b94 quran-assets quran-ws/quran-assets 591e8f515228 quran-engine quran-ws/quran-engine cf021bbe3831 Read from
public/demo/SOURCES.json, recorded 2026-09-12. Commits are shown to twelve characters; the file holds the full forty.Verify what you got
Every dataset publishes digests. The next section is which one to check for each.
What a digest proves
A digest proves one thing exactly: the bytes you hold are the bytes that were hashed. It says nothing about whether those bytes are correct, current, or the ones you wanted. The failure it catches does not announce itself.
Editions of the Uthmānī script that any reader would call identical are not identical as data. They disagree about where a small high seen sits, whether a hamzah is carried on a tatwīl, how tanwīn is drawn at a word end. Any one of those shifts every character offset after it. Apply Quran Tajweed spans to a text one character out and nothing throws; the colouring lands on the wrong letters and the error is found by a user who knows the text. Check the digest at load time and it becomes an exception instead.
Quran Tajweed pins the text, not the file
The annotations record the digest of the text they were computed against, and the digest is taken over the content in muṣḥaf order rather than over the JSON file — so re-serialising, re-indenting or reordering keys does not look like a different edition. The check is the same one shown on the Quran Tajweed page.
// Verified against tajweed-annotations-uthmani-hafs-v0.4.0.json and
// editions/uthmani-hafs.json at tag v0.4.0.
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
const edition = JSON.parse(readFileSync("editions/uthmani-hafs.json", "utf8"));
const annotations = JSON.parse(
readFileSync("tajweed-annotations-uthmani-hafs-v0.4.0.json", "utf8"),
);
const refs = Object.keys(edition.ayahs).sort((a, b) => {
const [sa, aa] = a.split(":").map(Number);
const [sb, ab] = b.split(":").map(Number);
return sa - sb || aa - ab;
});
const FIELD = String.fromCharCode(0); // separates a reference from its text
const RECORD = String.fromCharCode(1); // separates one ayah from the next
const canonical = refs.map((r) => `${r}${FIELD}${edition.ayahs[r]}`).join(RECORD);
const digest = createHash("sha256").update(Buffer.from(canonical, "utf8")).digest("hex");
digest === annotations.edition.sha256;
// true — b5d29736bb3ef49d9d331c4e60a59d83fe899b921e9e8dc35911bd4a18ce55f3
The package exports this as editionDigest() and assertEdition(). If you reimplement it in another
language, the two control characters are part of the contract.
Quran Text ships a manifest of every file
data/manifest.json lists the SHA-256 and byte length of every emitted file — 43 of them at the
commit this page was written against — and, separately, the SHA-256 of every KFGQPC source package the
build read.
// Verified against data/manifest.json at commit fea25cd.
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
const manifest = JSON.parse(readFileSync("data/manifest.json", "utf8"));
for (const f of manifest.files) {
let bytes;
try {
bytes = readFileSync(f.path);
} catch {
continue; // a file you did not download is not a failure
}
const got = createHash("sha256").update(bytes).digest("hex");
console.log(`${got === f.sha256 ? "ok " : "BAD "} ${f.path}`);
}
// ok data/catalog.json
One line per file you actually hold; files listed in the manifest but absent from your copy are skipped rather than failing, so the check works on a partial download too.
The fonts carry their own digest in each muṣḥaf file's font block, which is what this site checks
before converting one — a font swapped for a similar-looking one renders text that is wrong in ways a
non-reader cannot see.
Quran SVG Elements publishes two digests, and one command that lies
The release publishes quran-svg-hafs-kfgqpc.tar.gz.sha256 beside the tarball. Check that first; it
is the cheap one and it covers everything.
shasum -a 256 -c quran-svg-hafs-kfgqpc.tar.gz.sha256
# quran-svg-hafs-kfgqpc.tar.gz: OK
Inside the bundle, CHECKSUMS.txt covers every file. The obvious invocation fails on a bundle that is
completely intact:
cd quran-svg-hafs-kfgqpc
shasum -a 256 -c CHECKSUMS.txt > /dev/null
# shasum: WARNING: 2424 listed files could not be read
# exit 1
shasum -a 256 --ignore-missing -c CHECKSUMS.txt | grep -c ': OK$'
# 1223 — and the command exits 0
CHECKSUMS.txt lists 3,647 files, including 2,424 pre-compressed .svg.br and .svg.gz copies that
the tarball does not contain. Without --ignore-missing the check exits non-zero on a
good bundle, so a CI job that treats non-zero as corruption will reject every download. Verified by
extracting the published v1.0.0 tarball on 11 September 2026; the tarball digest matched.
The rules behind this
Which number means what, and what pinning cannot promise, are in Versioning and corrections, section 8, alongside the rule that the commit is the version until a dataset is tagged.
A build that resolves HEAD, or downloads “latest”, produces a different dataset on a
day you did not change anything, and because Qurʾānic text renders identically whether or not a
boundary moved, nothing in your test suite is likely to notice. Pin the SHA, check the digest, and
let the diff be a review.