◆ Canonical source

The authoritative, machine-readable specification is the source file. This page is a faithful human-readable rendering of it; the test vectors and code below are reproduced byte-for-byte. If anything in this rendering ever differs from the source file, the source file governs.

⬇ Download the canonical spec (HASH_ALGORITHM.md)
Covers: per-punch hash v1 & v2 · snapshot seal v1.  Permanent URL: wagecop.com/verify.  App correspondence: see §1 (dispatch) and §7 (change log).  Published spec version/commit: f30fb94 (committed 2026-06-15 · branch s5-archive-freeze · blob 4eb945cd)

Verify a record yourself

Paste the two values from a Verified export's verification appendix — the 64-character SHA-256 hash and its Verification text. Your browser checks them locally; nothing leaves this page (no upload, no lookup, no tracking).

Phase 1: pre-image check. The SHA-256 is computed in your browser with the Web Crypto API — your hash and text are never sent anywhere.

Verification status: the WageCop Verified Wage Records export (PDF / XLSX) includes a per-record verification appendix carrying, for every sealed punch, the full 64-character SHA-256 digest, its hash_version, and each hashed field at byte-exact fidelity — and, where shown, the ready-to-hash canonical payload (§3d) as a convenience. Independent third-party verification is therefore end-to-end exercisable from the Verified export alone — a verifier reproduces each hash from the appendix's fields using this specification (or, where the canonical payload is shown, by hashing it directly) and confirms byte-for-byte equality. The free Raw export and the main punch table continue to show the first 16 hex characters of the digest as a human-readable fingerprint; the full digest lives in the Verified export's appendix and server-side in punches.sha256_hash. Audience: third-party verifiers reproducing the SHA-256 integrity hash stored with every WageCop punch record. Examples: an attorney's expert witness, opposing counsel's auditor, a worker independently confirming their own data.

Purpose: ties the integrity claim made on every WageCop export ("Modifying any covered field breaks the hash") to a reproducible procedure any technically-literate party can follow without reading the application source.

Source of truth: this document is canonical. The Postgres function record_punch (defined in supabase_migrations/20260440_punches_evidentiary_hardening.sql) is the implementation; this document is its specification. If the two ever diverge, the test vector at the end of this document is the tiebreaker — the function and the spec must both reproduce it.


1. Verifier dispatch rule

Every WageCop punch row in the punches table has a hash_version smallint column. Read it first.

hash_versionAlgorithmCoverageSource code reference
1v1 (client-side, JSON.stringify)6 fieldslib/crypto.js (preserved as deprecated reference)
2v2 (server-side, jsonb canonical text)8 fieldsrecord_punch RPC body in migration 20260440

Legacy punches are hashed under v1; current punches are hashed under v2. The two algorithms coexist; a verifier dispatches on the hash_version column.


2. Algorithm v1 (legacy — pre-v1.15.3 rows)

2a. Fields, in insertion order

  1. userId — UUID of the WageCop user who recorded the punch.
  2. punchType — string. One of: work_in, work_out, meal_out, meal_in, meal_interrupted, meal_denied, rest_start, rest_end, rest_interrupted, rest_denied.
  3. punchedAt — ISO-8601 timestamp with milliseconds in UTC. Format produced by JavaScript Date.prototype.toISOString() (e.g., 2026-04-24T12:00:00.000Z).
  4. latitude — IEEE-754 double precision. May be null if location capture failed at punch time.
  5. longitude — IEEE-754 double precision. May be null.
  6. installId — UUID. Per-install identifier sourced from device secure storage (iOS Keychain / Android Keystore via expo-secure-store).

2b. Encoding

Apply JavaScript JSON.stringify({ userId, punchType, punchedAt, latitude, longitude, installId }):

2c. Digest

SHA-256 of the UTF-8 encoded payload string. Hex-encode lowercase. The hex string is stored in punches.sha256_hash.

2d. Sample v1 payload

For inputs userId="abc-...", punchType="work_in", punchedAt="2026-04-24T12:00:00.000Z", latitude=34.05, longitude=-118.24, installId="def-...":

{"userId":"abc-...","punchType":"work_in","punchedAt":"2026-04-24T12:00:00.000Z","latitude":34.05,"longitude":-118.24,"installId":"def-..."}

3. Algorithm v2 (current — post-v1.15.3 rows)

3a. Fields

The field list below preserves v1's order (per §2a) for diff-readability between v1 and v2. Serialization order is governed entirely by §3b's key sort rule and is independent of this list. A verifier reading §3a alone will get the field set right but will not produce the correct hash; §3b is the canonical encoding spec.

