ci: fix two things the baseline control run found
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m52s

Run 4960 was the control — the chain compared against itself, which must
come back clean before a clean diff after the squash means anything. It
did its job and failed on both counts.

1. The harness is sound. Both dumps came back 1123 normalised lines and
   differed on EXACTLY two, the \restrict / \unrestrict pair that newer
   pg_dump emits to fence a dump against injection during restore. It is
   a fresh random nonce per invocation, so it differs by construction and
   is noise by definition. Now filtered — and the control is what
   licenses that filter: it was OBSERVED to be the only false positive
   rather than assumed to be one, which matters for a check whose whole
   value is that its normalisation does not hide a real difference.

2. The candidate-baseline step never ran. `if: github.event.inputs
   .generate == 'true'` on a `type: boolean` input silently evaluated
   false — no diagnostic, step skipped, job carried on. The same
   `github.event.inputs` typing quirk build.yml already works around for
   force_build.

   Rather than fight the input typing, the gate is now the tree itself:
   skip if alembic/versions holds one file. That is the real question
   anyway — there is nothing to generate once the chain is collapsed —
   and it cannot be silently wrong the way an unevaluated expression can.

Worth noting what the control also proved incidentally: the two schemas
were byte-identical across 1123 lines despite being built by separate
alembic runs into separate databases, so pg_dump's object ordering is
stable enough to diff directly and no sort normalisation is needed.
This commit is contained in:
2026-08-30 13:35:26 -04:00
parent 62583791d8
commit 5fd171a544
+33 -17
View File
@@ -17,9 +17,10 @@
# still has them. That is what makes this the proof for step 1 and the
# pre-flight for step 2, rather than a one-shot script.
#
# `generate: true` additionally autogenerates a candidate baseline from the
# models and uploads it. That is a starting point, NOT the answer: autogenerate
# reads SQLAlchemy metadata, and three things in this schema do not live there —
# While the chain is still present it also autogenerates a candidate baseline
# from the models and prints it. That is a starting point, NOT the answer:
# autogenerate reads SQLAlchemy metadata, and three things here do not live
# there —
# * CREATE EXTENSION vector (0001)
# * CREATE EXTENSION tsm_system_rows (0004)
# * the HNSW index on image_record.siglip_embedding, which is raw SQL
@@ -36,10 +37,6 @@ on:
description: 'Commit/tag that still carries the full 0001..0087 chain'
type: string
default: '0a5bbe8'
generate:
description: 'Also autogenerate a candidate baseline from the models'
type: boolean
default: false
jobs:
compare:
@@ -111,13 +108,23 @@ jobs:
--no-owner --no-privileges -d fc_chain > chain.sql
wc -l chain.sql
# Optional: a candidate baseline, autogenerated from the models against an
# EMPTY database so every table shows up as a create. Uploaded for a human
# to finish — it will be missing the three raw-SQL items named at the top.
# A candidate baseline, autogenerated from the models against an EMPTY
# database so every table shows up as a create. Printed for a human to
# finish — it will be missing the three raw-SQL items named at the top.
#
# Gated on the TREE, not on a workflow input. A `type: boolean` input
# read back as `github.event.inputs.generate == 'true'` silently
# evaluated false on this runner (run 4960 skipped this step entirely
# with no diagnostic) — the same `github.event.inputs` typing quirk
# build.yml already works around. The file count is the real question
# anyway: there is nothing to generate once the chain is collapsed.
- name: Autogenerate a candidate baseline
if: ${{ github.event.inputs.generate == 'true' }}
run: |
set -eux
if [ "$(ls alembic/versions/*.py | wc -l)" -le 1 ]; then
echo "already collapsed — nothing to generate"
exit 0
fi
docker exec "$PG_CONTAINER" createdb -U fabledcurator fc_gen
# Hide the existing revisions so alembic sees an empty history and
# emits the whole schema rather than a delta.
@@ -153,16 +160,25 @@ jobs:
#
# pg_dump orders dumpable objects by name within type, not by creation
# order, so two schemas built by different routes are directly
# comparable. The only normalisation applied is dropping blank lines,
# comment lines and the alembic_version row-count noise — deliberately
# minimal, because a filter that hides a real difference is the one way
# this check passes when it should fail. Whatever is normalised is
# printed, so the filtering itself is reviewable.
# comparable. Normalisation is deliberately minimal, because a filter
# that hides a real difference is the one way this check passes when it
# should fail — blank lines, SQL comments, trailing whitespace, and:
#
# \restrict / \unrestrict — a per-invocation RANDOM NONCE that newer
# pg_dump emits to fence the dump against injection during restore. It
# differs on every run by construction, so it is noise by definition,
# not a schema difference. Measured on run 4960, the control: two dumps
# of the SAME schema came back 1123 lines each and differed on exactly
# these two lines and nothing else. That control is what licenses this
# filter — it was observed to be the only false positive, rather than
# assumed to be one.
- name: Diff
run: |
set -eu
norm() {
grep -vE '^\s*(--|$)' "$1" | sed 's/[[:space:]]*$//'
grep -vE '^\s*(--|$)' "$1" \
| grep -vE '^\\(un)?restrict ' \
| sed 's/[[:space:]]*$//'
}
norm chain.sql > a.txt
norm baseline.sql > b.txt