# Erratum — draft-fossil-repo-state-00 §4.2 step 4
| | |
|---|---|
| **Document** | `draft-fossil-repo-state-00` |
| **Section** | 4.2 — Crosslink procedure, step 4 (propagation) |
| **Type** | Technical (the text diverges from Fossil's implemented behavior, and from §5.3 step 4 of the same draft) |
| **Status** | Applied in `draft-fossil-repo-state-01` (2026-10-12) |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | Cross-check of the draft against Fossil 2.28 `src/tag.c`; confirmed by running Fossil 2.28 |
## 1. Summary
§4.2 step 4 says a singleton (+) tag "affects only its target and triggers no
walk". Fossil starts a walk for every tag it applies, singletons included: it
downgrades the singleton to a cancel and walks from the target. The walk
deletes the same-named tag that the target's descendants had inherited from
an ancestor, so a singleton placed in the middle of a propagated run stops the
inheritance below it.
§5.3 step 4 of the same draft already describes this ("When the tagged
artifact is used as a propagation source, the cancel it becomes blocks a
same-named tag from propagating through it"), so the two sections contradict
each other. §5.3 matches Fossil; §4.2 does not.
## 2. Location
§4.2 step 4, last two sentences. Current text:
> In addition, whenever a T-card in steps 1-3 applies a propagating (*) or
> cancel (-) tag to a resolved target, the propagation walk of Section 5.4
> MUST be invoked rooted at that target, so that the applied or cancelled tag
> reaches the target's descendants. A singleton (+) tag affects only its
> target and triggers no walk.
## 3. The divergence
Verified against Fossil 2.28 by reading the source and by running it.
### 3.1 Source
`tag_insert()` (`src/tag.c:157`) is the single entry point for every T-card the
crosslink applies (`src/manifest.c:2467`) and for the implicit cluster tag
(`src/manifest.c:2433`). After writing the target's own row it ends with:
```c
src/tag.c:239 if( tagtype==1 ) tagtype = 0;
src/tag.c:240 tag_propagate(rid, tagid, tagtype, rid, zValue, mtime);
```
There is no type test in front of the call: every applied tag starts a walk
rooted at its target. A singleton enters the walk as a cancel. In
`tag_propagate()` (`src/tag.c:34`), a cancel deletes a child's row when that
row was inherited (`srcid=0`) and is older than the singleton
(`src/tag.c:57`, the `doit` column), and the walk continues below every child
it changed (`src/tag.c:91`).
### 3.2 Run
A five-check-in chain on trunk (initial, c1, c2, c3, c4), using `fossil tag
add --raw` so the tag names are not prefixed. `tagxref` rows for `foo`, as
(tagtype, srcid, value):
| Check-in | after `*foo=v1` on c2 | after `+foo=v2` on c3 (1 s later) |
|---|---|---|
| c1 | — | — |
| c2 | 2, direct, v1 | 2, direct, v1 |
| c3 | 2, 0, v1 | 1, direct, v2 |
| c4 | 2, 0, v1 | **—** |
c4's inherited row is gone. `fossil rebuild` produces the same table, so it is
the crosslink result, not an artifact of the tag command. Under the text of
§4.2, c4 would keep `foo=v1`.
## 4. Impact
Observable whenever a singleton names a tag that the target's descendants
inherit. The usual case is a singleton `bgcolor` or `sym-*` placed on a
check-in inside a propagated run: in Fossil it truncates the run, so the
descendants lose the inherited tag (and, for `bgcolor`, their event colour).
An implementation that follows §4.2 keeps the inherited rows, and its tag
state, `tag find` output and timeline colours differ from Fossil's for the
same artifacts.
## 5. Correction
Make §4.2 say what §5.3 step 4 and Fossil do:
```diff
- In addition, whenever a T-card in steps 1-3 applies a propagating
- (*) or cancel (-) tag to a resolved target, the propagation walk
- of Section 5.4 MUST be invoked rooted at that target, so that the
- applied or cancelled tag reaches the target's descendants. A
- singleton (+) tag affects only its target and triggers no walk.
+ In addition, whenever a T-card in steps 1-3 applies a tag of any
+ type to a resolved target, the propagation walk of Section 5.4
+ MUST be invoked rooted at that target, so that the applied or
+ cancelled tag reaches the target's descendants. A singleton (+)
+ tag is first downgraded to cancel (Section 5.3, step 4): its value
+ stays on the target, and the walk removes the same-named tag that
+ the target's descendants had inherited.
```
## 6. Rationale
- **The draft already has the rule.** §5.3 step 4 and the opening of §5.4
describe the downgrade and its blocking effect. Only §4.2 says otherwise;
the correction removes the contradiction in favour of the sections that
match Fossil.
- **Fossil applies it on every path.** The downgrade appears twice in
`src/tag.c`, at `:239` for a newly applied tag and at `:131` for the
re-propagation of a parent's tags (`tag_propagate_all`), so a singleton
blocks inheritance whether it is crosslinked before or after its
descendants. Without the walk, the outcome would depend on crosslink order.
## 7. Verification
- Source: Fossil 2.28 `src/tag.c:34-110` (`tag_propagate`), `:118-135`
(`tag_propagate_all`), `:157-242` (`tag_insert`); `src/manifest.c:2433`,
`:2467`, `:2472` (the crosslink callers).
- Run: `fossil new`; four commits; `fossil tag add --raw --propagate foo c2
v1`; `fossil tag add --raw foo c3 v2`; query `tagxref` for `foo` on every
check-in; `fossil rebuild`; query again. c4 has no `foo` row after the
singleton, before and after the rebuild.
---
*Prepared against `draft-fossil-repo-state-00`. The RFC remains the oracle; the
reference implementation is used only to confirm the observable behavior.*