Same six fields as v1, plus two added in v2:

  1. userId — UUID
  2. punchType — string (same enum as v1)
  3. punchedAt — ISO-8601 with milliseconds in UTC, matching JavaScript Date.prototype.toISOString() (e.g., 2026-04-24T12:00:00.000Z). Server-formatted; the Postgres-specific format string is in §3b's "Timestamp formatting" subsection.
  4. latitude — double precision (may be null)
  5. longitude — double precision (may be null)
  6. installId — UUID
  7. locationAccuracy — text representation of GPS accuracy in meters (e.g., "5m"). May be null if accuracy capture failed.
  8. notes — text. User-provided free-form note attached at punch time. May be null if the user did not attach a note.

3b. Encoding (Postgres jsonb canonical text)

Build a JSON object via Postgres jsonb_build_object(...)::text with the field-keys in the listed order. The cast to text produces Postgres' canonical jsonb text representation, which has the following deterministic properties a verifier in any language must reproduce:

Key sort rule

Keys are NOT serialized in insertion order. Postgres jsonb sorts keys by (length ASC, then byte-order ASC over the UTF-8 representation of the key). The byte-order tiebreaker is locale-independent by design — Postgres compares the raw UTF-8 bytes of each key, not their locale-aware collation order. A verifier in any language must reproduce this byte-wise ordering, not a locale-aware sort. For the v2 keys (all ASCII identifiers), byte-wise UTF-8 ordering is identical to ASCII alphabetical ordering. The resulting v2 key order is fixed:

OrderKeyLength
1notes5
2userId6
3latitude8
4installId9
5longitude9
6punchType9
7punchedAt9
8locationAccuracy16

Within length=9 (four keys), byte-order resolves: installId < longitude < punchType < punchedAt.

A verifier emitting JSON in any other language must apply the same (length ASC, byte-order ASC) sort to reproduce Postgres' output. JavaScript example:

// Locale-neutral comparator. JS string < / > compares UTF-16 code units;
// for any key composed of BMP characters outside the surrogate range
// (U+0000..U+D7FF, U+E000..U+FFFF) this matches UTF-8 byte order. All
// current v2 keys are ASCII so the match is exact.
const sortedKeys = Object.keys(obj).sort(
  (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0)
)

Whitespace

A single space follows each colon (": "). Pairs are separated by a comma followed by a single space (", "). No other whitespace.

Number formatting

latitude and longitude are passed into jsonb_build_object(...) as double precision. record_punch executes with SET extra_float_digits = 1 at function entry — this setting is load-bearing for the hash: it selects Postgres' shortest-round-trip serialization of the double, which is the text the ::text cast emits and the exact bytes every stored hash was computed over. The result is the canonical shortest decimal that round-trips to the double, with no trailing zeros and no scientific notation. For the canonical inputs 34.05 and -118.24, the serialized payload reads 34.05 and -118.24 literally.

This shortest-round-trip text is byte-identical to JavaScript Number.prototype.toString() (equivalently JSON.stringify(n)) for every value of magnitude 1e-6 or greater — verified across a 2,300-value sweep against the production database over the full coordinate range. The emitters diverge only for magnitudes below 1e-6: Postgres emits plain decimal (0.0000001) where JavaScript switches to scientific notation (1e-7). A verifier in any language must emit plain decimal with no scientific notation. A coordinate component below 1e-6 cannot arise outside WageCop's expected California workplace range (California is far from the equator and the prime meridian), but a conformant verifier expands such values to plain decimal regardless. GUC-sensitive check: 0.30000000000000004 must serialize as 0.30000000000000004 (its value at extra_float_digits = 1), not 0.3.

A verifier reproducing the payload must emit, for the same double, the decimal Postgres produces under extra_float_digits = 1 — e.g., 34.05, not 3.405e1 or 34.050000000000001. Compare candidate output byte-for-byte against the canonical payload in §3d (and the §5 vector) before trusting the implementation on real punch data.

Null handling

JSON null (the literal four characters n, u, l, l, no quotes). Applies to all four nullable fields (latitude, longitude, locationAccuracy, notes) when they were not captured at punch time. The Postgres jsonb_build_object(...) call inside record_punch passes these through to the canonical text unchanged, so a verifier emitting null for any of the four reproduces the input byte-for-byte.

Timestamp formatting

