130 lines
8 KiB
Markdown
130 lines
8 KiB
Markdown
# Ordering axes — three distinct axes already present in the frozen spec
|
|
|
|
**Status:** INTERPRETATION of frozen spec text. This document changes **no** normative text and
|
|
introduces **no** new requirement. Where this document and the spec disagree, **the spec wins** and
|
|
this document is the thing that is wrong. It is a reading aid, not a contract.
|
|
|
|
**Issued:** 2026-07-25. Applied by at least one downstream implementation as binding for its own
|
|
work before this record existed — which is why the record exists.
|
|
|
|
**Scope:** the disambiguation is **across** the two specs. Each axis is already stated, normatively,
|
|
in one of them; nothing here is derived, inferred or extended. What was missing was a single place
|
|
saying that these are three different things and which one governs a given piece of code.
|
|
|
|
## 1. Why this exists
|
|
|
|
Three separate implementations reached, independently, for "the ordering rule" and each picked a
|
|
different one — twice arriving at code that looked correct and sat on the wrong contract. That
|
|
failure mode is expensive precisely because it is quiet: an implementation on the wrong axis is
|
|
usually deterministic, passes its own tests, and produces plausible output. Nothing fails until
|
|
another consumer depends on the property the right axis guaranteed.
|
|
|
|
The specs are not at fault. Each axis is stated clearly where it belongs. The gap is that no single
|
|
document said "there are three, they are not interchangeable, here is how to tell which one you are
|
|
standing on."
|
|
|
|
## 2. The three axes
|
|
|
|
| | Axis | Question it answers | Normative source |
|
|
|---|---|---|---|
|
|
| **A** | Membership | *Which files does a re-materialization replace?* | `ingest-spec.md:175-177` |
|
|
| **B** | Generation / index-link order | *In what order are generated files and their index links emitted?* | `ingest-spec.md:178-181`, §6 (`:183`) |
|
|
| **C** | Navigation order | *In what order does a reader traverse a bundle?* | `method-spec.md:76-78` |
|
|
|
|
**Axis A — membership, read from disk.** The set of files a re-materialization replaces is exactly
|
|
those the bundle already carries the stamp for, *"read from disk (§3), **never a caller-supplied
|
|
file list**; a caller can therefore never name a file for deletion"* (`:175-177`). This is a safety
|
|
property, not an ordering one: it is what keeps re-materialization from becoming a caller-controlled
|
|
delete.
|
|
|
|
**Axis B — generation order, supplied by the caller.** *"The ORDER of generated files and their
|
|
index links (§6) is the manifest's extraction order — supplied by the caller, **never filesystem
|
|
enumeration order**"* (`:178-181`). This is a determinism property: output must not vary with
|
|
directory iteration order.
|
|
|
|
**Axis C — navigation order, first-seen link order.** *"Traversal is **depth-first in first-seen
|
|
link order** and fully deterministic (the root index first, then each link followed in the order it
|
|
appears, recursively)"* (`method-spec.md:76-78`). This governs a **reader** walking an existing
|
|
bundle. It says nothing about how that bundle came to be ordered.
|
|
|
|
A and B are deliberately split by the spec itself — `:175` labels the split *load-bearing* — because
|
|
fusing them would trade a safety property for a determinism property. B and C are adjacent and
|
|
easily confused: both are "order", but one describes an act of **writing** and the other an act of
|
|
**reading**.
|
|
|
|
## 3. The test — which axis am I on?
|
|
|
|
Ask what the code *does*, not what it is called:
|
|
|
|
- **Does it decide what to delete or replace?** → **A**. The answer comes from disk. A caller-supplied
|
|
list is a defect regardless of how convenient it is.
|
|
- **Does it emit files or index entries?** → **B**. The order comes from the caller's extraction
|
|
order. Any order the code chooses for itself is wrong — including a deterministic one.
|
|
- **Does it read an existing bundle and visit its files?** → **C**. The order comes from the links,
|
|
depth-first, first seen.
|
|
|
|
The trap is that "generator" and "reader" are roles, not file names. A module that both writes an
|
|
index and reads one is on **both** B and C, and owes each its own rule.
|
|
|
|
## 4. Worked applications
|
|
|
|
**An index generator that sorts its entries alphabetically.** It emits index entries, so it is on
|
|
**B**. Alphabetical order is deterministic, which is what makes this hard to see — but B does not ask
|
|
for *a* deterministic order, it asks for *the caller's* order. Alphabetical is therefore
|
|
deterministically **wrong**, not merely unlucky, and the fix is to thread the caller's extraction
|
|
order through, not to substitute a different self-chosen sort. Attributing such a generator to axis A
|
|
("it is about membership, so ordering is a benign detail") is the specific mis-read that lets the
|
|
sort survive review.
|
|
|
|
**A convention that says a directory index is a "directory enumeration".** Ambiguous on exactly this
|
|
axis, and worth resolving in the text rather than in the reader's head. If it means the index file
|
|
*contains* an authored enumeration of that directory's children, it is compatible with C. If it means
|
|
the reader *enumerates the directory* to navigate, it contradicts `method-spec.md:82-83`, which says
|
|
an intermediate directory is *"navigated only through the links its own files carry, **never by
|
|
directory enumeration**."* Prefer wording that cannot be read the second way.
|
|
|
|
**A re-materializer narrowing what it deletes.** On **A**. Note that A's text scopes membership by
|
|
the stamp, and the stamp's identity field is `{manifest filename stem}@{h}` (`ingest-spec.md:168-170`)
|
|
where `{h}` is a hash of the manifest's bytes. Matching on the stem rather than the full reference is
|
|
what lets an edited manifest reclaim its own prior output, since `{h}` changes on every edit.
|
|
(Separately: `ingest-spec.md:171-174` assumes **one** manifest per bundle in version 1 and names the
|
|
multi-manifest case an extension point. Code that scopes ownership per-manifest is implementing that
|
|
extension point and runs ahead of the frozen text — see the amendment queue, not this document.)
|
|
|
|
## 5. Axis B does not travel alone
|
|
|
|
An implementation adopting B inherits §6's idempotence with it, not afterwards:
|
|
|
|
- linking is **idempotent by target** — a link whose target is already present is never added twice
|
|
(`ingest-spec.md:187-190`);
|
|
- on re-materialization, index links whose target is an ingest-owned file removed in that run MUST be
|
|
removed, and **all** other links — curated and promoted — are preserved verbatim (`:191-193`).
|
|
|
|
Taking the ordering rule without the idempotence rule produces duplicate links on the second run.
|
|
The two are one package.
|
|
|
|
## 6. What this document does not say
|
|
|
|
- It does not rule on any axis outside these three. Installation root, bundle root and source root are
|
|
a **different** family of distinctions and are not addressed here.
|
|
- It does not make the multi-manifest extension point written, ratified, or safe to assume.
|
|
- It does not bind any implementation's internal design — only the observable properties the specs
|
|
already require.
|
|
|
|
## 7. Verification
|
|
|
|
Testable criteria, so this document can be checked rather than believed:
|
|
|
|
- **V1 (the anchors are real and still say what is quoted):**
|
|
`grep -n 'Membership from disk' ingest-spec.md` → `:175`;
|
|
`grep -n 'never filesystem enumeration order' ingest-spec.md` → `:179`;
|
|
`grep -n 'depth-first in first-seen' method-spec.md` → `:76`.
|
|
Verified 2026-07-25.
|
|
- **V2 (no normative text was changed by this document):** `git show --stat` for the commit adding
|
|
this file touches `docs/plan/` only — never `ingest-spec.md`, `method-spec.md` or `examples/`.
|
|
- **V3 (the axes are genuinely distinct, not three phrasings of one rule):** A is sourced from disk,
|
|
B from the caller, C from link order in the file. Substituting any one for another changes
|
|
observable behaviour: A→B would let a caller name files for deletion; B→C would make output order
|
|
depend on a reader's traversal; C→B would make navigation depend on how the bundle was generated.
|
|
- **V4 (§6 idempotence is not separable from B):** `grep -n 'idempotent by target' ingest-spec.md` →
|
|
a hit inside §6, i.e. in the same section B points at for index links.
|