volt: Q71 (part 1/2) -- commits-table.md row 4 matches its source byte for byte, and the file is now graded by a re-fetching verifier wired into the lane gate

Q71's two faults at 6d936be, both re-measured here:
- Row 4's Subject read .../pub/scm/linux/kernel_git/mtd/linux where the source
  says .../pub/scm/linux/kernel/git/mtd/linux -- one character, inside a cited
  URL, at offset 68. 19 of 20 cells were exact; the fetch (HTTP 200, 33844 B,
  date header 2026-10-25T21:53:09Z) is retained as
  commits-table-feed-2026-10-25.json so the reading needs no network.
- Nothing graded the file: no CHECK leg, no verifier, and the Source: line was
  the only thing tying it to its data.

Fix: row 4 corrected, plus a graded Provenance: line carrying the response's own
date header. New gate wizard-work/ledger/verify_commits_table.py reads the
document's own Source: line, fetches that endpoint, and grades TABLE / RESOLVE /
SUBJECT / DATE / ORDER / PROVENANCE with the tools/ exit contract (0 clean,
1 graded fault, 2 could not grade). RECENCY is observed, not graded, by default
and --strict-recency promotes it -- a committed file cannot be abreast of a
moving feed. No asserts, so -O cannot strip a check. Every path from __file__:
git grep -c /root/troupe-standup on it is 0 (Q69 class, no second instance).