punchedAt is formatted as ISO-8601 with milliseconds in UTC: YYYY-MM-DDTHH:MM:SS.MMMZ (e.g., 2026-04-24T12:00:00.000Z). The Postgres format string is 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'. Matches Date.prototype.toISOString().

UUID handling

UUIDs (userId, installId) emit as standard 8-4-4-4-12 hex format strings (e.g., 00000000-0000-0000-0000-000000000001).

3c. Digest

SHA-256 of the UTF-8 encoded payload string. Hex-encode lowercase via Postgres encode(digest(payload, 'sha256'), 'hex'). The hex string is stored in punches.sha256_hash.

3d. Sample v2 payload

For the canonical test vector (full inputs + expected hash in §5):

{"notes": null, "userId": "00000000-0000-0000-0000-000000000001", "latitude": 34.05, "installId": "00000000-0000-0000-0000-000000000002", "longitude": -118.24, "punchType": "work_in", "punchedAt": "2026-04-24T12:00:00.000Z", "locationAccuracy": "5m"}

Note the key ordering: notes, userId, latitude, installId, longitude, punchType, punchedAt, locationAccuracy. Single space after each colon; comma+space between pairs.


4. What the integrity claim covers

The punches table has 16 columns. The column-level seal (table-level REVOKE UPDATE from anon + authenticated, see migration 20260440) prevents direct client updates to all of them — but the integrity claim itself only covers the 8 fields hashed in v2 (or 6 in v1). The remaining sealed columns are protected by the seal but not bound to the hash; their integrity rests on the seal alone.

Columnv1 hashedv2 hashedSealed (REVOKE)Source
notesuser input at punch time
user_id✓ (userId)✓ (userId)server (auth.uid())
latitudedevice GPS
install_id✓ (installId)✓ (installId)device secure storage
longitudedevice GPS
punch_type✓ (punchType)✓ (punchType)user action
punched_at✓ (punchedAt)✓ (punchedAt)client clock at punch time
location_accuracy✓ (locationAccuracy)device GPS
voided_atserver (void_punch RPC)
void_reasonuser input via void_punch
hash_versionserver-set
recorded_atserver (record_punch RPC, set at INSERT)
sha256_hash(the hash itself)(the hash itself)server-computed in record_punch
idserver (uuid_generate_v4)
created_atserver (now())
shift_idserver (record_punch shift bookkeeping)
uploadedserver (record_punch)
device_idlegacy column, no client write path

Four sealed-but-not-hashed columns deserve specific note:

Four sealed-but-deliberately-uncovered columns (id, created_at, shift_id, uploaded, device_id) are server-managed or legacy. The hash claim makes no assertion about them.

(Note: created_at and recorded_at carry the same value for any row inserted via record_punch. The difference is naming — created_at is a Postgres-convention column on most tables in this database; recorded_at is a legal-audience-friendly alias surfaced in exports. Both are sealed.)


5. Canonical test vector

Any third-party verifier implementation should reproduce this hash from the inputs below, or know its implementation is broken.

Inputs (v2 algorithm)

userId            = 00000000-0000-0000-0000-000000000001
punchType         = work_in
punchedAt         = 2026-04-24T12:00:00.000Z
latitude          = 34.05
longitude         = -118.24
installId         = 00000000-0000-0000-0000-000000000002
locationAccuracy  = 5m
notes             = null

Expected serialized payload (jsonb canonical text)

{"notes": null, "userId": "00000000-0000-0000-0000-000000000001", "latitude": 34.05, "installId": "00000000-0000-0000-0000-000000000002", "longitude": -118.24, "punchType": "work_in", "punchedAt": "2026-04-24T12:00:00.000Z", "locationAccuracy": "5m"}

Expected SHA-256 (hex, lowercase)

2ccac848b8f9f7c217fab8440a58c74c76333736c0f9fcab6feb863ccabf0e7b

If your verifier produces this hash, your implementation is correct. If not, walk back through §3b checking key sort order, whitespace, number formatting, and UUID/null encoding.

Full-precision reference vector (the extra_float_digits = 1 pin)

The canonical vector above uses short coordinates (34.05, -118.24) whose text is identical at extra_float_digits = 0 and = 1, so it does not exercise the float-precision pin (§3b Number formatting). The vector below uses a full-precision coordinate that does — it is the worked proof that the pin is load-bearing.

Inputs: as §5 above, but latitude = 34.05223421234568, longitude = -118.49275398765432.

Expected serialized payload (extra_float_digits = 1):

