From 80c04adc7e5a579596570c957f64840c03f21575 Mon Sep 17 00:00:00 2001 From: Antonios Voulvoulis Date: Tue, 29 Sep 2026 14:00:32 +0300 Subject: [PATCH] Payload gate: compare documentation by path, not basename The first attested release run for 0.1.0 (workflow_dispatch on public main c16c46b, run 36557987409) stopped at the package payload gate, before any attestation: "deb and rpm ship DIFFERENT documentation". Both packages ship the same nine files. The gate compared basenames, and 0.1.0 installs its guides in docs/: tar lists that directory as "docs/" (an empty basename), rpm as "docs". A basename comparison also could never see a guide installed in the wrong directory. The gate now compares paths relative to /usr/share/doc/isedraf, regular files only (tar directory entries end in "/", rpm reports modes). On the GA packages: "deb and rpm ship the same 9 documentation files". Falsification: "D-86 the rpm and the deb ship different documentation" did not commit its mutation, so build.sh refused the dirty tree and the gate's "no packages" failure matched its loose evidence - detection for the wrong reason. It now commits first, like the other packaging injections, and requires the real evidence. New: "D-86 the rpm installs a guide outside docs/", which the basename comparison could not detect. Both were shown to pass on real builds and fail on the mutation in harness-style sandboxes. Assisted-by: Claude (implementation agent, Claude Code) Signed-off-by: Antonios Voulvoulis --- docs/CURRENT_STATE.md | 2 +- .../generated/10_falsification_coverage.md | 4 ++-- scripts/ci/check_package_payload.sh | 12 ++++++++---- scripts/ci/falsifiable.sh | 12 ++++++++++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index 9e5ea3a..32d4ebb 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -112,7 +112,7 @@ Not asserted. Each number is counted at generation time. | | | |---|---| | Gates | 28 | -| Falsification injections | 372 | +| Falsification injections | 373 | | Golden vector cases | 15 | | Frozen artifacts | 7 | | Test files | 33 | diff --git a/docs/development/architecture/generated/10_falsification_coverage.md b/docs/development/architecture/generated/10_falsification_coverage.md index d7acf07..302c12b 100644 --- a/docs/development/architecture/generated/10_falsification_coverage.md +++ b/docs/development/architecture/generated/10_falsification_coverage.md @@ -21,8 +21,8 @@ GENERATED from `scripts/ci/falsifiable.sh`. A large total is not coverage if mos | architecture | 7 | | privacy / disclosure | 27 | | licensing / framework | 21 | -| packaging / release | 9 | +| packaging / release | 10 | | governance / gates | 8 | | docs truth | 7 | | other / cross-cutting | 209 | -| **total** | **371** | +| **total** | **372** | diff --git a/scripts/ci/check_package_payload.sh b/scripts/ci/check_package_payload.sh index 26c42a3..b34a39e 100755 --- a/scripts/ci/check_package_payload.sh +++ b/scripts/ci/check_package_payload.sh @@ -56,11 +56,15 @@ if [ -n "$DEB" ] && [ -n "$RPM" ] && command -v rpm >/dev/null 2>&1; then # .rpm states the same thing in its `License:` metadata field instead. Requiring both # formats to carry both mechanisms would be parity for its own sake. Everything else # must still match exactly. + # Paths relative to the doc directory, regular files only. Basenames were compared + # until 0.1.0 shipped its guides in docs/: tar lists that directory as "docs/" and + # rpm as "docs", so identical payloads compared unequal - and a basename comparison + # could never see a guide installed in the wrong directory. ar p "$DEB" data.tar.gz 2>/dev/null | tar tz 2>/dev/null \ - | grep 'usr/share/doc/isedraf/.' | sed 's|.*/||' | grep -v '^copyright$' \ - | sort -u > "$D/deb" - rpm -qlp "$RPM" 2>/dev/null \ - | grep 'usr/share/doc/isedraf/.' | sed 's|.*/||' | sort -u > "$D/rpm" + | sed -n 's|^\./||; s|^usr/share/doc/isedraf/||p' | grep -v '/$' \ + | grep -v '^copyright$' | grep -v '^$' | sort -u > "$D/deb" + rpm -qp --qf '[%{FILEMODES:perms} %{FILENAMES}\n]' "$RPM" 2>/dev/null \ + | sed -n 's|^-[^ ]* /usr/share/doc/isedraf/||p' | sort -u > "$D/rpm" if cmp -s "$D/deb" "$D/rpm"; then ok "deb and rpm ship the same $(wc -l < "$D/deb" | tr -d ' ') documentation files" else diff --git a/scripts/ci/falsifiable.sh b/scripts/ci/falsifiable.sh index e5dade9..ebcff04 100755 --- a/scripts/ci/falsifiable.sh +++ b/scripts/ci/falsifiable.sh @@ -2317,7 +2317,7 @@ inject "D-114 the retired DEVICE_SOLID_STATE constant returns to the engine" \ # D-86. The deb and the rpm are built by two different implementations. Dropping a # document from one of them must be caught by comparing them, not by anyone remembering. inject "D-86 the rpm and the deb ship different documentation" \ - 'bash packaging/build.sh 2>&1; bash scripts/ci/check_package_payload.sh' \ + 'git add -A >/dev/null 2>&1; git -c user.name=falsifiable -c user.email=falsifiable@invalid commit -qm mutation >/dev/null 2>&1; bash packaging/build.sh 2>&1; bash scripts/ci/check_package_payload.sh' \ 'python3 - <<'"'"'PYX'"'"' import pathlib p = pathlib.Path("packaging/rpm/isedraf.spec.in"); s = p.read_text() @@ -2326,7 +2326,15 @@ assert old in s, "mutation anchor miss" head, sep, tail = s.partition(old) p.write_text(head + tail.split("\n", 1)[1].split("\n", 1)[1]) PYX' \ - 'ship DIFFERENT documentation|package payload gate FAILED' + 'ship DIFFERENT documentation' + +# The comparison was by basename until the guides moved into docs/ (0.1.0, 2026-09-29): it +# failed identical payloads, and it could never see a guide installed in the wrong +# directory. It now compares paths relative to the doc directory. +inject "D-86 the rpm installs a guide outside docs/" \ + 'git add -A >/dev/null 2>&1; git -c user.name=falsifiable -c user.email=falsifiable@invalid commit -qm mutation >/dev/null 2>&1; bash packaging/build.sh 2>&1; bash scripts/ci/check_package_payload.sh' \ + 'sed -i "s|%{_docdir}/isedraf/docs/\$guide.md|%{_docdir}/isedraf/\$guide.md|" packaging/rpm/isedraf.spec.in' \ + 'ship DIFFERENT documentation' # PUBLIC-OPS-001 structured. YAML sat outside the prose scan because prose patterns fire # on every legitimate `permissions: contents: read`. But YAML carries exactly what the