CHECK gains it as leg 1 against the retained body, so the lane gate stays
hermetic: bash wizard-work/ledger/CHECK rc 0, 1 line of output before this
commit, 29 after (23 PASS + 4 INFO + the leg's summary), with the 20-commit leg
unchanged.

23 control cells, all run (scratch/q71/controls.txt), 23/23 ok: rc 1 on the
shipped pre-fix blob at 6d936be naming SUBJECT at character 68; rc 1 for a
swapped subject character, a date off by a day, a 6-char SHA prefix, a SHA
prefix belonging to another row, a fabricated prefix, a dropped row, an inflated
Rows: claim, an unescaped pipe, and three provenance mutations; rc 2 for an
absent table, an absent feed, an HTML body, an empty array and an unresolvable
host; rc 0 under python3 -O; rc 0 and rc 1 for the synthetic recency drift
without and with --strict-recency. Live leg by hand: rc 0.

Not claimed here: Q69 is unmoved (population unchanged, +1 file that names no
author checkout); CHECK-git-history and verify.sh still carry the Q69 pin and
are not this item's.

Closure record and evidence doc land in the next commit.
This commit is contained in:
volt committed 2026-10-25 21:57:46 +00:00
1 parent 0a86c9c5f0
commit b9e3780b32
4 files changed
+805 -2

No files matched your search

+14 -1
View File
@@ -1,8 +1,21 @@
#!/usr/bin/env bash
# CHECK — ledger 20-commit-table (Extract lane) verification.
# Re-derives the table from git and diffs against commits.md/tsv/json/detail.tsv.
# Exits 0 only on exact match.
# Two legs: (1) commits-table.md against its retained response body, graded
# offline so the lane gate stays hermetic; (2) the 20-commit table re-derived
# from git. Exits 0 only when both legs pass.
set -u
OUT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$OUT" || exit 1
# Leg 1 — commits-table.md (Q71). The verifier re-reads the document's own
# `Source:` line and normally fetches it; here it is pointed at the response
# body pinned at 2026-10-25T21:53:09Z (sha256 a7f55e4e...) so the gate needs no
# network and grades the commit rather than the author's tree. The live leg is
# `python3 verify_commits_table.py` with no --feed.
python3 "$OUT/verify_commits_table.py" \
--table "$OUT/commits-table.md" \
--feed "$OUT/commits-table-feed-2026-10-25.json" || exit 1
# Leg 2 — the 20-commit table.
exec python3 "$OUT/verify_commits_check.py" "$@"
@@ -0,0 +1,437 @@
[
{
"sha": "21a7f44fd0765fcc5194130b28767786eba1d1e7",
"node_id": "C_kwDOACN7MtoAKDIxYTdmNDRmZDA3NjVmY2M1MTk0MTMwYjI4NzY3Nzg2ZWJhMWQxZTc",
"commit": {
"author": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:11:23Z"
},
"committer": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:11:23Z"
},
"message": "Merge tag 'devicetree-for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/robh/linux\n\nPull devicetree updates from Rob Herring:\n\n - A bunch of improvements in dt-check-style. Fix various bindings based\n on these additional checks.\n\n - Convert hisilicon,hix5hd2-ir, hisilicon,hi6220-ade,\n hisilicon,hi6220-dsi, hisilicon,hip05-sas-v1, MediaTek RT5650 codecs,\n mediatek,g3dsys, mediatek,mt2701-scpsys, ti,omap-mpu, and\n ti,omap-ocp2scp to DT schema format\n\n - Add schemas for ST STM32MP2 DWC3 xHCI USB, ST STM32MP25 USB2-FEMTO\n PHY, spacemit,k1-gpu and spacemit,k3-gpu, HiSilicon local bus,\n HiSilicon HIP05 CPLD, SDMC DM2016, and QCom SM7250 and Kuno PDC\n\n - Various binding updates to fix schema warnings in .dts files\n\n - Drop obsolete .txt bindings for STM32(MP1) RCC, VExpress hwmon and\n regulator, and mailbox.txt\n\n - Remove the unused of_overlay_remove_all() function\n\n - Fixes for of_check_msi_parent()\n\n - Add \"i2c-parent\" tracking to fw_devlink\n\n* tag 'devicetree-for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/robh/linux: (72 commits)\n dt-bindings: usb: dwc3: Document ST STM32MP2 DWC3 xHCI USB controller\n dt-bindings: iommu: Describe MT8195 infra IRQs\n dt-bindings: clock: mediatek: Add MT8516 fallbacks\n dt-bindings: media: Convert hisilicon,hix5hd2-ir to DT schema\n dt-bindings: display: Convert hisilicon,hi6220-ade to DT schema\n dt-bindings: display: Convert hisilicon,hi6220-dsi to DT schema\n dt-bindings: phy: Document ST STM32MP25 USB2-FEMTO PHY\n of: overlay: remove unused of_overlay_remove_all()\n dt-bindings: power: Convert mediatek,mt2701-scpsys to DT schema\n dt-bindings: bus: Add HiSilicon local bus\n dt-bindings: gpu: img,powervr-rogue: add spacemit,k1-gpu and spacemit,k3-gpu\n dt-bindings: interrupt-controller: qcom,pdc: Document SM7250 PDC\n dt-bindings: vendor-prefixes: add Ambarella\n dt-bindings: reset: Drop obsolete STM32(MP1) RCC bindings\n dt-bindings: arm: keystone: ti,sci: Correct reg and mboxes constraints\n dt-bindings: ASoC: Convert MediaTek RT5650 codecs bindings to DT schema\n dt-bindings: scsi: Convert hisilicon,hip05-sas-v1 to DT schema\n of/irq: Stop the MSI walk at the first msi-parent\n of/irq: Fix device node refcount leak in of_check_msi_parent()\n dt-bindings: display: faraday,tve200: Use a level-high interrupt\n ...",
"tree": {
"sha": "fd0caac36e8ce4eb477f0d3aa37655fe94ba09e4",
"url": "https://api.github.com/repos/torvalds/linux/git/trees/fd0caac36e8ce4eb477f0d3aa37655fe94ba09e4"
},
"url": "https://api.github.com/repos/torvalds/linux/git/commits/21a7f44fd0765fcc5194130b28767786eba1d1e7",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/torvalds/linux/commits/21a7f44fd0765fcc5194130b28767786eba1d1e7",
"html_url": "https://github.com/torvalds/linux/commit/21a7f44fd0765fcc5194130b28767786eba1d1e7",
"comments_url": "https://api.github.com/repos/torvalds/linux/commits/21a7f44fd0765fcc5194130b28767786eba1d1e7/comments",
"author": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"url": "https://api.github.com/repos/torvalds/linux/commits/89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"html_url": "https://github.com/torvalds/linux/commit/89d2d7bf0698b7fdfc914bfd219e739e9308d332"
},
{
"sha": "e8ff9af18e80279b80cf7063244db91b12f5e27e",
"url": "https://api.github.com/repos/torvalds/linux/commits/e8ff9af18e80279b80cf7063244db91b12f5e27e",
"html_url": "https://github.com/torvalds/linux/commit/e8ff9af18e80279b80cf7063244db91b12f5e27e"
}
]
},
{
"sha": "89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"node_id": "C_kwDOACN7MtoAKDg5ZDJkN2JmMDY5OGI3ZmRmYzkxNGJmZDIxOWU3MzllOTMwOGQzMzI",
"commit": {
"author": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:07:00Z"
},
"committer": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:07:00Z"
},
"message": "Merge tag 'efi-next-for-v7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/efi/efi\n\nPull EFI updates from Ard Biesheuvel:\n\n - Set the LoaderDevicePartUUID EFI variable from the stub to help\n systemd discover the file system layout when booting straight into\n Linux without GRUB or systemd-boot (Vincent Mailhol)\n\n - Some GUID deduplication work from Vincent\n\n - Remove pointless UTF16->UTF8 and vice versa conversions from the stub\n\n - Some kmap() -> kmap_local_page() conversions in the capsule loader\n\n - Add a runtime control to en/disable EFI runtime services so that\n PREEMPT_RT systems can enable them as needed\n\n - Add a 'nostatfs' mount option to efivarfs so that the cost of\n querying the EFI variable store (which is already rate limited) can\n be avoided entirely on systems where it is disproportionately costly\n\n - Rework the EFI libstub Makefile to align x86 with other\n architectures, and inherit/adapt the global CFLAGS variable rather\n than reconstruct it from scratch\n\n - Constify attributes of statically allocated sysfs nodes\n\n - Avoid register x18 when building the arm64 EFI stub, in order to\n comply with the UEFI spec, and to permit it to execute in a context\n where x18 is special\n\n - Preserve the physical addresses of all EFI config tables that may be\n of interest to an incoming kexec kernel\n\n - Add some LKDTM tests for EFI runtime crashes where the faulting\n address is inside a EFI memory region\n\n - Other minor bug fixes and cosmetic cleanups\n\n* tag 'efi-next-for-v7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/efi/efi: (35 commits)\n lkdtm: Add more EFI runtime tests\n efi: Initialize all configuration table addresses to EFI_INVALID_TABLE_ADDR\n efi: Retain the address of the LINUX_EFI_MEMRESERVE table\n efi/libstub: Don't replace an existing LINUX_EFI_MEMRESERVE table\n efi/runtime-wrappers: Don't park the worker after a recovered firmware fault\n efi: Use sysfs_emit() not sprintf() to generate sysfs file contents\n efi: Constify sysfs attributes\n efi/libstub: Disable kernel stack erasing in the common flags\n efi/libstub: Build the x86 stub from KBUILD_CFLAGS\n efi/libstub: Declare the x86 stub's assembly entry points\n efi/libstub: arm64: Don't use the platform register x18\n efi/libstub: Free cmdline_ptr in efi_pe_entry\n efi/riscv: libstub: Don't set image_size in handle_kernel_image()\n efi/libstub: Fix error unwind freeing fdt in allocate_new_fdt_and_exit_boot()\n efivarfs: add nostatfs mount option to skip QueryVariableInfo()\n efi/libstub: add initial Boot Loader Interface support\n efi/libstub: Add efi_snprintf() to construct wide strings\n efi/libstub: Add support for printing human readable GUIDs\n efi/libstub: Output UTF-16 directly from vsnprintf()\n efi/libstub: Avoid efi_puts() for compile time constant strings\n ...",
"tree": {
"sha": "d568e43e3d67daccda400e6e4c720b65b1e1718a",
"url": "https://api.github.com/repos/torvalds/linux/git/trees/d568e43e3d67daccda400e6e4c720b65b1e1718a"
},
"url": "https://api.github.com/repos/torvalds/linux/git/commits/89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/torvalds/linux/commits/89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"html_url": "https://github.com/torvalds/linux/commit/89d2d7bf0698b7fdfc914bfd219e739e9308d332",
"comments_url": "https://api.github.com/repos/torvalds/linux/commits/89d2d7bf0698b7fdfc914bfd219e739e9308d332/comments",
"author": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"url": "https://api.github.com/repos/torvalds/linux/commits/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"html_url": "https://github.com/torvalds/linux/commit/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b"
},
{
"sha": "2c493ac8c347167eec9e4e985d2b58d5fa6fd420",
"url": "https://api.github.com/repos/torvalds/linux/commits/2c493ac8c347167eec9e4e985d2b58d5fa6fd420",
"html_url": "https://github.com/torvalds/linux/commit/2c493ac8c347167eec9e4e985d2b58d5fa6fd420"
}
]
},
{
"sha": "9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"node_id": "C_kwDOACN7MtoAKDk2MjhlZTBjMjIxOWRhYWE2Mzc0YjBjNWY1YTVjMGQ1NWUwZTI2M2I",
"commit": {
"author": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:02:48Z"
},
"committer": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T06:02:48Z"
},
"message": "Merge tag 'gpio-fixes-for-v7.4-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/brgl/linux\n\nPull gpio fixes from Bartosz Golaszewski:\n\n - add ACPI quirks for Thinkpad L13 Gen 2a and Acer Nitro 16S AI to\n ignore a wrong GPIO interrupt declaration\n\n - add missing return value check of devm_gpiochip_add_data() to\n gpio-lpc32xx\n\n - don't set GPIOD_FLAG_IS_OUT from character device code as GPIO core\n already does it where needed and it can lead to providing invalid\n info to user-space on request errors\n\n - don't report a bogus -ENOMEM for a line requested without a label\n\n - propagate errors occurring when setting line values for pins for\n which we emulate open-drain or open-source drive settings\n\n - fix interrupt handling bugs in gpio-pca953x and gpio-davinci\n\n - fix more memory leaks in GPIO core\n\n - clear event clock flags from pins on GPIO release\n\n* tag 'gpio-fixes-for-v7.4-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/brgl/linux:\n gpiolib: don't report -ENOMEM for a requested line without a label\n gpiolib: Propagate emulated drive errors when setting arrays\n gpio: pca953x: Handle direction register read errors in IRQ paths\n gpio: pca953x: Preserve wakeup accounting on parent IRQ failure\n gpio: davinci: Clear interrupt status in every bank on suspend\n gpiolib: Free chip resources when allocating the label fails\n gpiolib: cdev: don't set GPIOD_FLAG_IS_OUT\n gpiolib: clear the event clock flags when a line is released\n gpiolib: fix memory leak in gpiod_set_array_value_complex() EPERM error path\n gpiolib: acpi: Ignore GPIO 11 interrupt on Acer Nitro 16S AI\n gpio: lpc32xx: check devm_gpiochip_add_data() return value\n gpiolib: acpi: Ignore GPIO 11 interrupt on ThinkPad L13 Gen 2a",
"tree": {
"sha": "43c5168bdacd79bd2f2356d2902b82db4e5e877e",
"url": "https://api.github.com/repos/torvalds/linux/git/trees/43c5168bdacd79bd2f2356d2902b82db4e5e877e"
},
"url": "https://api.github.com/repos/torvalds/linux/git/commits/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/torvalds/linux/commits/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"html_url": "https://github.com/torvalds/linux/commit/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b",
"comments_url": "https://api.github.com/repos/torvalds/linux/commits/9628ee0c2219daaa6374b0c5f5a5c0d55e0e263b/comments",
"author": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "44388c8f3e22b578c9089df53301ef7e53e7fb01",
"url": "https://api.github.com/repos/torvalds/linux/commits/44388c8f3e22b578c9089df53301ef7e53e7fb01",
"html_url": "https://github.com/torvalds/linux/commit/44388c8f3e22b578c9089df53301ef7e53e7fb01"
},
{
"sha": "dae739137441667d67fbfcc4d303a0ecc6618662",
"url": "https://api.github.com/repos/torvalds/linux/commits/dae739137441667d67fbfcc4d303a0ecc6618662",
"html_url": "https://github.com/torvalds/linux/commit/dae739137441667d67fbfcc4d303a0ecc6618662"
}
]
},
{
"sha": "44388c8f3e22b578c9089df53301ef7e53e7fb01",
"node_id": "C_kwDOACN7MtoAKDQ0Mzg4YzhmM2UyMmI1NzhjOTA4OWRmNTMzMDFlZjdlNTNlN2ZiMDE",
"commit": {
"author": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T05:49:58Z"
},
"committer": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T05:49:58Z"
},
"message": "Merge tag 'mtd/for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/mtd/linux\n\nPull MTD updates from Miquel Raynal:\n \"Core MTD:\n - Fixes to the virt-concat driver\n - Added INT0800 firmware-flash map driver\n - mtdblock: prevent reclaim I/O during request processing\n - cfi_cmdset_0002: cap the write-buffer chunk at 256 bytes on an x8 device\n\n Raw NAND:\n - Clean up the Qualcomm controller driver again\n - Improve / create Arasan and IBM\n - Reflect that ESMT and GigaDevice share ID 0xc8, and disable broken\n timings feature on GD9FU2G8F3A\n - Drop platform data support from the lpc32xx drivers\n\n SPI NAND:\n - Heyangtek uses two bytes for the BBM like every other device\n - Winbond: Add support for W25N04LW and W25N08LW\n - Support for ISSI SPI NAND flashes\n\n SPI NOR:\n - Support for new Macronix and (lots of) Winbond flash devices\n - Rewrite the status register handling. There were many small helper\n functions to enable quad I/O mode in many different ways. That's\n now consolidated in one generic function.\n - Rework the SFPD handling. It used to be only parsed for flashes\n present in the in-kernel database which had any of the multi-I/O\n bit set - or with the addition of generic flash driver for any\n unknown flashes. This left a gap where SFDP wasn't handled and it\n turns out newer flashes (with SFDP support) will reused IDs of just\n these old flashed. With the rework, SFDP is tried to be parsed for\n any flashes and will amend the information which is already present\n in the in-kernel database.\n - Increase SFDP size limit to 16KiB. There are flashes which have\n some larger gaps between tables and thus exceeds the former 4KiB\n limit.\n - Flash fixups are getting more and more common. Due to this, the\n fixups are now a separate list (per manufacturer) instead of being\n attached to a specific flash. A new .match() callback makes them\n more flexible, when one is applied.\n - Support 1-1-8 and 1-8-8 modes with larger flashes which need 4byte\n addresses\n\n Then there is the load of API updates and misc AI fixes\"\n\n* tag 'mtd/for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/mtd/linux: (155 commits)\n mtd: spi-nor: sfdp: reduce stack usage in spi_nor_parse_sfdp()\n mtd: spi-nor: sfdp: fix missing kernel-doc\n mtd: parsers: qcom: fix offset/size overflow on large partitions\n mtd: spinand: heyangtek: Reserve both bad block marker bytes\n mtd: rawnand: pl353: fix clk reference leak in pl35x_nfc_setup_interface()\n mtd: nand: use dmaengine_submit() instead of ->tx_submit()\n dt-bindings: mtd: convert IBM NDFC binding to YAML\n mtd: rawnand: fsl_ifc: allocate shared ctrl with devm_kzalloc\n mtd: rawnand: qcom: fix read location register usage in config_nand_cw_read()\n mtd: rawnand: qcom: move common code to the 'qpic_common' module\n mtd: rawnand: qcom: rename 'nand_stats' structure\n mtd: rawnand: qcom: make nandc_set_read_loc_last() more generic\n mtd: rawnand: qcom: make nandc_set_read_loc_first() more generic\n mtd: rawnand: qcom: make config_nand_cw_write() more generic\n mtd: rawnand: qcom: make config_nand_page_write() more generic\n mtd: rawnand: qcom: make config_nand_page_read() more generic\n mtd: rawnand: qcom: make check_flash_errors() more generic\n dt-bindings: mtd: arasan: Add \"power-domains\" for ZynqMP\n mtd: virt-concat: unlink discarded items before freeing\n mtd: virt-concat: unlink concat node before freeing it\n ...",
"tree": {
"sha": "77c4a6f6237cdb483cc471a01cc500406fdcd553",
"url": "https://api.github.com/repos/torvalds/linux/git/trees/77c4a6f6237cdb483cc471a01cc500406fdcd553"
},
"url": "https://api.github.com/repos/torvalds/linux/git/commits/44388c8f3e22b578c9089df53301ef7e53e7fb01",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/torvalds/linux/commits/44388c8f3e22b578c9089df53301ef7e53e7fb01",
"html_url": "https://github.com/torvalds/linux/commit/44388c8f3e22b578c9089df53301ef7e53e7fb01",
"comments_url": "https://api.github.com/repos/torvalds/linux/commits/44388c8f3e22b578c9089df53301ef7e53e7fb01/comments",
"author": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "a8c2facb7b2cef4d110e3b25037bc29703485c71",
"url": "https://api.github.com/repos/torvalds/linux/commits/a8c2facb7b2cef4d110e3b25037bc29703485c71",
"html_url": "https://github.com/torvalds/linux/commit/a8c2facb7b2cef4d110e3b25037bc29703485c71"
},
{
"sha": "0ef71adafaa9b36a06dd5b823aebcd2fa9b9b9c2",
"url": "https://api.github.com/repos/torvalds/linux/commits/0ef71adafaa9b36a06dd5b823aebcd2fa9b9b9c2",
"html_url": "https://github.com/torvalds/linux/commit/0ef71adafaa9b36a06dd5b823aebcd2fa9b9b9c2"
}
]
},
{
"sha": "a8c2facb7b2cef4d110e3b25037bc29703485c71",
"node_id": "C_kwDOACN7MtoAKGE4YzJmYWNiN2IyY2VmNGQxMTBlM2IyNTAzN2JjMjk3MDM0ODVjNzE",
"commit": {
"author": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T05:44:48Z"
},
"committer": {
"name": "Linus Torvalds",
"email": "torvalds@linux-foundation.org",
"date": "2026-10-25T05:44:48Z"
},
"message": "Merge tag 'pinctrl-v7.4-1' of git://git.kernel.org/pub/scm/linux/kernel/git/linusw/linux-pinctrl\n\nPull pin control updates from Linus Walleij:\n \"Nothing special really, some new drivers, the usual trickle of fixes.\n\n Oddly not even vary many LLM-assisted fixes, I guess they haven't\n found their way into this subsystem yet.\n\n Core changes:\n\n - Support input-debounce-ns pin config property.\n\n New drivers:\n\n - Qualcomm:\n - Hawi SoC TLMM and LPASS LPI TLMM\n - Kuno (ARM32!) SoC TLMM\n - MSM8952 SoC TLMM\n - PMAU0102 SPMIO GPIO\n\n - Starfive JHB100 SoC sys0, sys0h, sys1, sys2, per0, per1,\n per2 per2pok and per3 (apparently strongly modularized silicon)\n\n - Ambarella CV75\n\n - Apple T8140 SoC used in iPhone 16 [Pro] and MacBook Neo\n\n - Econet EN7528\n\n - Allwinner Sunxi A733\n\n - Mediatek MT6735\n\n Improvements:\n\n - Renesas RZ/3GL: SD channel PoC, I2C power source etc\n\n - The SCMI pin controller uses generic helpers for handling\n groups and functions\"\n\n* tag 'pinctrl-v7.4-1' of git://git.kernel.org/pub/scm/linux/kernel/git/linusw/linux-pinctrl: (106 commits)\n pinctrl: stm32: program the EXTI mux from .alloc instead of .activate\n pinctrl: stm32: Fix the unit of the hwspinlock timeout\n pinctrl: stm32: Fix fwnode reference leak in stm32_gpiolib_register_bank()\n dt-bindings: pinctrl: brcm,nsp-gpio: Convert to DT schema\n pinctrl: mediatek: Add MT6735 pinctrl driver\n dt-bindings: pinctrl: mediatek,mt6779-pinctrl: Add bindings for MT6735 pin controller\n dt-bindings: pinctrl: mediatek,mt6779-pinctrl: Document MT6765 pin controller\n dt-bindings: pinctrl: mediatek,mt6779-pinctrl: Add MT6795\n dt-bindings: pinctrl: mediatek,mt6779-pinctrl: Improve pinctrl subnode and property descriptions\n dt-bindings: pinctrl: mediatek,mt6779-pinctrl: Pull pinctrl node changes from MT6795 document\n pinctrl: tegra: xusb: free maps on DT map failure\n pinctrl: sprd: free maps on DT map failure\n pinctrl: kconfig: Select generic pinctrl helpers for scmi pinctrl\n pinctrl: scmi: Replace pinctrl ops get group info with generics\n pinctrl: scmi: Replace pinmux ops get function info with generics\n pinctrl: ambarella: fix NULL vs IS_ERR() bug in amb_pinctrl_register()\n pinctrl: starfive: fix missing CONFIG_OF dependencies\n pinctrl: ambarella: add CONFIG_OF dependency\n pinctrl: apple: Add t8140-pinctrl support\n dt-bindings: pinctrl: apple,pinctrl: Add t8140 compatible\n ...",
"tree": {
"sha": "7de712b46fddd7bddc14c3ee2a0425a22b38badc",
"url": "https://api.github.com/repos/torvalds/linux/git/trees/7de712b46fddd7bddc14c3ee2a0425a22b38badc"
},
"url": "https://api.github.com/repos/torvalds/linux/git/commits/a8c2facb7b2cef4d110e3b25037bc29703485c71",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/torvalds/linux/commits/a8c2facb7b2cef4d110e3b25037bc29703485c71",
"html_url": "https://github.com/torvalds/linux/commit/a8c2facb7b2cef4d110e3b25037bc29703485c71",
"comments_url": "https://api.github.com/repos/torvalds/linux/commits/a8c2facb7b2cef4d110e3b25037bc29703485c71/comments",
"author": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "torvalds",
"id": 1024025,
"node_id": "MDQ6VXNlcjEwMjQwMjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/1024025?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/torvalds",
"html_url": "https://github.com/torvalds",
"followers_url": "https://api.github.com/users/torvalds/followers",
"following_url": "https://api.github.com/users/torvalds/following{/other_user}",
"gists_url": "https://api.github.com/users/torvalds/gists{/gist_id}",
"starred_url": "https://api.github.com/users/torvalds/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/torvalds/subscriptions",
"organizations_url": "https://api.github.com/users/torvalds/orgs",
"repos_url": "https://api.github.com/users/torvalds/repos",
"events_url": "https://api.github.com/users/torvalds/events{/privacy}",
"received_events_url": "https://api.github.com/users/torvalds/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "a32e63a613aa80dfdb9acc0a09efab940e96054f",
"url": "https://api.github.com/repos/torvalds/linux/commits/a32e63a613aa80dfdb9acc0a09efab940e96054f",
"html_url": "https://github.com/torvalds/linux/commit/a32e63a613aa80dfdb9acc0a09efab940e96054f"
},
{
"sha": "f33a24cc012d34f25bb9942681bd478a1757b806",
"url": "https://api.github.com/repos/torvalds/linux/commits/f33a24cc012d34f25bb9942681bd478a1757b806",
"html_url": "https://github.com/torvalds/linux/commit/f33a24cc012d34f25bb9942681bd478a1757b806"
}
]
}
]
+2 -1
View File
@@ -1,6 +1,7 @@
# torvalds/linux — 5 most recent commits
Source: https://api.github.com/repos/torvalds/linux/commits?per_page=5
Provenance: https://api.github.com/repos/torvalds/linux/commits?per_page=5 fetched 2026-10-25T21:53:09Z (5 records, per_page=5)
Rows: 5
| # | SHA | Date | Subject |
@@ -8,5 +9,5 @@ Rows: 5
| 1 | 21a7f44fd0 | 2026-10-25 | Merge tag 'devicetree-for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/robh/linux |
| 2 | 89d2d7bf06 | 2026-10-25 | Merge tag 'efi-next-for-v7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/efi/efi |
| 3 | 9628ee0c22 | 2026-10-25 | Merge tag 'gpio-fixes-for-v7.4-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/brgl/linux |
| 4 | 44388c8f3e | 2026-10-25 | Merge tag 'mtd/for-7.4' of git://git.kernel.org/pub/scm/linux/kernel_git/mtd/linux |
| 4 | 44388c8f3e | 2026-10-25 | Merge tag 'mtd/for-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/mtd/linux |
| 5 | a8c2facb7b | 2026-10-25 | Merge tag 'pinctrl-v7.4-1' of git://git.kernel.org/pub/scm/linux/kernel/git/linusw/linux-pinctrl |
+352
View File
@@ -0,0 +1,352 @@
#!/usr/bin/env python3
"""Verification for `commits-table.md` (ledger / Extract lane).
The table names its own source on a `Source:` line. This gate re-reads that
line, fetches *that* endpoint, and grades every row against the response body
the endpoint returns now -- so the artifact is tied to its data by a
measurement rather than by the sentence that claims it.
What is graded (rc 1 on any failure):
TABLE the document parses: a `Source:` URL, a `Rows: N` claim, a four
column header over N body rows, 4 cells per row, no unescaped
pipe (which is how a markdown table silently loses a cell).
FEED the response body is a JSON array of commit objects.
RESOLVE every row's SHA prefix is >= 7 lowercase hex characters and is a
prefix of exactly one SHA in the feed (ambiguous -> fault).
SUBJECT the row's Subject equals the feed's first message line, byte for
byte. This is the leg that catches a hand-retyped URL inside a
merge subject -- one character, invisible to every reader who
does not re-fetch, indistinguishable from a fabricated path.
DATE the row's Date equals the feed's *author* date, day precision.
The column is day-precision by construction, so a within-day
author/committer difference is printed as an observation.
ORDER rows appear in feed order (row i's feed index strictly increases
with i), i.e. the table is most-recent-first as its title says.
PROVENANCE the `Provenance:` line names the same URL as `Source:`, a UTC
fetch timestamp, the record count the body rows imply, and the
per_page the URL asked for. Q62 leg (c)'s lesson: an artifact
that names its source is not the same as one that can be audited.
RECENCY OBSERVED by default, not graded: whether these rows are still the
N most recent commits at the endpoint. A committed file cannot be
abreast of a moving feed, and a leg that goes red for every reader
forever is a leg everyone learns to ignore. `--strict-recency`
promotes it to a graded leg for a periodic sweep.
`--feed PATH` grades against a retained response body instead of the network
(this is how the lane gate runs it: hermetic, no network in CHECK). The
`--record PATH` flag writes whatever body was fetched to PATH, which is how a
retained body is pinned in the first place.
House exit contract (the same one `tools/` uses): 0 = every graded leg passed,
1 = a graded leg failed, 2 = could not grade (missing table, unparsable body,
transport failure). No asserts, so `-O` cannot delete a check.
Every path is resolved from `__file__`; nothing here names the author's
checkout, so the gate grades the commit it sits in and not the tree it was
written in.
"""
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import NoReturn
HERE = Path(__file__).resolve().parent
TABLE = HERE / "commits-table.md"
COLUMNS = ["#", "SHA", "Date", "Subject"]
MIN_PREFIX = 7
# Resolution depth. The artifact cites per_page=5; asking for 100 only widens
# the window rows are resolved in, so a row that has since been pushed down the
# feed is still identifiable instead of looking invented.
RESOLVE_DEPTH = 100
TIMEOUT = 40
GRADED = []
def say(ok, leg, detail=""):
print("[%s] %-10s %s" % ("PASS" if ok else "FAIL", leg, detail))
if not ok:
GRADED.append(leg)
def observe(leg, detail):
print("[INFO] %-10s %s" % (leg, detail))
def die(code, msg) -> NoReturn:
print("UNGRADABLE: %s" % msg)
sys.exit(code)
# ---------------------------------------------------------------- document --
def read_table(path):
if not path.is_file():
die(2, "no table at %s" % path)
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
die(2, "cannot read %s: %s" % (path, exc))
return text
def parse_table(text):
"""Return dict(url, rows_claim, rows). Rows are 4-cell lists; a row whose
cell count is not 4 is kept (so it can be named) with `cells` recording it.
"""
url = None
for line in text.split("\n"):
m = re.match(r"^Source:\s*(\S+)\s*$", line)
if m:
url = m.group(1)
break
m = re.search(r"^Rows:\s*(\d+)\s*$", text, re.MULTILINE)
rows_claim = int(m.group(1)) if m else None
rows, header = [], None
for line in text.split("\n"):
s = line.strip()
if not s.startswith("|"):
continue
cells = [c.strip() for c in s[1:-1].split("|")] if s.endswith("|") else None
if cells is None:
continue
if header is None:
header = cells
continue
if all(re.fullmatch(r":?-{2,}:?", c) for c in cells):
continue # separator
if re.fullmatch(r"\d+", cells[0]):
rows.append(cells)
return {"url": url, "rows_claim": rows_claim, "header": header, "rows": rows}
def grade_document(doc):
say(doc["url"] is not None, "TABLE", "Source: %s" % doc["url"] if doc["url"] else "no `Source:` line")
if doc["url"]:
parsed = urllib.parse.urlsplit(doc["url"])
say(parsed.scheme == "https" and bool(parsed.netloc), "TABLE", "source is an https URL")
say(doc["header"] == COLUMNS, "TABLE", "header %r" % (doc["header"],))
say(doc["rows_claim"] is not None, "TABLE", "Rows: %s" % doc["rows_claim"])
say(doc["rows_claim"] == len(doc["rows"]), "TABLE",
"Rows: %s vs %d body rows" % (doc["rows_claim"], len(doc["rows"])))
bad = [(r[0], len(r)) for r in doc["rows"] if len(r) != len(COLUMNS)]
say(not bad, "TABLE", "cell counts %s" % (bad or "all %d" % len(COLUMNS)))
say(len(doc["rows"]) > 0, "TABLE", "%d data row(s)" % len(doc["rows"]))
# ------------------------------------------------------------------- feed ---
def fetch(url):
sep = "&" if "?" in url else "?"
deep = "%s%sper_page=%d" % (url, sep, RESOLVE_DEPTH) if "per_page=" not in url else re.sub(
r"per_page=\d+", "per_page=%d" % RESOLVE_DEPTH, url)
req = urllib.request.Request(deep, headers={
"User-Agent": "ledger-commits-table-gate/1.0",
"Accept": "application/vnd.github+json",
})
try:
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
body = r.read()
status = r.status
except urllib.error.HTTPError as exc:
die(2, "HTTP %s from %s" % (exc.code, deep))
except (urllib.error.URLError, OSError) as exc:
die(2, "transport failure for %s: %s" % (deep, exc))
if status != 200:
die(2, "HTTP %s from %s" % (status, deep))
observe("FEED", "fetched %s (%d bytes, per_page=%d)" % (deep, len(body), RESOLVE_DEPTH))
return body, deep
def load_feed(body, origin):
try:
feed = json.loads(body)
except (ValueError, UnicodeDecodeError) as exc:
die(2, "response body is not JSON (%s): %s" % (origin, exc))
if not isinstance(feed, list) or not feed:
die(2, "response body is not a non-empty JSON array (%s)" % origin)
for i, rec in enumerate(feed):
try:
rec["sha"], rec["commit"]["message"], rec["commit"]["author"]["date"]
except (KeyError, TypeError):
die(2, "record %d lacks sha/commit.message/commit.author.date (%s)" % (i, origin))
return feed
# ------------------------------------------------------------------ grading --
def record_view(rec):
msg = rec["commit"]["message"]
subject = msg.split("\n", 1)[0]
author = rec["commit"]["author"]["date"]
committer = rec["commit"].get("committer", {}).get("date", author)
return {"sha": rec["sha"], "subject": subject, "author": author, "committer": committer}
def grade_rows(doc, feed):
views = [record_view(r) for r in feed]
index_of = {}
for i, v in enumerate(views):
index_of.setdefault(v["sha"], i)
resolved = []
for row in doc["rows"]:
num, prefix = row[0], row[1]
if not re.fullmatch(r"[0-9a-f]{%d,}" % MIN_PREFIX, prefix):
say(False, "RESOLVE", "row %s: %r is not a >=%d-char lowercase hex prefix" % (num, prefix, MIN_PREFIX))
resolved.append(None)
continue
hits = [i for i, v in enumerate(views) if v["sha"].startswith(prefix)]
if len(hits) != 1:
say(False, "RESOLVE", "row %s: %r matches %d feed SHAs (need exactly 1)" % (num, prefix, len(hits)))
resolved.append(None)
continue
resolved.append(hits[0])
ok = [r for r in resolved if r is not None]
say(len(ok) == len(resolved), "RESOLVE",
"%d/%d rows resolve to exactly one feed SHA" % (len(ok), len(resolved)))
for row, idx in zip(doc["rows"], resolved):
if idx is None:
continue
v = views[idx]
num, _, date, subject = row[0], row[1], row[2], row[3]
if subject != v["subject"]:
say(False, "SUBJECT", "row %s differs from feed %s" % (num, v["sha"][:10]))
if len(subject) == len(v["subject"]):
k = next((j for j in range(len(subject)) if subject[j] != v["subject"][j]), None)
if k is not None:
print(" first difference at character %d" % k)
print(" table: ...%s..." % subject[max(0, k - 24):k + 25])
print(" feed : ...%s..." % v["subject"][max(0, k - 24):k + 25])
else:
print(" table: %r" % subject)
print(" feed : %r" % v["subject"])
continue
say(True, "SUBJECT", "row %s == %s" % (num, v["sha"][:10]))
day = v["author"][:10]
say(date == day, "DATE", "row %s: table %s vs feed author %s" % (num, date, v["author"]))
if v["committer"][:10] != v["author"][:10]:
observe("DATE", "row %s author %s and committer %s fall on different days" % (num, v["author"], v["committer"]))
seq = [r for r in resolved if r is not None]
say(seq == sorted(set(seq)) and len(seq) == len(set(seq)), "ORDER",
"row order vs feed index %s" % (seq,))
return resolved, views
def grade_provenance(text, doc):
m = re.search(r"^Provenance:\s+(\S+)\s+fetched\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"
r"\s+\((\d+)\s+records?,\s*per_page=(\d+)\)\s*$", text, re.MULTILINE)
if not m:
say(False, "PROVENANCE", "no `Provenance: <url> fetched <UTC> (<n> records, per_page=<k>)` line")
return
url, stamp, records, per_page = m.group(1), m.group(2), int(m.group(3)), int(m.group(4))
say(url == doc["url"], "PROVENANCE", "names the same endpoint as Source: (%s)" % url)
try:
datetime.strptime(stamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
parsed = True
except ValueError:
parsed = False
say(parsed, "PROVENANCE", "fetch timestamp %s parses as UTC" % stamp)
say(records == len(doc["rows"]), "PROVENANCE",
"%d records claimed for %d body rows" % (records, len(doc["rows"])))
m2 = re.search(r"per_page=(\d+)", doc["url"] or "")
say(bool(m2) and per_page == int(m2.group(1)), "PROVENANCE",
"per_page=%d matches the Source URL's per_page=%s" % (per_page, m2.group(1) if m2 else "absent"))
observe("PROVENANCE", "captured %s" % stamp if parsed else "captured (unparsed)")
def grade_recency(doc, resolved, strict):
n = len(doc["rows"])
top = list(range(n))
seq = [r for r in resolved if r is not None]
if seq == top:
observe("RECENCY", "rows are the %d most recent records at the endpoint" % n)
if strict:
say(True, "RECENCY", "rows are still the top %d" % n)
return
if strict:
say(False, "RECENCY", "rows are not the top %d (%s)" % (n, seq))
else:
newest = seq[0] if seq else None
observe("RECENCY", "rows are not the top %d at this reading (row 1 sits at feed index %s, "
"so %s commit(s) have landed since capture); not graded, "
"pass --strict-recency to grade it"
% (n, newest, newest))
# --------------------------------------------------------------------- main --
def main(argv):
table_path, feed_path, record_path = TABLE, None, None
strict = False
i = 0
while i < len(argv):
a = argv[i]
if a == "--table" and i + 1 < len(argv):
table_path = Path(argv[i + 1]).expanduser()
i += 2
elif a == "--feed" and i + 1 < len(argv):
feed_path = Path(argv[i + 1]).expanduser()
i += 2
elif a == "--record" and i + 1 < len(argv):
record_path = Path(argv[i + 1]).expanduser()
i += 2
elif a == "--strict-recency":
strict = True
i += 1
elif a in ("-h", "--help"):
print(__doc__)
return 0
else:
print("unknown argument: %r" % a, file=sys.stderr)
return 2
text = read_table(table_path)
doc = parse_table(text)
grade_document(doc)
if not doc["url"]:
die(2, "the document names no source, so there is nothing to fetch")
if feed_path is not None:
if not feed_path.is_file():
die(2, "no retained feed body at %s" % feed_path)
try:
body = feed_path.read_bytes()
except OSError as exc:
die(2, "cannot read %s: %s" % (feed_path, exc))
origin = "retained body %s" % feed_path.name
observe("FEED", "%s (%d bytes)" % (origin, len(body)))
else:
body, origin = fetch(doc["url"])
if record_path is not None:
try:
record_path.write_bytes(body)
observe("FEED", "wrote %d bytes to %s" % (len(body), record_path))
except OSError as exc:
die(2, "cannot write %s: %s" % (record_path, exc))
feed = load_feed(body, origin)
observe("FEED", "%d record(s) in %s" % (len(feed), origin))
resolved, views = grade_rows(doc, feed)
grade_provenance(text, doc)
grade_recency(doc, resolved, strict)
if GRADED:
print("TABLE FAIL: %s" % ", ".join(GRADED))
return 1
print("TABLE PASS: %d row(s) reproduce from %s byte for byte."
% (len(doc["rows"]), "the retained body" if feed_path else "a live fetch"))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))