{"notes": null, "userId": "00000000-0000-0000-0000-000000000001", "latitude": 34.05223421234568, "installId": "00000000-0000-0000-0000-000000000002", "longitude": -118.49275398765432, "punchType": "work_in", "punchedAt": "2026-04-24T12:00:00.000Z", "locationAccuracy": "5m"}

Expected SHA-256 (hex, lowercase):

f3d7fa8050c9b5c0fcb8467500a9e623f74c961a4d1ed516e3c50f52a3a60c78

At extra_float_digits = 0 the same inputs serialize as 34.0522342123457 / -118.492753987654, yielding a different payload and a different hash — exactly the regression the pin prevents. (Captured from prod bymuhltjmgjbcupkdnsf, PG 17.6, 2026-06-21.)

Forward-looking secondary vector (locale-divergence reference)

The canonical vector above uses 8 ASCII-only keys for which byte-order and locale collation produce identical results. The second vector below is hypothetical — it does not correspond to any real punch row today and is not exercised by test_punches_evidentiary_hardening.js. Its purpose is to lock the spec's "byte-order over UTF-8" rule (§3b) against a future schema evolution that could add a key whose locale collation diverges from byte order, by giving a concrete pair where the divergence would manifest.

Hypothetical key pair where divergence would surface:

KeyUTF-8 bytes (hex)First-byteLocale collation (en-US, ICU)Byte-order
f660x66between e and gbetween e and g
é (U+00E9)c3 a90xC3typically near e (treated as e-with-accent)after every ASCII letter (0xC3 > 0x7A)

A verifier using localeCompare (en-US) would order é < f. Postgres jsonb's byte-order rule places é > f (0xC3 > 0x66 at first byte). Stored hashes are produced by Postgres, so the byte-order placement is canonical; any verifier that reproduces locale collation would compute the wrong serialized payload and the wrong hash.


6. What this hash does NOT prove

These limitations apply equally to v1 and v2.


7. Change log


8. Snapshot seal (week_snapshots.seal_hash) — v1

8.1 What it is

The snapshot seal is a single SHA-256 over the whole frozen workweek record stored in week_snapshots. It is computed on the worker's device at FREEZE and stored in week_snapshots.seal_hash. There is no server-side computation or server-side verification of the seal (by design): the server stores the value the client supplies and rejects any later mutation of the row's sealed content. The authority a verifier reproduces against is this specification plus the canonical test vector in §8.8 — reproducible by the client and by any third-party verifier in any language. Postgres is not a party to this seal (unlike the per-punch v2 hash in §3, which Postgres computes).

8.2 The three-layer integrity model (read before relying on the seal)

A sealed snapshot makes a layered integrity claim. State it precisely; do not over-claim:

  1. Server-attested per-punch content — each punch record inside the snapshot carries the sha256_hash Postgres computed at record time (§3). A client cannot forge these. This is the evidentiary anchor.
  2. Client-attested collection sealseal_hash (this section) proves the set of records/alerts/pay/provenance has not been altered since FREEZE. It is computed on-device, so its trust profile is closer to the v1 client hash than to the server-attested v2 hash. It is tamper-evident, not server-attested.
  3. Server-enforced immutability — once sealed, the row's sealed columns cannot be updated (table-level REVOKE + the snapshot RPC's immutable check). Once records are archived on the server, WageCop provides no mechanism to correct or suppress archived records to anyone including the user (corrections/amendments out of scope — §8.4).

The honest one-line claim is: "server-attested raw punches + a tamper-evident collection seal + server-enforced immutability." Never describe the seal itself as "server-attested" or the computed pay/alerts as "server-verified."

8.3 Layer classification of the sealed fields

8.4 Sealed payload

The seal covers exactly these top-level fields (the evidentiary content available on-device at FREEZE): seal_version, workweek_start, workweek_end, depth_at_capture, captured_at, punch_records[], resolved_alerts[], pay_block, provenance.

Excluded (and a verifier must reconstruct the payload without them): seal_hash (it is the output) and the server-assigned confirmed_at / created_at / id (they do not exist when the client computes the seal). (There is no status/supersedes exclusion: a frozen snapshot is immutable and is never corrected in place — corrections/amendments are out of WageCop scope.)

8.5 Canonicalization (a verifier in any language must reproduce this exactly)

Serialize the §8.4 payload to a single canonical string, then SHA-256 it. The serialization is a hybrid — do not use an off-the-shelf JCS / RFC-8785 canonicalizer wholesale (its key-sort is wrong here):

8.6 seal_version

