| Document | draft-fossil-artifact-format-00 |
| Section | 4.7.3 — B Card |
| Type | Technical (divergence from the reference implementation) |
| Status | Proposed — operator decision pending |
| Reported | 2026-10-11 |
| Submitter | Dan Mestas |
| Found via | Checking an illustrated reading of the draft against Fossil 2.28 |
1. Summary
§4.7.3 says a delta manifest whose B card names another delta manifest is invalid and MUST be rejected. Fossil does not reject it. It parses and cross-links such an artifact, then resolves the file set one level only: it merges the delta's F cards with the named delta's own F cards. Files that the named delta inherited from its own baseline disappear from the result without any error.
2. Location
§4.7.3, current text:
The referenced artifact MUST itself be a baseline manifest — a delta of a delta is forbidden at the format level. If the resolved baseline is itself a delta manifest (it carries a B card), the delta manifest naming it is invalid and MUST be rejected.
3. The divergence
Verified against Fossil 2.28 (52445a27f1) by source reading and by running
the code.
3.1 Source
fetch_baseline()(src/manifest.c:1367-1370) loads the baseline withmanifest_get(rid, CFTYPE_MANIFEST, 0).manifest_get()(:1158-1177) checks only that the artifact's type is MANIFEST. A delta manifest is type MANIFEST (the B card sets it,:614). Nothing checks whether the loaded baseline has its ownzBaseline.fetch_baseline()fails only when the baseline cannot be loaded or is not a manifest (:1371-1379).manifest_crosslink()(:2365) treats only that failure as an error, and then only by recording an orphan.manifest_file_next()(:1408-1428) mergesp->aFilewithp->pBaseline->aFile. It never followspBaseline->pBaseline. The by-name lookup (:1655-1667) does the same.
3.2 Experiment
In a fresh repository with forbid-delta-manifests off:
- Check-in
base(baseline manifest) hasa.txt,b.txtandc.txt. d1 = fossil commit --deltachangesa.txt. Its artifact isB <base>,C d1,D,F a.txt <h>,P <base>,R,U,Z.- A hand-built
d2changesb.txt:B <d1>,C d2,D,F b.txt <h>,P <d1>,U,Z. It is stored withfossil test-content-put, thenfossil rebuildis run.
Results:
| Check | Observed |
|---|---|
fossil test-parse-manifest d2 |
manifest_parse() worked |
fossil rebuild |
exit 0; d2 appears in the timeline as a check-in |
fossil ls -r d1 |
a.txt b.txt c.txt |
fossil ls -r d2 |
a.txt b.txt. c.txt is missing |
So the implementation neither rejects the artifact (as the current draft says) nor builds its full file set. It accepts the artifact and computes a truncated file list.
4. Impact
A reader built to the current §4.7.3 rejects artifacts that Fossil accepts and
cross-links, so a sync peer refuses content that every Fossil peer takes. A
reader that resolves baselines recursively gets a file set Fossil does not:
here it would include c.txt, and Fossil does not. Both reimplementations
disagree with Fossil about what this check-in contains.
The earlier draft (29 July copy) put the requirement on writers and said the reference parser does not reject the artifact. That was correct on acceptance. It was imprecise about the consequence: "can fail later" understates it, because nothing fails and the file set is silently short.
5. Correction
manifest (Section 8). The referenced artifact MUST itself be a
- baseline manifest — a delta of a delta is forbidden at the format
- level. If the resolved baseline is itself a delta manifest (it
- carries a B card), the delta manifest naming it is invalid and MUST
- be rejected. Ordering guarantees relative to F cards are given in
- Section 4.4.
+ baseline manifest: a writer MUST NOT emit a B card naming another
+ delta manifest. The Fossil reference implementation does not
+ enforce this on read. It parses and cross-links a delta manifest
+ whose baseline is itself a delta and computes its file set from
+ that delta's own F cards and the named artifact's own F cards only.
+ Files the named artifact inherits from its own baseline are absent
+ from the result, and no error is reported. A reader that rejects
+ such an artifact, or resolves it recursively, diverges from the
+ reference implementation. Ordering guarantees relative to F cards
+ are given in Section 4.4.
If the operator would rather keep a reader-side rejection as a normative tightening, the text must then say that this departs from Fossil 2.28. As written, it presents the rejection as Fossil behavior, which it is not.
6. Rationale
- The writer rule is the part Fossil honors. Fossil's own writer never emits a delta of a delta, so the MUST on writers describes all Fossil-produced content.
- The reader side has to describe what peers do. Rejecting an artifact that every Fossil peer accepts breaks sync; recursing produces a different file set from the one Fossil checks out.
- The silent truncation is the hazard worth naming. An implementer who matches Fossil needs to know the resolution is one level. An implementer who validates needs to know that Fossil does not.
7. Verification
- Source:
src/manifest.c:614,:1158-1177,:1367-1379,:1408-1428,:1655-1667,:2365(Fossil 2.28). - Experiment: §3.2, run with Fossil 2.28 (
52445a27f1) usingfossil commit --delta,fossil test-content-put,fossil rebuildandfossil ls -r.
Prepared against draft-fossil-artifact-format-00. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.