Decision note 0017: owning fab, and how a package this project depends on is named #18

Open
marcos-mendez wants to merge 3 commits from docs/decision-0017-keel-fab into main
marcos-mendez commented 2026-09-28 05:49:02 -03:00 (Migrated from github.com)

Closes #17.

Decision note 0017, and only 0017: 0014, 0015 and 0016 are concurrent work, so the number was taken deliberately rather than as "the next free one". 0011 stays a gap, per the record in #11.

What it records

The maintainer's decision of 2026-09-28 in Keel-Linux/fab#8, and the reasoning that decides the shape rather than assuming it. The rule it settles outlives fab: a Debian package this project depends on and modifies becomes a Keel package with a version this project chooses, not upstream's version with a +keelN suffix. For fab that is keel-fab at 2.0.0.

Implementation is Keel-Linux/fab#9, which closes fab#8 and fab#7. Not merged, and the build host has not been converted.

Three things the note adds beyond the issue

It corrects the record on what 1.1.1+keel2 was. Keel-Linux/fab#7 states that the version exists in no commit and implies the installed files might diverge from git. Both were measured and both are wrong. Every file the package ships is in git: comparing the extracted .deb against e610377, 22 of 25 byte identical and the three that differ do so only in dh_python3's shebang normalisation. And the changelog entry merged as fab#6 at 2026-09-27T19:58:53Z, ten hours before fab#7 was filed; git log -- debian/changelog on master has returned two commits since then. fab#7 was read from a stale ref: the build host clone had not fetched since before fab#6, so its origin/master predates the entry while its HEAD carries it.

That matters for the note because it changes what the defect is. It was never the missing entry. It was that a version string was the only provenance there was, and neither packaged version was tagged at all, so core.manifest's fab_version 1.1.1+keel1 named no commit. The note says that plainly instead of repeating the issue.

It separates three axes that were being conflated. The repository name (0006 rules on it, keeps fab, and stands), the Debian package name (renamed, and this is the first divergence between the two, so it is stated rather than inferred), and the command names (unchanged, decided by 380 call sites of which not one reads the package name). It also states why decision 0015 does not reach these commands: a build tool is not operator facing, and extending 0015 to it would rename 380 call sites to no operator's benefit inside a change whose point is that the build host keeps building.

It argues the name from the tooling rather than from taste. apt/lib/build.sh classifies a package as native when the Source starts with keel, the clone has no upstream remote and neither debian/watch nor debian/upstream/metadata exists. The fab clone already satisfied the last two, so keel-fab makes bin/build-package accept 0.1.0 on its own and the +keelN guard correctly stops applying, with nobody passing --native. The rule that selects the version scheme is keyed on the prefix, so a package whose scheme is ours has to carry the prefix or the policy and the code disagree.

The version number was got wrong first, and the note records how

The note argued 0.1.0, matching the scheme every package this project already owns starts at. The changelog gate wired up in Keel-Linux/fab#9 refused it on its first run and was right: dpkg --compare-versions 0.1.0 gt 1.1.1+keel2 is false, require-changelog compares exactly that way, and reprepro and dpkg-genchanges read a changelog as one monotonic series too. A rename does not give the changelog a fresh start.

So the note carries the rule the next package to move needs: a package that becomes ours takes the next version above the highest it has already published, chosen by what the change means, and only a package with no predecessor starts at 0.1.0. For fab that is 2.0.0, which also stops 0.x claiming that the thing building every layer in production is pre-release.

This is recorded rather than quietly corrected because it was not obvious by reasoning; a gate caught it, and the gate only existed because the same pull request wired it up.

The Provides/Replaces/Conflicts reasoning, for tracker#13's benefit