seal_version is inside the sealed payload (so the version declaration is itself tamper-evident — a downgrade relabel breaks the seal) and stored as a column. A verifier dispatches on the stored column to pick the algorithm version, then confirms the sealed payload carries the same value.

8.7 Digest

SHA-256 of the UTF-8 canonical string; hex-encode lowercase. (expo-crypto on-device; any SHA-256 for a verifier — the digest is identical across implementations.)

8.8 Canonical test vector

The representative snapshot is the VECTOR in test_snapshot_seal.js (a one-punch, one-alert frozen week); that pure-node test locks both the exact canonical string and the seal hash below.

Inputs (the §8.4 sealed payload, natural order):

{
  "seal_version": 1,
  "workweek_start": "2026-04-20",
  "workweek_end": "2026-04-26",
  "depth_at_capture": 5,
  "captured_at": "2026-04-24T12:00:00.000Z",
  "punch_records": [
    { "id": "00000000-0000-0000-0000-000000000001", "punch_type": "work_in",
      "punched_at": "2026-04-24T12:00:00.000Z", "latitude": 34.05, "longitude": -118.24,
      "location_accuracy": "5m", "sha256_hash": "2ccac848b8f9f7c217fab8440a58c74c76333736c0f9fcab6feb863ccabf0e7b",
      "hash_version": 2, "notes": null, "voided_at": null, "void_reason": null, "hourly_rate": 20,
      "user_id": "00000000-0000-0000-0000-0000000000aa",
      "install_id": "00000000-0000-0000-0000-000000000002", "recorded_at": "2026-04-24T12:00:05.000Z" }
  ],
  "resolved_alerts": [
    { "violation_type": "MEAL_MISSING", "occurred_at": "2026-04-24T17:30:00.000Z", "punch_id_start": "00000000-0000-0000-0000-000000000001", "dismissed_at": null, "dismiss_reason": null }
  ],
  "pay_block": {
    "inputs": { "default_hourly_rate": 20, "daily_cutoff_hour": 0, "workweek_start": "Monday", "on_duty_meal_consent": false },
    "output": { "regularHours": 8, "otHours": 0, "doubleOtHours": 0, "totalHours": 8, "weightedRate": 20,
                "baseEarnings": 160, "otPay": 0, "doubleOtPay": 0, "totalPay": 160,
                "perRateBreakdown": [{ "rate": 20, "hours": 8, "pay": 160 }] }
  },
  "provenance": { "ruleSetDigest": "3bc23d0a5c0390975a38f49a2b78e80b4c963cb2d4726df841f7bb1a9aa4cf12", "settings": { "workweek_start": "Monday", "daily_cutoff_hour": 0, "first_meal_waiver": false, "second_meal_waiver": false, "on_duty_meal_consent": false }, "computedAt": "2026-04-24T12:00:00.000Z" }
}

Canonical string (§8.5 applied — 1438 bytes):

{"pay_block":{"inputs":{"workweek_start":"Monday","daily_cutoff_hour":0,"default_hourly_rate":20,"on_duty_meal_consent":false},"output":{"otPay":0,"otHours":0,"totalPay":160,"totalHours":8,"doubleOtPay":0,"baseEarnings":160,"regularHours":8,"weightedRate":20,"doubleOtHours":0,"perRateBreakdown":[{"pay":160,"rate":20,"hours":8}]}},"provenance":{"settings":{"workweek_start":"Monday","daily_cutoff_hour":0,"first_meal_waiver":false,"second_meal_waiver":false,"on_duty_meal_consent":false},"computedAt":"2026-04-24T12:00:00.000Z","ruleSetDigest":"3bc23d0a5c0390975a38f49a2b78e80b4c963cb2d4726df841f7bb1a9aa4cf12"},"captured_at":"2026-04-24T12:00:00.000Z","seal_version":1,"workweek_end":"2026-04-26","punch_records":[{"id":"00000000-0000-0000-0000-000000000001","notes":null,"user_id":"00000000-0000-0000-0000-0000000000aa","latitude":34.05,"longitude":-118.24,"voided_at":null,"install_id":"00000000-0000-0000-0000-000000000002","punch_type":"work_in","punched_at":"2026-04-24T12:00:00.000Z","hourly_rate":20,"recorded_at":"2026-04-24T12:00:05.000Z","sha256_hash":"2ccac848b8f9f7c217fab8440a58c74c76333736c0f9fcab6feb863ccabf0e7b","void_reason":null,"hash_version":2,"location_accuracy":"5m"}],"workweek_start":"2026-04-20","resolved_alerts":[{"occurred_at":"2026-04-24T17:30:00.000Z","dismissed_at":null,"dismiss_reason":null,"punch_id_start":"00000000-0000-0000-0000-000000000001","violation_type":"MEAL_MISSING"}],"depth_at_capture":5}

