Skip to content
zit Artifact [ca9877eca2]
Login

Artifact [ca9877eca2]

Artifact ca9877eca23f525bdc6decdf876c39030993341155a44c69bb61ee8bc11637c3:


# Erratum — draft-fossil-artifact-format-00 §4.7.3

| | |
|---|---|
| **Document** | `draft-fossil-artifact-format-00` |
| **Section** | 4.7.3 — B Card |
| **Type** | Technical (divergence from the reference implementation) |
| **Status** | Applied in `draft-fossil-artifact-format-01` (2026-10-12) |
| **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 with
  `manifest_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 own `zBaseline`.
- `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`) merges `p->aFile` with
  `p->pBaseline->aFile`. It never follows `pBaseline->pBaseline`. The
  by-name lookup (`:1655-1667`) does the same.

### 3.2 Experiment

In a fresh repository with `forbid-delta-manifests` off:

1. Check-in `base` (baseline manifest) has `a.txt`, `b.txt` and `c.txt`.
2. `d1 = fossil commit --delta` changes `a.txt`. Its artifact is
   `B <base>`, `C d1`, `D`, `F a.txt <h>`, `P <base>`, `R`, `U`, `Z`.
3. A hand-built `d2` changes `b.txt`: `B <d1>`, `C d2`, `D`,
   `F b.txt <h>`, `P <d1>`, `U`, `Z`. It is stored with
   `fossil test-content-put`, then `fossil rebuild` is 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

```diff
    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.  Fossil 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 MUST accept such an artifact and MUST
+   compute its file set in the same way (Section 8.2); rejecting it,
+   or resolving it recursively, diverges from Fossil.  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`) using
  `fossil commit --delta`, `fossil test-content-put`, `fossil rebuild` and
  `fossil 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.*