This is the first such triple in the organization's packaging, so the note records what each field is for. Conflicts is load bearing and Replaces hands the paths over. Provides satisfies a dependency on the name and nothing more: it does not protect an install by name while TurnKey's archive offers a real fab (apt removes keel-fab for it, reproduced 2026-09-29), and a fab plan cannot resolve it at all (tkldev#4). The negative pin Package: fab / Pin: release o=turnkeylinux / Pin-Priority: -1 is what protects the host; Pin: origin "" only matches a local repository and does not. So apt#15 is a prerequisite of the conversion, and tracker#13 needs the same pin per replaced name.

A versioned Provides: fab (= 1.1.1+keel2) is recorded as considered and rejected: it would satisfy a versioned dependency that exists nowhere, at the cost of writing upstream's version number back into the package whose point is not to carry one.

Also in this pull request: one new trap

docs/traps.md gains "Renaming a Debian package drops whatever debhelper found by its name". debhelper keys .install, .links and .docs on the binary package name, and dh_python3 finds a private python directory the same way, so a rename silently drops whatever was being picked up by name and the failure surfaces at first use. Naming the directory back has its own trap: dh_python3 <dir> processes that directory instead of its default pass, and the default pass is what moves a module off /usr/lib/python3.13/dist-packages onto the version independent path, so one call with the argument trades a missing registration for a module pinned to one python version. Both calls are needed.

It was found by building the old and new packages in a container and diffing them. Every static check on debian/ passed throughout and said nothing, which is the part worth writing down. The entry carries the version lesson as a second paragraph, since it has the same root, a rename, and was likewise found by a tool rather than by reading.

The other trap is left alone on purpose. #13 is adding "two machines answer to tkldev, and they run different fab", and this work hit that trap in its other form: a claim measured from the wrong clone reads exactly like one measured from the wrong machine. The note says so and says the fix needs one word added, "name the clone and its head, not only the machine", but leaves the amendment to #13 so the two do not collide in the same file.

Test plan

  • The note follows the form of 0001-0013: title line, Date, Status, ## Decision, the three part justification of brief section 10, ## Consequences. It also carries ## What this does not decide, ## Traps found while writing this and ## Implementation, following 0016.
  • No em dashes; commas, colons, parentheses and semicolons, per brief section 10.
  • Every factual claim carries its measurement and its date, and the readings from the build host name the file and the digest so the next reader can tell which machine and which clone they came from.
  • Does not contradict 0006 (names the divergence explicitly), 0008 (the fork keeps its history and its remote), 0012 or 0015 (says why it does not reach build tools).
  • Two files only: the note and docs/traps.md. No index to update; docs/decisions/ has none.
  • Maintainer confirms keel-fab and 2.0.0, which is the part fab#9 cannot land without.

This repository ships no package and has no package / changelog gate, so there is no changelog entry to add and no checks are reported on the branch.

Closes #17. Decision note **0017**, and only 0017: 0014, 0015 and 0016 are concurrent work, so the number was taken deliberately rather than as "the next free one". 0011 stays a gap, per the record in #11. ## What it records The maintainer's decision of 2026-09-28 in Keel-Linux/fab#8, and the reasoning that decides the shape rather than assuming it. The rule it settles outlives fab: **a Debian package this project depends on and modifies becomes a Keel package with a version this project chooses**, not upstream's version with a `+keelN` suffix. For fab that is `keel-fab` at `2.0.0`. Implementation is Keel-Linux/fab#9, which closes fab#8 and fab#7. Not merged, and **the build host has not been converted.** ## Three things the note adds beyond the issue **It corrects the record on what `1.1.1+keel2` was.** Keel-Linux/fab#7 states that the version exists in no commit and implies the installed files might diverge from git. Both were measured and both are wrong. Every file the package ships is in git: comparing the extracted `.deb` against `e610377`, 22 of 25 byte identical and the three that differ do so only in `dh_python3`'s shebang normalisation. And the changelog entry merged as fab#6 at 2026-09-27T19:58:53Z, ten hours before fab#7 was filed; `git log -- debian/changelog` on `master` has returned two commits since then. fab#7 was read from a stale ref: the build host clone had not fetched since before fab#6, so its `origin/master` predates the entry while its `HEAD` carries it. That matters for the note because it changes what the defect is. It was never the missing entry. It was that a version string was the only provenance there was, and **neither packaged version was tagged at all**, so `core.manifest`'s `fab_version 1.1.1+keel1` named no commit. The note says that plainly instead of repeating the issue. **It separates three axes that were being conflated.** The repository name (0006 rules on it, keeps `fab`, and stands), the Debian package name (renamed, and this is the first divergence between the two, so it is stated rather than inferred), and the command names (unchanged, decided by 380 call sites of which not one reads the package name). It also states why decision 0015 does not reach these commands: a build tool is not operator facing, and extending 0015 to it would rename 380 call sites to no operator's benefit inside a change whose point is that the build host keeps building. **It argues the name from the tooling rather than from taste.** `apt/lib/build.sh` classifies a package as native when the `Source` starts with `keel`, the clone has no `upstream` remote and neither `debian/watch` nor `debian/upstream/metadata` exists. The fab clone already satisfied the last two, so `keel-fab` makes `bin/build-package` accept `0.1.0` on its own and the `+keelN` guard correctly stops applying, with nobody passing `--native`. The rule that selects the version scheme is keyed on the prefix, so a package whose scheme is ours has to carry the prefix or the policy and the code disagree. ## The version number was got wrong first, and the note records how The note argued `0.1.0`, matching the scheme every package this project already owns starts at. The changelog gate wired up in Keel-Linux/fab#9 refused it on its first run and was right: `dpkg --compare-versions 0.1.0 gt 1.1.1+keel2` is false, `require-changelog` compares exactly that way, and `reprepro` and `dpkg-genchanges` read a changelog as one monotonic series too. **A rename does not give the changelog a fresh start.** So the note carries the rule the next package to move needs: a package that becomes ours takes the next version above the highest it has already published, chosen by what the change means, and only a package with no predecessor starts at 0.1.0. For fab that is `2.0.0`, which also stops `0.x` claiming that the thing building every layer in production is pre-release. This is recorded rather than quietly corrected because it was not obvious by reasoning; a gate caught it, and the gate only existed because the same pull request wired it up. ## The `Provides`/`Replaces`/`Conflicts` reasoning, for tracker#13's benefit This is the first such triple in the organization's packaging, so the note records what each field is for. `Conflicts` is load bearing and `Replaces` hands the paths over. `Provides` satisfies a dependency on the name and nothing more: **it does not protect an install by name** while TurnKey's archive offers a real `fab` (apt removes `keel-fab` for it, reproduced 2026-09-29), and a fab plan cannot resolve it at all (tkldev#4). The negative pin `Package: fab` / `Pin: release o=turnkeylinux` / `Pin-Priority: -1` is what protects the host; `Pin: origin ""` only matches a local repository and does not. So apt#15 is a prerequisite of the conversion, and tracker#13 needs the same pin per replaced name. A versioned `Provides: fab (= 1.1.1+keel2)` is recorded as considered and rejected: it would satisfy a versioned dependency that exists nowhere, at the cost of writing upstream's version number back into the package whose point is not to carry one. ## Also in this pull request: one new trap `docs/traps.md` gains **"Renaming a Debian package drops whatever debhelper found by its name"**. debhelper keys `.install`, `.links` and `.docs` on the binary package name, and `dh_python3` finds a private python directory the same way, so a rename silently drops whatever was being picked up by name and the failure surfaces at first use. Naming the directory back has its own trap: `dh_python3 <dir>` processes that directory *instead of* its default pass, and the default pass is what moves a module off `/usr/lib/python3.13/dist-packages` onto the version independent path, so one call with the argument trades a missing registration for a module pinned to one python version. Both calls are needed. It was found by building the old and new packages in a container and diffing them. Every static check on `debian/` passed throughout and said nothing, which is the part worth writing down. The entry carries the version lesson as a second paragraph, since it has the same root, a rename, and was likewise found by a tool rather than by reading. **The other trap is left alone on purpose.** #13 is adding "two machines answer to tkldev, and they run different fab", and this work hit that trap in its other form: a claim measured from the wrong clone reads exactly like one measured from the wrong machine. The note says so and says the fix needs one word added, "name the clone and its head, not only the machine", but leaves the amendment to #13 so the two do not collide in the same file. ## Test plan - [x] The note follows the form of 0001-0013: title line, `Date`, `Status`, `## Decision`, the three part justification of brief section 10, `## Consequences`. It also carries `## What this does not decide`, `## Traps found while writing this` and `## Implementation`, following 0016. - [x] No em dashes; commas, colons, parentheses and semicolons, per brief section 10. - [x] Every factual claim carries its measurement and its date, and the readings from the build host name the file and the digest so the next reader can tell which machine and which clone they came from. - [x] Does not contradict 0006 (names the divergence explicitly), 0008 (the fork keeps its history and its remote), 0012 or 0015 (says why it does not reach build tools). - [x] Two files only: the note and `docs/traps.md`. No index to update; `docs/decisions/` has none. - [ ] Maintainer confirms `keel-fab` and `2.0.0`, which is the part fab#9 cannot land without. ## Related - Keel-Linux/fab#9, the package, closing fab#8 and fab#7. All three checks green. - Keel-Linux/apt#15, the pin on the build host that matches nothing today. - Keel-Linux/buildtasks#12, `fab_commit` in the manifest, after fab#9. - Keel-Linux/tracker#18, the other repositories that ship something and do not gate their changelog. This repository ships no package and has no `package / changelog` gate, so there is no changelog entry to add and no checks are reported on the branch.
marcos-mendez commented 2026-09-28 06:45:06 -03:00 (Migrated from github.com)

Review of #18

Reviewed alongside Keel-Linux/fab#9, where the implementation findings are. This comment is about the note as a record: whether it argues its choices, and whether what it states is true.

It argues rather than records. "Why 2.0.0 and not 0.1.0" carries the measurement that refused the first answer and the rule it generalises to. "Why keel-fab is the name the tooling already wanted" derives the name from apt/lib/build.sh instead of from taste, and I confirmed the three conditions: the clone has only origin, and neither debian/watch nor debian/upstream/metadata exists, so Source: keel-fab is the one that flips the classification. The versioned Provides is recorded as considered and rejected with the reason. "What this does not decide" draws the boundary in four places. The three-part justification does the work brief section 10 asks of it rather than restating the decision three times. Recording that the version number was got wrong first, and that a gate rather than reasoning caught it, is the part that will be worth most to the next reader.

The findings below are all in the same place: the note is precise about everything it measured itself and loose about the two or three things it inherited.


HIGH. The Provides row states a mechanism that is false on the build host, and it is offered as the precedent for tracker#13

Lines 132-139, the relationships table:

apt installs a virtual package when exactly one real package provides it, so both keep working untouched.

That holds only when no real package of that name is reachable. On the build host one is, at a higher priority than the local build:

$ apt-cache policy fab                    # build host, read only
 *** 1.1.1+keel2 100   /var/lib/dpkg/status
     1.1.1       999   http://archive.turnkeylinux.org/debian trixie/main

Measured in a container with the same shape, keel-fab 2.0.0 installed from a file and a real fab in an archive at 999:

$ apt-get install -s fab
The following packages will be REMOVED:
  keel-fab
The following NEW packages will be installed:
  fab

So of the two sites the row says Provides exists for, one is not protected by it and the other is not protected by apt at all:

  • tkldev-setup:372 is apt-get install -y fab, and it removes keel-fab and installs upstream's fab. So does docs/infra-recovery.md:96, the rebuild-from-a-fresh-Debian procedure, which the note's follow-up table does not list.
  • tkldev/plan/main:3 never reaches apt. tkldev/Makefile is include $(FAB_PATH)/common/mk/turnkey.mk, so that line goes through fab-plan-resolve, and fab's own resolver matches the pool's .deb filenames against literal names (fablib/plan.py:205-206) and reads Provides only from packages it has already fetched (plan.py:329, used only at plan.py:406). It cannot resolve a virtual name. Worse, the miss is silent: missing is never mutated while brokendeps iterates it, so resolve() returns no error for a package the pool could not supply.

This matters more here than in fab#9, because the note says explicitly that the triple is "the precedent for tracker#13's keel-version and keel-sysinfo, which take over from packages that do have a reverse dependency and an operator facing command, so their Provides will be doing real work that fab's is not". Those two are more exposed to this exact failure, not less. The row as written tells the next author that Provides makes install-by-name safe. It does not. What makes it safe is a negative pin on the real package, which I confirmed works:

Package: fab
Pin: origin ""
Pin-Priority: -1

The rest of the row is right and worth keeping: Conflicts is load bearing, Replaces is what hands the paths over, unversioned is the correct choice for both because upstream's fab keeps releasing. Two sentences fix this: Provides satisfies a dependency on the name, it does not win against a real package of that name in an enabled archive, and it does nothing at all for a fab plan.

It also changes the sequencing two paragraphs later. "That is a live defect independent of this note" is true of the pin matching nothing; it is not true of the whole of apt#15. Blocking upstream's real fab is a requirement the rename creates, so apt#15 is a prerequisite of the conversion rather than a neighbour of it.


MEDIUM 1. "all 28 paths the old package had are present" is not what was measured

Line 253. I rebuilt the package and compared it against the fab 1.1.1+keel2 installed on the host. The substance holds: make-release-deb.py and turnkey-version.py come out byte identical to the installed ones, all nine aliases are there, product.mk is a06bfe03, and fablib lands on the version independent path. The counting does not.

The old package ships 26 regular files plus 9 symlinks. 28 reconstructs only as 19 files plus 9 symlinks, that is, with the 4 dist-info files and the 3 usr/share/doc/fab/ files silently dropped from the comparison. Everything the sentence does not mention is inside the dropped set:

  • usr/share/doc/fab/ becomes usr/share/doc/keel-fab/, so three paths the old package had are not present under their old names. Nothing in the organization reads that path, so the effect is nil, but the claim as phrased is the one thing that is not true.
  • changelog.gz differs. There are three differing files, not two.
  • usr/lib/python3/dist-packages/fab-1.1.0.dist-info/ is unchanged, so the installed package tells Python it is fab 1.1.0 while dpkg calls it keel-fab 2.0.0.

And runtime.d/fab.rtupdate becomes keel-fab.rtupdate, which the sentence covers with a glob rather than naming as a rename. "35 entries in the old package, 32 byte identical, 3 renamed by debhelper, 2 new" is both truer and a stronger claim, and it is also a better illustration of the trap the note is adding, since two of the three renames are debhelper keying on the package name.

MEDIUM 2. The Conflicts row still names keel-fab_0.1.0_all.deb

Line 132. 4668f5a moved the note to 2.0.0 and this one survived. It is inside the table an operator reads to understand the conversion, two screens above the runbook that correctly says 2.0.0. Same file, one word.

MEDIUM 3. The note argues the name from "no upstream remote" while committing to cherry-picks in both directions

Lines 108-120 against the 0008 paragraph at line 66. One of the three native conditions is git -C "$dir" remote | grep -qx upstream (apt/lib/build.sh:83), and the ordinary way to cherry-pick from upstream is to add that remote. Whoever does it on the build host clone makes bin/build-package reclassify keel-fab as a rebuild and refuse 2.0.0 for lacking +keelN. The note is the right place to notice that, since it is the document asserting both things.

MEDIUM 4. "The seven other repositories that ship something and do not gate their changelog"

Line 397, and the same phrase in the pull request body. tracker#18's title says seven, its first sentence says eight, and the true statement is neither: 13 repositories ship something installable and do not call the gate. 8 is 7 recipes plus fab, with the 5 changelog-less Debian sources bucketed separately. Since the note is what the next reader will cite, "the other repositories that do not gate their changelog, audited in tracker#18" without a number would age better than a number that three documents disagree about.

MEDIUM 5. On tracker#18's second group, which the note defers to

You asked for a verdict on this, so: the facts are right and the conclusion is the weaker half.

bin/require-changelog really does return 0 when the changelog is absent, before it even looks at the diff, and the behaviour is locked in by tests/require-changelog.bats, "a repository without a changelog requires nothing". So "a permanently green check asserting nothing" is exact.

But all five of those repositories have debian/control and debian/rules, and dpkg-buildpackage cannot run at all without a changelog: it reads the name, version and distribution from it. The shared build-deb.yml is a plain dpkg-buildpackage -us -uc -b with no dch and no generation step anywhere. So "the version being produced at build time" describes an inherited TurnKey convention rather than a working pipeline here, and the honest answer to "could a package source with no changelog legitimately produce a package" is no. It is also only a convention for three of the five: tklbam, tklbam-python-boto and turnkey-pylib gitignore debian/changelog, while webmin and turnkey-chroot do not, so for those two the absence reads as a plain omission, which is the bug a gate exists to catch.

And the same branch is a live bypass in the 15 repositories that are gated, not just a dormant one in the 5 that are not: the check is [[ ! -f "$changelog" ]] against the checked-out merge ref, so a pull request that deletes debian/changelog, or a typo in a caller's changelog: input, turns package / changelog green.

So the right order is the reverse of the audit's: make the gate refuse a package source it cannot find a changelog for, which is a few lines in a repository whose suite is at 100, and then wire all five up immediately with a red check that states a true fact. A red check that says "this is a package source with no changelog" is strictly more useful than prose in an issue. tracker#18 reaches for this itself in its last bullet and then files it as an open question; it should be the first action, not the last. The one part of its conclusion worth keeping is that wiring the five up before fixing the script would manufacture false assurance.

This does not change anything in note 0017, which only links the audit. It is worth a line in whichever note settles it, since "a gate that cannot find its subject and reports success" is the same shape as the trap the gate was written for.


LOW

  • Line 393: "125 of 125". It is 126. 19 + 56 + 36 + 15, and all four suites run clean: 1..19, 1..56, 1..36, 1..15, zero not ok. fab#9's own table says 126 and its test plan says 125, so this is a number three places disagree about in a note whose standard is that every factual claim carries its measurement.
  • Line 44: "nine hours before fab#7 was filed" is 10 hours 7 minutes, 2026-09-27T19:58:53Z to 2026-09-28T06:06:20Z.
  • Lines 46-47 and 385-389: the stale-clone diagnosis is right and can be sharper. On the build host, /root/src/fab is on branch pkg/keel2 at e610377 with a clean worktree, and git log -- debian/changelog there, at HEAD, already returns both commits. What is stale is remotes/origin/master, which is 16730a5, the #5 merge: the host has not fetched since before #6 landed. master is behind 4 of that stale ref and behind 6 of the real one. That is a better version of the amendment the note wants to hand to #13, because "name the clone and its head" would not have caught this one either. The word that would is "name the ref you measured": HEAD was current and origin/master was not, in the same clone.
  • docs/traps.md, the new entry: accurate on every point I could check. debhelper does key .install, .links and .docs on the binary package name; dh_python3 does find a private directory as /usr/share/<package>; and the two-call claim is right in effect, which I confirmed by building the package, since the bare call is what puts fablib in /usr/lib/python3/dist-packages rather than python3.13, and the directory call is what restores the shebang rewrite that makes make-release-deb.py and turnkey-version.py byte identical to the installed ones. The version paragraph belongs where it is. One unmeasured word: "lintian is quiet" is the only claim in the entry that nothing in fab#9's test plan backs, and the entry does not need it.
  • The note does not mention pyproject.toml, which keeps name = "fab", version = "1.1.0", so the built package reports three different versions depending on who is asked. Keeping it frozen is probably what holds the dist-info files byte identical; one clause under "What this does not decide" would settle it.
  • Line 273, "the twelve commands": correct, /usr/bin/fab plus fab-investigate, fab-rewind and the nine aliases. Worth adding to the verification line: check ownership with dpkg -S /usr/bin/fab, not dpkg -V. If anyone ever reaches for dpkg -i --force-conflicts here, Replaces being one-directional leaves keel-fab owning /usr/bin/fab and all nine aliases even though fab unpacked second, and removing keel-fab then deletes them while dpkg -V fab still exits 0.

Verdict

The decision is right, the reasoning is genuinely argued, the trap entry is accurate and useful, and the correction of fab#7 is correct: 1.1.1+keel2 was on master before #7 was filed, and I reproduced the byte comparison independently. One HIGH, and it is one paragraph: the Provides row states a mechanism that does not hold on the machine this decision is about, in the sentence the note explicitly offers as precedent for the next two packages. Fix that row and the 0.1.0 in the table above it and I would call it Approve.

Warning.

## Review of #18 Reviewed alongside Keel-Linux/fab#9, where the implementation findings are. This comment is about the note as a record: whether it argues its choices, and whether what it states is true. **It argues rather than records.** "Why 2.0.0 and not 0.1.0" carries the measurement that refused the first answer and the rule it generalises to. "Why `keel-fab` is the name the tooling already wanted" derives the name from `apt/lib/build.sh` instead of from taste, and I confirmed the three conditions: the clone has only `origin`, and neither `debian/watch` nor `debian/upstream/metadata` exists, so `Source: keel-fab` is the one that flips the classification. The versioned `Provides` is recorded as considered and rejected with the reason. "What this does not decide" draws the boundary in four places. The three-part justification does the work brief section 10 asks of it rather than restating the decision three times. Recording that the version number was got wrong first, and that a gate rather than reasoning caught it, is the part that will be worth most to the next reader. The findings below are all in the same place: the note is precise about everything it measured itself and loose about the two or three things it inherited. --- ### HIGH. The `Provides` row states a mechanism that is false on the build host, and it is offered as the precedent for tracker#13 Lines 132-139, the relationships table: > apt installs a virtual package when exactly one real package provides it, so both keep working untouched. That holds only when no **real** package of that name is reachable. On the build host one is, at a higher priority than the local build: ``` $ apt-cache policy fab # build host, read only *** 1.1.1+keel2 100 /var/lib/dpkg/status 1.1.1 999 http://archive.turnkeylinux.org/debian trixie/main ``` Measured in a container with the same shape, `keel-fab 2.0.0` installed from a file and a real `fab` in an archive at 999: ``` $ apt-get install -s fab The following packages will be REMOVED: keel-fab The following NEW packages will be installed: fab ``` So of the two sites the row says `Provides` exists for, one is not protected by it and the other is not protected by apt at all: - **`tkldev-setup:372`** is `apt-get install -y fab`, and it removes `keel-fab` and installs upstream's fab. So does `docs/infra-recovery.md:96`, the rebuild-from-a-fresh-Debian procedure, which the note's follow-up table does not list. - **`tkldev/plan/main:3`** never reaches apt. `tkldev/Makefile` is `include $(FAB_PATH)/common/mk/turnkey.mk`, so that line goes through `fab-plan-resolve`, and fab's own resolver matches the pool's `.deb` filenames against literal names (`fablib/plan.py:205-206`) and reads `Provides` only from packages it has already fetched (`plan.py:329`, used only at `plan.py:406`). It cannot resolve a virtual name. Worse, the miss is silent: `missing` is never mutated while `brokendeps` iterates it, so `resolve()` returns no error for a package the pool could not supply. This matters more here than in fab#9, because the note says explicitly that the triple is "the precedent for tracker#13's `keel-version` and `keel-sysinfo`, which take over from packages that *do* have a reverse dependency and an operator facing command, so their `Provides` will be doing real work that fab's is not". Those two are more exposed to this exact failure, not less. The row as written tells the next author that `Provides` makes install-by-name safe. It does not. What makes it safe is a negative pin on the real package, which I confirmed works: ``` Package: fab Pin: origin "" Pin-Priority: -1 ``` The rest of the row is right and worth keeping: `Conflicts` is load bearing, `Replaces` is what hands the paths over, unversioned is the correct choice for both because upstream's `fab` keeps releasing. Two sentences fix this: `Provides` satisfies a dependency on the name, it does not win against a real package of that name in an enabled archive, and it does nothing at all for a fab plan. It also changes the sequencing two paragraphs later. "That is a live defect independent of this note" is true of the pin matching nothing; it is not true of the whole of apt#15. Blocking upstream's real `fab` is a requirement the rename creates, so apt#15 is a prerequisite of the conversion rather than a neighbour of it. --- ### MEDIUM 1. "all 28 paths the old package had are present" is not what was measured Line 253. I rebuilt the package and compared it against the `fab 1.1.1+keel2` installed on the host. The substance holds: `make-release-deb.py` and `turnkey-version.py` come out byte identical to the installed ones, all nine aliases are there, `product.mk` is `a06bfe03`, and `fablib` lands on the version independent path. The counting does not. The old package ships 26 regular files plus 9 symlinks. 28 reconstructs only as 19 files plus 9 symlinks, that is, with the 4 `dist-info` files and the 3 `usr/share/doc/fab/` files silently dropped from the comparison. Everything the sentence does not mention is inside the dropped set: - `usr/share/doc/fab/` becomes `usr/share/doc/keel-fab/`, so three paths the old package had are not present under their old names. Nothing in the organization reads that path, so the effect is nil, but the claim as phrased is the one thing that is not true. - `changelog.gz` differs. There are three differing files, not two. - `usr/lib/python3/dist-packages/fab-1.1.0.dist-info/` is unchanged, so the installed package tells Python it is `fab 1.1.0` while dpkg calls it `keel-fab 2.0.0`. And `runtime.d/fab.rtupdate` becomes `keel-fab.rtupdate`, which the sentence covers with a glob rather than naming as a rename. "35 entries in the old package, 32 byte identical, 3 renamed by debhelper, 2 new" is both truer and a stronger claim, and it is also a better illustration of the trap the note is adding, since two of the three renames are debhelper keying on the package name. ### MEDIUM 2. The `Conflicts` row still names `keel-fab_0.1.0_all.deb` Line 132. `4668f5a` moved the note to 2.0.0 and this one survived. It is inside the table an operator reads to understand the conversion, two screens above the runbook that correctly says `2.0.0`. Same file, one word. ### MEDIUM 3. The note argues the name from "no `upstream` remote" while committing to cherry-picks in both directions Lines 108-120 against the 0008 paragraph at line 66. One of the three native conditions is `git -C "$dir" remote | grep -qx upstream` (`apt/lib/build.sh:83`), and the ordinary way to cherry-pick from upstream is to add that remote. Whoever does it on the build host clone makes `bin/build-package` reclassify `keel-fab` as a rebuild and refuse `2.0.0` for lacking `+keelN`. The note is the right place to notice that, since it is the document asserting both things. ### MEDIUM 4. "The seven other repositories that ship something and do not gate their changelog" Line 397, and the same phrase in the pull request body. tracker#18's title says seven, its first sentence says eight, and the true statement is neither: 13 repositories ship something installable and do not call the gate. 8 is 7 recipes plus `fab`, with the 5 changelog-less Debian sources bucketed separately. Since the note is what the next reader will cite, "the other repositories that do not gate their changelog, audited in tracker#18" without a number would age better than a number that three documents disagree about. ### MEDIUM 5. On tracker#18's second group, which the note defers to You asked for a verdict on this, so: **the facts are right and the conclusion is the weaker half.** `bin/require-changelog` really does return 0 when the changelog is absent, before it even looks at the diff, and the behaviour is locked in by `tests/require-changelog.bats`, "a repository without a changelog requires nothing". So "a permanently green check asserting nothing" is exact. But all five of those repositories have `debian/control` **and** `debian/rules`, and `dpkg-buildpackage` cannot run at all without a changelog: it reads the name, version and distribution from it. The shared `build-deb.yml` is a plain `dpkg-buildpackage -us -uc -b` with no `dch` and no generation step anywhere. So "the version being produced at build time" describes an inherited TurnKey convention rather than a working pipeline here, and the honest answer to "could a package source with no changelog legitimately produce a package" is no. It is also only a convention for three of the five: `tklbam`, `tklbam-python-boto` and `turnkey-pylib` gitignore `debian/changelog`, while `webmin` and `turnkey-chroot` do not, so for those two the absence reads as a plain omission, which is the bug a gate exists to catch. And the same branch is a live bypass in the 15 repositories that *are* gated, not just a dormant one in the 5 that are not: the check is `[[ ! -f "$changelog" ]]` against the checked-out merge ref, so a pull request that deletes `debian/changelog`, or a typo in a caller's `changelog:` input, turns `package / changelog` green. So the right order is the reverse of the audit's: make the gate refuse a package source it cannot find a changelog for, which is a few lines in a repository whose suite is at 100, and then wire all five up immediately with a red check that states a true fact. A red check that says "this is a package source with no changelog" is strictly more useful than prose in an issue. tracker#18 reaches for this itself in its last bullet and then files it as an open question; it should be the first action, not the last. The one part of its conclusion worth keeping is that wiring the five up *before* fixing the script would manufacture false assurance. This does not change anything in note 0017, which only links the audit. It is worth a line in whichever note settles it, since "a gate that cannot find its subject and reports success" is the same shape as the trap the gate was written for. --- ### LOW - Line 393: "125 of 125". It is 126. `19 + 56 + 36 + 15`, and all four suites run clean: `1..19`, `1..56`, `1..36`, `1..15`, zero `not ok`. fab#9's own table says 126 and its test plan says 125, so this is a number three places disagree about in a note whose standard is that every factual claim carries its measurement. - Line 44: "nine hours before fab#7 was filed" is 10 hours 7 minutes, `2026-09-27T19:58:53Z` to `2026-09-28T06:06:20Z`. - Lines 46-47 and 385-389: the stale-clone diagnosis is right and can be sharper. On the build host, `/root/src/fab` is on branch `pkg/keel2` at `e610377` with a clean worktree, and `git log -- debian/changelog` *there, at HEAD,* already returns both commits. What is stale is `remotes/origin/master`, which is `16730a5`, the #5 merge: the host has not fetched since before #6 landed. `master` is `behind 4` of that stale ref and behind 6 of the real one. That is a better version of the amendment the note wants to hand to #13, because "name the clone and its head" would not have caught this one either. The word that would is "name the ref you measured": HEAD was current and `origin/master` was not, in the same clone. - `docs/traps.md`, the new entry: accurate on every point I could check. debhelper does key `.install`, `.links` and `.docs` on the binary package name; `dh_python3` does find a private directory as `/usr/share/<package>`; and the two-call claim is right in effect, which I confirmed by building the package, since the bare call is what puts `fablib` in `/usr/lib/python3/dist-packages` rather than `python3.13`, and the directory call is what restores the shebang rewrite that makes `make-release-deb.py` and `turnkey-version.py` byte identical to the installed ones. The version paragraph belongs where it is. One unmeasured word: "`lintian` is quiet" is the only claim in the entry that nothing in fab#9's test plan backs, and the entry does not need it. - The note does not mention `pyproject.toml`, which keeps `name = "fab"`, `version = "1.1.0"`, so the built package reports three different versions depending on who is asked. Keeping it frozen is probably what holds the `dist-info` files byte identical; one clause under "What this does not decide" would settle it. - Line 273, "the twelve commands": correct, `/usr/bin/fab` plus `fab-investigate`, `fab-rewind` and the nine aliases. Worth adding to the verification line: check ownership with `dpkg -S /usr/bin/fab`, not `dpkg -V`. If anyone ever reaches for `dpkg -i --force-conflicts` here, `Replaces` being one-directional leaves `keel-fab` owning `/usr/bin/fab` and all nine aliases even though `fab` unpacked second, and removing `keel-fab` then deletes them while `dpkg -V fab` still exits 0. ### Verdict The decision is right, the reasoning is genuinely argued, the trap entry is accurate and useful, and the correction of fab#7 is correct: `1.1.1+keel2` was on `master` before #7 was filed, and I reproduced the byte comparison independently. One HIGH, and it is one paragraph: the `Provides` row states a mechanism that does not hold on the machine this decision is about, in the sentence the note explicitly offers as precedent for the next two packages. Fix that row and the 0.1.0 in the table above it and I would call it Approve. **Warning.**
marcos-mendez commented 2026-09-29 00:40:26 -03:00 (Migrated from github.com)

fad3160: the Provides row now says what it does not do and which pin closes it, with the conversion order (apt#15 first). The pin the review proposed, Pin: origin "", does not block an http archive (it only matches file: repositories); reproduced in a scratch apt tree, and the note records Pin: release o=turnkeylinux instead. MEDIUMs 1 to 4 and the LOWs are folded in.

fad3160: the `Provides` row now says what it does not do and which pin closes it, with the conversion order (apt#15 first). The pin the review proposed, `Pin: origin ""`, does not block an http archive (it only matches `file:` repositories); reproduced in a scratch apt tree, and the note records `Pin: release o=turnkeylinux` instead. MEDIUMs 1 to 4 and the LOWs are folded in.
This pull request has changes conflicting with the target branch.
  • docs/traps.md
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin docs/decision-0017-keel-fab:docs/decision-0017-keel-fab
git switch docs/decision-0017-keel-fab

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff docs/decision-0017-keel-fab
git switch docs/decision-0017-keel-fab
git rebase main
git switch main
git merge --ff-only docs/decision-0017-keel-fab
git switch docs/decision-0017-keel-fab
git rebase main
git switch main
git merge --no-ff docs/decision-0017-keel-fab
git switch main
git merge --squash docs/decision-0017-keel-fab
git switch main
git merge --ff-only docs/decision-0017-keel-fab
git switch main
git merge docs/decision-0017-keel-fab
git push origin main
Sign in to join this conversation.
No description provided.