Seal hash (SHA-256 hex, lowercase):

d168b397fb00de8c9557f8f47cc6c093748486342ca78a2375ac98dde2d3c076

A verifier that serializes the inputs per §8.5 reproduces the canonical string, and SHA-256 of that string is the seal hash. test_snapshot_seal.js locks both byte-for-byte.

8.9 Cross-engine determinism

The number rule (§8.5) relies on ECMAScript NumberString producing byte-identical output across engines. The ECMAScript specification mandates the shortest-round-trip algorithm, so all conformant engines produce identical output; WageCop validates this on its production runtime before relying on any seal.

8.10 What the seal does NOT prove

Everything in §6 applies (no hardware attestation; service_role can write; etc.). Additionally: the seal is client-computed, so it does not by itself prove the correctness of the layer-2 computed values — that rests on re-derivation from the layer-1 inputs (§8.3).

8.11 Re-derivation procedure (punch_records is a superset)

punch_records is the session-overlap superset for the frozen week: it contains every punch of every shift-session whose worked interval overlaps the week's boundaries — including the post-boundary tails of a session that straddles into an adjacent week — plus any voided punches whose instant falls in the week (sealed as evidence). It is therefore a superset of the punches the week's pay_block.output and resolved_alerts were derived over. A verifier re-deriving the layer-2 values (§8.3) must reproduce the same windowing the device applied:

  1. Reconstruct the week as the half-open interval [W_start, W_end), cutoff-anchoredW_start is the workweek-start day at the hour provenance.settings.daily_cutoff_hour (NOT midnight); W_end is that same cutoff hour seven days later. (A punch whose local PT hour is below the cutoff belongs to the prior work-date, and may belong to the prior week.)
  2. Drop voided punches (any carrying voided_at) before deriving — they are sealed as evidence, never counted.
  3. Clamp each shift-session's worked interval to [W_start, W_end): a session straddling a boundary contributes only its in-week portion; an open / forgot-clock-out session is bounded by W_end (the week's own cutoff-anchored end) — the same clamp every session gets.
  4. Re-derive pay_block.output and resolved_alerts over the clamped, voided-filtered set, supplying W_end as the derivation clock (nowMs)never a live clock, and never captured_at — so the result is a pure function of (punches, W_start, W_end), deterministic and reproducible at any later date. (captured_at remains a sealed provenance field; it is not the pay/alert bound.)

A shift that spans multiple weeks is sealed in full in each week it overlaps; each week's seal is clamped to that week's portion, and the worked minutes are partitioned across the weeks — never summed into a single week. A verifier that follows this procedure reproduces the sealed pay_block.output and resolved_alerts exactly. A verifier that derives straight from the raw superset — skipping the clamp (step 3) or using a live clock (step 4) — will NOT match, and that is expected, not a seal failure.

8.12 Rule-set digest (provenance.ruleSetDigest)

provenance.ruleSetDigest identifies the exact wage-rule thresholds used to derive a sealed week's pay_block and resolved_alerts. It is the SHA-256 (hex, lowercase) of the §8.5 canonical string of the durable rule bundle { schemaVersion, stateCode, thresholds }thresholds holding the jurisdiction's wage-rule values in minutes (daily/weekly overtime, the two meal positions and their waiver ceilings, the meal interval and minimum, the rest-break ladder and step, the rest minimum, the split-shift gap) and the seventh-consecutive-day count. A verifier reproduces the digest from the bundle below, confirms it matches the record's ruleSetDigest (proving which rules were applied), then re-derives the layer-2 values (§8.11) from those thresholds.

The California default bundle (canonical string, keys in §8.5 order) and its digest:

{"stateCode":"CA","thresholds":{"dailyOtMin":480,"dailyDotMin":720,"mealPos1Min":300,"mealPos2Min":600,"restStepMin":240,"splitGapMin":60,"weeklyOtMin":2400,"mealShortMin":30,"restShortMin":10,"restLadderMin":[210,360,600,840,1080,1320],"mealIntervalMin":300,"mealPos1WaivedMin":360,"mealPos2WaivedMin":720,"seventhDayConsecutive":7},"schemaVersion":1}

SHA-256 = 3bc23d0a5c0390975a38f49a2b78e80b4c963cb2d4726df841f7bb1a9aa4cf12