Research — Built-in re-vendor pin + vendor command + drift gate (#3504)
Phase 0 decisions. Grounded on the §1 grounding squad (architect / brownfield / alignment) and the live re-seed just done (open-packs PR #38 + tag v0.1.0). ADR 2026-08-16-2 (amended 2026-10-11) is binding; this is cite-and-implement, no new amendment.
D1 — Pin lockfile: location, direction, schema
the digested tree (it is the pin, authored in core; it must not be part of what the digest covers).
open-packs → core ("which open-packs SHA core vendored from"). open-packs' own built-in.source.yaml records the opposite anchor (core → open-packs, "which core commit the open-packs built-in tree was seeded from"). Same schema shape, opposite provenance direction — the loader/docstring must state this plainly.
SHA and never stored — FR-002), tree_digest {algorithm, value}, requires_core (PEP 440 range). Mirrors the open-packs file's fields so the two can be cross-checked.
- Location:
packs/built-in.source.yaml— a sibling ofpacks/built-in/, so it is outside - Direction (document to avoid confusion): core's
packs/built-in.source.yamlrecords - Schema:
repository(open-packs URL),commit(open-packs SHA; a tag is resolved to a
D2 — Digest: clean_tree_digest is the gate's authority (pollution-immune)
charter.offering.packs.pack_clean_digest.clean_tree_digest(pack_root, level=BUILT_IN) — a declared-file-set digest (never rglob), LF-normalized, reproducible across a git-archive export, a __pycache__-polluted worktree, and an installed copy.
is vulnerable to — core carried a stray git-ignored assets/__pycache__/*.pyc, so snapshot_sha256 over the raw worktree returned 9c96178e while the clean tree is 99e5e97c. A gate that may run in a developer's polluted worktree must not red on untracked bytecode. clean_tree_digest enumerates only declared files, so it is immune. ADR decision 2 names "charter.offering.packs hashing (snapshot_sha256 and the manifest hash)"; clean_tree_digest is the same family, kept separate precisely for this robustness (NFR-001/NFR-004). We MAY also record snapshot_sha256 in the lockfile as a secondary cross-check against open-packs' built-in.source.yaml, but the gate's verdict is clean_tree_digest.
open-packs compute the same value from the same bytes — no producer conflict; core verifies, never regenerates vendored content.
- Decision: the pin's integrity value and the drift gate use
- Why, not
snapshot_sha256: the live re-seed exposed exactly the failure modesnapshot_sha256 - One producer (ADR decision 3):
clean_tree_digestis deterministic from the tree, so core and
D3 — Fetch + SHA resolution + subtree scoping
ref; the vendor command resolves the pin's ref to a SHA and checks out that SHA, then copies the built-in subtree only of the open-packs ref (C-006 — content elsewhere in open-packs must not affect the pin/digest; open-packs uses a per-pack packs/<name>/ layout, built-in lives at its own path, confirm the exact subpath at implement time against the tagged v0.1.0 tree).
requires_core leaves packs/built-in/ untouched and exits non-zero. Never a partial/empty write.
time; integrity is pin-by-SHA + verify-by-content-digest + fail-closed. No consumer-runtime fetch (that is Option C / #3022). This satisfies supply-chain-install-safety for this dependency.
- Reuse
src/specify_cli/charter_packs/sources/git_source.py— it already clones and pins a git - Fail-closed (FR-006): an unreachable/unverifiable ref, a digest mismatch, or an incompatible
- Supply-chain posture (DIRECTIVE_051): the open-packs ref is a dependency fetched at pin-bump
D4 — Drift gate placement + offline
compares the committed tree to the committed lockfile — offline, no network (NFR-004). The acceptance test drives the same command on a drifting-tree fixture (non-vacuity; not a YAML text assertion) and asserts non-zero + the FR-010 remedy message. Red-first: the fixture reds before the gate/command exists.
.github/workflows/packs.ymlbuilt-in lane runsspec-kitty charter pack vendor --check; it
D5 — Release re-verification placement
python -m build ("before the distribution build", per the amendment's note that release uses python -m build, not hatch build). A post-build assertion (reusing the tests/charter_offering/test_packaging_parity.py pattern + the existing build fixtures) confirms the built wheel's packs/built-in/ matches the pin. Force-include and the packs/internal/ exclusion are unchanged.
.github/workflows/release.ymlandRELEASE_CHECKLIST.mdrun the verify step before
D6 — No ADR amendment; cite only
pin; one producer; requires_core compat) and defers the authoring flip (decision 6). This mission implements them. File a fresh ADR only if the lockfile schema or gate placement introduces a decision the amendment does not cover — not anticipated. Cite ADR 2026-08-16-2, 2026-08-16-3 (internal stays in core), 2026-10-11-2 (pack_level), 2026-08-16-1 (manifest hash).
- The 2026-10-11 amendment already settles decisions 1–4 (committed tree + drift gate; SHA + digest
Adversarial evidence (planning)
No new runtime dependency is added (the git fetch reuses existing machinery; no new PyPI package). The only "dependency decision" is the open-packs ref, handled by D3's pin-by-SHA + verify-by-digest + fail-closed posture. No contested supply-chain finding outstanding.