Repository navigation
Release: reproducible build identity and release manifest (HOM-28, HOM-29) - #50
Merged
Merged
Conversation
…M-29) infra/release/release_manifest.py (standard library only) describes one commit from Git objects (git cat-file / ls-tree, GIT_* variables and replace refs ignored), never the working tree: source sha, tree and commit time, version, Python, base image and uv version, digests of uv.lock, pyproject, backend/requirements.txt and the console and website locks, the single migration head and revision count (several heads or a broken chain are refused), Dockerfile and .dockerignore digests and base images of every image, the digest of the code the API image ships, profile, image digests and validation links passed in, requires-python and what is pinned. Canonical JSON, no wall clock (build time = commit time unless given), manifest_sha256 over the body. build-args prints the API build arguments of the commit; verify compares a manifest with an image's build-info.json and OCI labels and lists every mismatch. API image: build args SOURCE_SHA, SOURCE_DATE_EPOCH, SOURCE_DATE, RELEASE_VERSION, RELEASE_PROFILE (must agree with INSTALL_AI_LOCAL); OCI labels revision/version/created/source/base.name; uv.lock and pyproject copied to /app/release-inputs and build-info.json written at build time; base image pinned by the digest of its multi-platform index; uv sync --locked (also in the backend CI job). Builds without release arguments keep working and carry the all-zero SOURCE_SHA: homayar_build_info reports source_sha="unreleased" and verify fails. build-info.json now also records the base image reference, uv version and a digest of the shipped code files (alembic.ini and backend/ without tests, environments and caches), so a dirty build context fails verify. CI job release-manifest: manifest generated twice and byte-compared, image built with the commit's release arguments, build-info and labels verified, /metrics of the running image checked, manifest uploaded as an artifact. Input-reproducible, not bit-for-bit: apt packages, timestamps and layer digests are not pinned. docs/operations/release.md, ADR 0036 decision 5.
The build_info row no longer says the values stay unknown until HOM-28: images write build-info.json at build time, an image built without release arguments reports source_sha="unreleased", and only a process outside an image reports unknown.
…nto backend/hom-28-29-release
…authorship (HOM-29) manifest_sha256 is unkeyed, so whoever edits a manifest can recompute it: release.md, ADR 0036 decision 5 and the tool docstring now say integrity checksum, not proof of authorship. Every manifest's not_pinned list adds "not signed / no provenance attestation; images are local ids, not registry digests". release.md lists the known gaps: verify does not compare the installed distribution set, uv is installed by version not hash, the on-prem Compose PYTHON_IMAGE default is a tag, and SOURCE_DATE_EPOCH does not control layer file timestamps.
Co-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>
…k (HOM-29) main removed the legacy public site (#75) with infra/docker/public-web.Dockerfile and the root pnpm-lock.yaml, so generate refused every revision after it. The manifest now covers the api, console-web and egress-proxy images and the website/pnpm-lock.yaml lock. Co-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>
Kohandezh
marked this pull request as ready for review
October 19, 2026 08:55
Conflict in docs/operations/ci.md: keep main's console end-to-end row and this branch's Backend (uv sync --locked) and Release manifest rows. Co-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>
ADR 0036 conflict: keep both sides in numeric order (decision 4 alerting from main, then decision 5 release). The manifest is unkeyed, so release.md and ADR 0036 decision 5 now say verify checks that an image matches the manifest instead of proving it. Co-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>
Owner
Author
|
Integrator merge evidence (HOM-28, HOM-29), head 693da84:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HOM-28 (WP01-T04, reproducible backend release artifacts from an exact source revision) and HOM-29 (WP01-T06, machine-readable release manifest), Lane D of the Infrastructure Freeze. An operator can now answer "exactly what source and schema produced this running backend?" (
homayar_build_infoon/metrics,python -m backend.observability.release show, OCI labels), andrelease_manifest.py verifyturns that into a pass/fail check against the manifest of a commit. A developer's local environment cannot silently change release inputs: the manifest reads only Git objects at the commit, the image build refuses a stale lock, andverifycatches an overridden base image or uv version, a different lock/pyproject/migration head and a dirty build context.Stacked on #43 (HOM-113): #43 is merged (main
87ab41a) and main is merged into this branch (d61cd25,eb9a537); the HOM-28/29 change is7cc5349plus the doc commits1d57995and4dd6c73. Lane D merge order is 113 → 114 → 115 → 126 → 28 → 29; this PR merges last. Expected rebase points: ADR 0036 (every Lane D PR appends a decision at the end of the file; a test merge with #47 conflicts only there) andci.yml(a test merge with #47 auto-merges cleanly; #47 edits the middle ofcontainer-config, this PR adds a new job at the end and changes one line ofbackend).What changes
infra/release/release_manifest.py(new, standard library only, Python 3.12)generate --rev <commit>: reads every input withgit cat-file/git ls-treeat the commit (ignoresGIT_*variables and replace refs; never the working tree). Canonical JSON with sorted keys and no wall clock (build time = commit time unless--build-time). Contents:schema_version;source{repository, sha, tree_sha, commit_time, commit_time_epoch};application{name, version fromHomayar/pyproject.toml};runtime{python fromHomayar/.python-version, base image (reference, digest, pinned_by_digest) and uv version from the Dockerfile ARGs};dependencies(path + sha256 ofHomayar/uv.lock,Homayar/pyproject.toml,Homayar/backend/requirements.txt,Homayar/frontend/package-lock.json,pnpm-lock.yaml,website/pnpm-lock.yaml);schema{migration_head, migration_count} parsed from the revision literals (exactly one head, or it fails; broken chains and duplicate ids also fail);build_inputsper image (Dockerfile and.dockerignoresha256, base images; forapialso the release build args and the digest of the shipped code);profile;artifacts.imagesfrom--image name=sha256:<digest>;validationfrom--validation name=url;compatibility.requires_python;reproducibility(what is / is not pinned, computed from the Dockerfiles);manifest_sha256over the canonical body.build-args --rev <commit>: theKEY=VALUEbuild arguments of the API image (SOURCE_SHA,SOURCE_DATE_EPOCH,SOURCE_DATE,RELEASE_VERSION,RELEASE_PROFILE,INSTALL_AI_LOCAL).verify --manifest m.json --build-info build-info.json [--image-labels labels.json]: exit 0OK: ..., or exit 1 with one line per mismatch (manifest_sha256 of an edited manifest, source sha and date, version, profile, uv.lock and pyproject digests, migration head, shipped-code digest and count, base image, uv version, Python minor; with labels: revision, version, created, base.name). Errors exit 2.infra/docker/api.Dockerfile: base image pinned by digest (python:3.12-slim-bookworm@sha256:34386ef0…, the multi-platform index resolved 2026-10-18 withdocker buildx imagetools inspect, Python 3.12.15); single global defaults forPYTHON_IMAGE,UV_VERSION,INSTALL_AI_LOCAL;uv sync --lockedinstead of--frozen; build argsSOURCE_SHA(default all-zero = unreleased),SOURCE_DATE_EPOCH,SOURCE_DATE,RELEASE_VERSION(defaultunreleased),RELEASE_PROFILE(default derived fromINSTALL_AI_LOCAL; a contradicting value fails the build); OCI labelsorg.opencontainers.image.{title,source,revision,version,created,base.name};pyproject.tomlanduv.lockcopied to/app/release-inputs/;write-build-infowrites/app/build-info.json(root-owned, read-only for the app user) beforeUSER.Homayar/backend/observability/release.py(from Observability: correlation ids, Prometheus metrics and sweep heartbeats (HOM-113) #43, additive):UNRELEASED_SHA(all zeros) accepted and shown assource_sha="unreleased"inhomayar_build_info(label set unchanged);RELEASE_PROFILES(standard,ai-local) validated because the profile is a label;build-info.jsongainssource.released,build{base_image, uv_version} andcode{files_sha256, file_count} (alembic.ini + backend/ without tests, environments and caches — the same rule asapi.Dockerfile.dockerignore, implemented identically in the manifest tool and tested for agreement); CLI--base-image,--uv-version..github/workflows/ci.yml): new jobrelease-manifest(needssource-contract, full-history checkout, setup-python 3.12): generates the manifest for${{ github.sha }}twice andcmps; builds the API image with thebuild-argsoutput (and checksSOURCE_SHA=github.shaandSOURCE_DATE_EPOCH=git show --format=%ct); copies/app/build-info.jsonout of the image, dumps the labels, generates the manifest with the image id and the run URL and runsverify --image-labels; starts the image and checks that/metricsreportssource_sha="<github.sha>"and printsrelease show; uploadsrelease-manifest.json,build-info.json,image-labels.json,build-args.txtas artifactrelease-manifest(actions/upload-artifact@ea165f8d… # v4.6.2, 90 days). Jobbackend:uv sync --frozen→uv sync --locked. No other job's steps changed.Homayar/backend/tests/test_release_manifest.py(19): byte-identical double generation (CLI), manifest describes the revision (tree, version, Python, every lock digest, build args ==build-argsoutput), API base image pinned by digest anduv sync --lockedonly, untracked files / dirty edits / an untracked extra migration head / a planted.envin the working tree do not change the manifest (synthetic repository built from the committed inputs, so the real checkout is never touched), the manifest follows the requested revision, migration head == AlembicScriptDirectoryhead ==release.code_migration_head()and count == revisions, a broken chain and a second head are refused (CLI exits 2), parser unit tests (annotated literals, merge migrations, duplicates, non-literals), shipped-code rule identical in image and tool, code digest of a clean working tree == the revision's,build_info_documentdeterministic and validates sha/profile,runtime_identityshows real sha /unreleased/unknown,verifypasses on a match and names exactly the mismatching field for source sha, lock digest, migration head, version, code digest and base image, flags an unreleased image and an edited manifest, CLI exit codes and label checks,backend/requirements.txtlists exactly the uv.lock runtime closure (tomllib, extras followed), no secret in the manifest (secret-field names,KEY=value, e-mail addresses, a runtime canary exported as everySECRET_FIELDSenv var), validation URLs with credentials/query/fragment and malformed digests/times/revisions refused. Tests skip cleanly without Git metadata, and the working-tree comparisons skip when the shipped code or migrations are uncommitted.docs/operations/release.md(cut a release, generate/verify, read identity from a running backend, what is and is not reproducible, maintenance: re-pin, regenerate requirements, one migration head); index line indocs/README.md; thehomayar_build_inforow ofdocs/operations/observability.md(values frombuild-info.json,unreleased,unknownonly outside an image); Release-manifest row anduv sync --lockedindocs/operations/ci.md; ADR 0036 Decision 5 appended at the end.Files outside the list above: none. Not touched:
infra/compose/*,infra/scripts/*, backup/restore/tombstone code, migrations,rls.py,saas_models.py,backend/config.py,ReportAppvisionProgress.md.Local evidence
GitHub Actions is billing-blocked (runs fail at startup with zero steps), so CI has not run; everything below is local. Host: macOS, Docker Desktop linux/x86_64, load average 100–300.
Re-verified on HEAD
eb9a537(main65f1d75merged)eb9a537: normal merge oforigin/main65f1d75; one conflict indocs/operations/ci.md, resolved by keeping main's console end-to-end row next to this branch's Backend (uv sync --locked) and Release manifest rows. TrailerCo-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>.eb9a537with exactly thebuild-argsoutput (first attempt):OK: build-info.json matches manifest 85017c43b6b17218 (source eb9a5371077dd0b257e57a45851fa7a0ed504d57, version 0.5.0, migration head d8a2f4c6b1e3); code 238 files;installed_set_sha256still7c4769cc…;/metricsshows the commit single-process and multiprocess; the unreleased image still failsverify.uv sync --frozen; it is not part of this PR and was left unchanged.Re-verified on HEAD
3be6ba8(after #43 merged; main merged ind61cd25)d61cd25: normal merge oforigin/mainba70ee4(includes Observability: correlation ids, Prometheus metrics and sweep heartbeats (HOM-113) #43 as87ab41a), no conflicts, trailerCo-Authored-By: Mohammad Kohandezh <kohandezh@hotmail.com>.3be6ba8: main removed the legacy public site (Remove the legacy public site [3/3] #75:infra/docker/public-web.Dockerfileand the rootpnpm-lock.yaml), sogeneraterefused every newer revision; the manifest now coversapi,console-webandegress-proxyandwebsite/pnpm-lock.yaml.test_release_manifest.py+test_observability_metrics.py92 passed, 1 skipped (Redis).3be6ba8with exactly thebuild-argsoutput (attempt 1 failed on a Debian archive fetch, attempt 2 built).verify --image-labels:OK: build-info.json matches manifest 1ed3ebde9e15aee0 (source 3be6ba89cf12539c4e5998be844e32cee983c03b, version 0.5.0, migration head d8a2f4c6b1e3); code 231 files;installed_set_sha256still7c4769cc…;/metricsreportssource_sha="3be6ba8…"single-process and in multiprocess mode (/run/homayar-metrics); the unreleased image still failsverify.Doc must-fix on HEAD
f0b5550(Integrator)manifest_sha256is described as an integrity checksum, not proof of authorship (unkeyed: whoever edits a manifest can recompute it) inrelease.md, ADR 0036 decision 5 and the tool docstring.reproducibility.not_pinnedandrelease.mdnow include "not signed / no provenance attestation; images are local ids, not registry digests".release.mdlists known gaps:verifydoes not compare the installed distribution set; uv is installed by version, not by hash; the on-prem ComposePYTHON_IMAGEdefault is a tag;SOURCE_DATE_EPOCHdoes not control layer file timestamps.test_release_manifest.py+test_observability_metrics.py: 92 passed, 1 skipped (Redis); ruff,ruff format --checkandmypy --stricton the tool clean; gitleaks on the changed files: no leaks. Image inputs are unchanged by this commit (docs, a docstring, a message string and onenot_pinnedentry), so the4dd6c73image verification below still describes the build; not rebuilt.Re-verified on HEAD
4dd6c73(after merging #43's heade0b577e, Integrator round 2)origin/backend/hom-113-observability(e0b577e)712e22d, no conflicts. Test merge withorigin/main6694306: clean4dd6c73)release.pymodule docstring and ADR 0036 decision 1 consequences now describebuild-info.jsonas written by the API image build (unreleasedwithout release args;unknownonly outside an image); theobservability.mdrow already said so (1d57995)127.0.0.1:54337test_release_manifest.py+test_observability_metrics.py: 92 passed, 1 skipped (Redis heartbeat, noTEST_REDIS_URL)no leaks foundbuild-argsof4dd6c73, first attempt;Created= commit time2026-10-18T17:12:26Z; code 211 filesec73f6fc…(= manifest);installed_set_sha2567c4769cc…(same as all earlier builds)verify --image-labelsOK: build-info.json matches manifest d1413ddda3dd160b (source 4dd6c730c311ff4ceabd5b27a7ad43c08d879dde, version 0.5.0, migration head d8a2f4c6b1e3); unreleased image: exit 1 with per-field diff/metricssingle process and multiprocess (ascompose.cloud.yamlnow: read-only, tmpfs/tmpand/run/homayar-metrics,PROMETHEUS_MULTIPROC_DIR=/run/homayar-metrics, 2 workers)homayar_build_info{…,source_sha="4dd6c730c311ff4ceabd5b27a7ad43c08d879dde",…} 1.0in both; counters written by uid 10001 under/run/homayar-metrics. The image itself assumes neither pathRe-verified on HEAD
1d57995(after merging #43's headdf191e6)origin/backend/hom-113-observability(df191e6)a5167df, no conflicts (#43 did not touchrelease.py; ADR 0036 merged automatically, Decision 5 stays last). Test merge withorigin/mained9cd2c: cleandocs/operations/observability.mdhomayar_build_inforow: values come from the image'sbuild-info.json;source_sha="unreleased"without release args;unknownonly outside an image (commit1d57995)127.0.0.1:54337(freshinitdb,LC_ALL=C, socket/tmp/claude/pgsock-release),TEST_DATABASE_URL=…:54337/homayar_test HOMAYAR_REQUIRE_POSTGRES_TESTS=1 pytest backend/tests/test_release_manifest.py backend/tests/test_observability_metrics.py -v -rs→ 88 passed, 1 skipped in 338.8 s (19 release + 69 metrics; skip: Redis heartbeat test, noTEST_REDIS_URL)no leaks foundon the 10 files this PR changes relative to #43build-args --rev 1d57995…(SOURCE_SHAandSOURCE_DATE_EPOCHchecked against git) →docker buildwith exactly those args, first attempt (~12 min).Created=2026-10-18T12:27:16Z(commit time).build-info.json: sha1d57995…,released: true, code 211 files0f1ea1fe…(= manifest),installed_set_sha2567c4769cc…— the same as both earlier buildsverify --image-labelsOK: build-info.json matches manifest 5e02d5283c06b884 (source 1d57995f8f463466b2e659afa521dc21a36207c4, version 0.5.0, migration head d8a2f4c6b1e3); the unreleased image fails with one line per mismatch/metrics, single processhomayar_build_info{migration_head="d8a2f4c6b1e3",profile="standard",python_version="3.12.15",source_sha="1d57995f8f463466b2e659afa521dc21a36207c4",uv_lock_sha256="78e6e975d3ad0256",version="0.5.0"} 1.0/metrics, multiprocess likecompose.cloud.yaml--read-only --tmpfs /tmp,PROMETHEUS_MULTIPROC_DIR=/tmp/homayar-metrics,uvicorn --workers 2: samehomayar_build_infoline;/tmp/homayar-metricscreated and written by uid 10001 (counter_*.db,histogram_*.db), so/tmpis writable in the release image as Compose mounts itEarlier run on
7cc5349(before mergingdf191e6)uv run pytest backend/tests/test_release_manifest.py backend/tests/test_observability_metrics.py -v -rs(one worker, SQLite; the release tests need no database)test_redis_heartbeat_store_round_trip, noTEST_REDIS_URL).test_release_manifest.py: 19/19 with no skips, so the working-tree comparisons (code digest, migration head vs Alembic) ran against this commit. #43's metrics tests still pass with therelease.pychanges. No full-suite run (host load ~300).release_manifest.py generate --rev 7cc5349…twice,cmpmanifest_sha2563734a777…; headd8a2f4c6b1e3, 33 revisions; base image pinned by digest; shipped code 211 files, digest equal torelease.code_file_digests()of the clean working treedocker run --rm --volume "$PWD:/repo:ro" --workdir /repo rhysd/actionlint:1.7.7 -color=false .github/workflows/ci.ymlhadolint/hadolint:v2.12.0-alpine infra/docker/api.Dockerfile(as in CI)ruff check --config Homayar/pyproject.tomlonrelease.py,test_release_manifest.py,infra/release/release_manifest.py;ruff format --checkon the two new filesmypy --config-file Homayar/pyproject.toml backend/observability/release.py backend/tests/test_release_manifest.py ../infra/release/release_manifest.py;mypy --strict infra/release/release_manifest.pybackend/config.py,backend/providers/asanak.py,registry.py,whisper.py); strict:Success: no issues foundUV_OFFLINE=1 uv lock --check(uv 0.11.24)Resolved 106 packages— whatuv sync --lockedenforcesdir --config .gitleaks.toml --redacton the 9 changed files at HEAD +.gitleaks.tomlno leaks foundgit merge-tree --write-tree origin/main HEAD; same withorigin/backend/hom-126-alerts(#47)4e1b1b0: clean. #47:ci.ymlanddocs/README.mdauto-merge; only ADR 0036 conflicts (both append a decision at the end)docker build --file infra/docker/api.Dockerfile .(no build args, as the CI smoke test and developers do; built from the working tree during development, before the shipped-code digest was added)uv sync --lockedpassed in the image;python -m backend.observability.release show:source.shaall zeros,released: false, profilestandard, Python 3.12.15, base imagepython:3.12-slim-bookworm@sha256:34386ef0…, uv 0.11.24, headd8a2f4c6b1e3, 74 installed distributions; labelsrevision=000…0,version=unreleased,created=1970-01-01T00:00:00Z,base.name= the pinned digestbuild-argsof this commit)build-args --rev 7cc5349…(checkedSOURCE_SHA= HEAD andSOURCE_DATE_EPOCH=git show --format=%ct), thendocker build --file infra/docker/api.Dockerfilewith exactly those six--build-args, from the clean checkoutuv sync --lockedwithoperation timed outdownloading wheels from PyPI under host load, after the lock resolved;SOURCE_DATE_EPOCHdisables layer reuse, so each attempt downloads everything; ~29 min). Image configCreated=2026-10-18T07:19:33Z(the commit time).build-info.json: sha7cc5349…,released: true, date epoch1792307973, profilestandard, Python 3.12.15, base image = the pinned digest, uv 0.11.24, headd8a2f4c6b1e3, code 211 filesce676fff…(= manifest), 74 distributions withinstalled_set_sha2567c4769cc…— identical to the unreleased image built hours earlier. Labels: revision7cc5349…, version0.5.0, created2026-10-18T07:19:33Z, base.name = the pinned digest.generate --rev 7cc5349… --image api=<image id>, thenverify --manifest --build-info --image-labelsOK: build-info.json matches manifest b09ebeb4e96816d1 (source 7cc534915b87649c7bd7ac3017b883e056ba286a, version 0.5.0, migration head d8a2f4c6b1e3)verifyof the unreleased image'sbuild-info.jsonagainst the same manifestsource.sha,source.commit_time_epoch,build_inputs.api.code.files_sha256,build_inputs.api.code.file_countand "built without release arguments (unreleased)"docker runas in the CI job (read-only, SQLite, development),curl /healthz,curl /metrics,docker exec … python -m backend.observability.release showhomayar_build_info{migration_head="d8a2f4c6b1e3",profile="standard",python_version="3.12.15",source_sha="7cc534915b87649c7bd7ac3017b883e056ba286a",uv_lock_sha256="78e6e975d3ad0256",version="0.5.0"} 1.0;showprints the same document as the extractedbuild-info.jsonSecurity notes
.envfiles and records no Git author/committer identity (only the commit epoch). A test exports a runtime-generated canary as everySECRET_FIELDSvariable and checks it is absent, and scans for secret-field names,KEY=valuepairs and e-mail addresses.--validationaccepts plainhttp(s)links only: credentials, query strings and fragments are refused (signed URLs and tokens).--imageacceptssha256:<64 hex>only.--revrefuses option-like values and is passed after--end-of-options.GIT_*variables removed andGIT_NO_REPLACE_OBJECTS=1, so an environment cannot point the tool at another repository, index or object store; blobs are read withgit cat-file --batch(no textconv/filters).build-info.jsonis written as root beforeUSER 10001, so the app user cannot rewrite its own identity. No build argument carries a secret.Honest limits
uv.lock+ same base digest + same uv version → samebuild-info.jsonand installed distribution set, not the same image digest. Not pinned: Debian packages fromapt-get(build-essential,libpq-dev,ca-certificates,ffmpeg,libgomp1,libmagic1,libpq5), file timestamps and layer digests; uv is installed withpip install uv==0.11.24(version, not hash).build-info.jsonhashes the shipped code and the names/versions of installed distributions, not their files. The manifest lists all of this underreproducibility.console-web,public-webandegress-proxyremain tags (recorded aspinned_by_digest: false); only the API image was in scope.infra/compose/compose.onprem.yamlpassesPYTHON_IMAGE: "${PYTHON_IMAGE:-python:3.12-slim-bookworm}"as a build arg, sodocker compose buildbypasses the digest pin. Compose builds are unreleased anyway (noSOURCE_SHA) andverifyreports the base-image difference; changing the Compose default is aninfra/composechange I did not make (follow-up for its owner).SOURCE_DATE_EPOCHto every step, so a release build does not reuse cached layers of an earlier commit (fresh runners in CI are unaffected; local release builds take a full rebuild).docs/operations/release.md).ai-localprofile image (INSTALL_AI_LOCAL=true) was not built locally.verifycompares the Python minor version with.python-version(the patch comes from the pinned base image).Homayar/docs/architecture/legacy-retirement.mddefers pruning ofreportlab(runtime, no importer left) and theldap3extra to WP01-T04/HOM-28 (thepersian-exportextra also has no importer). It changespyproject.toml,uv.lockandrequirements.txt, which the other Lane D PRs share, so it is left for a lead decision (this PR or a follow-up).