portfolio-optimiser/docs/plan/2026-07-25-ordering-axes-ruling.md
Kjell Tore Guttormsen 8a86f2ab6d Squashed 'shared/' changes from 7aa53fc..a2b57d2
a2b57d2 docs(plan): V1 — «de 5 linjene» var ikke homogene; :214 er ikke en literal
d63e45d docs(plan): okf-versjonssjekken utført — hypotesen falsifisert på to stale premisser
ef31dda docs(plan): V1 §4.2 — pin + id + sitering avgjort, og ratifiseringsgaten funnet
35220f7 docs(plan): V1 §4.1 — serialiseringsformen er bundet av :158, så 6 sider er invariant
8a7d430 docs(plan): operatøren avgjorde V1 (O2) og B1 (O1) — B1 utført, V1 gated
f306c7b docs(plan): V1 §5.1 — konsument-kostnaden målt @ 8a14137, O3-raden priset
ab0ea8f docs(plan): innboksrunden — V1 utvidet med tre målte funn, §7.2-siteringen ref-bundet
84a3010 docs(plan): §7.2 — MCP-spørsmålet er avgjort av frossen tekst, S2.2 og S2.4 er ugated
e984d51 docs(plan): D-B ankret, D-A#3s årsak er repo-avhengig, V1 utvidet med oppstrøms-evidens
3de702b docs(plan): V1 §6 — rekkevidde-forbeholdet korrigert, og funnet under funnet
249425b docs(plan): amendment-underlag — D-A#3s ÅRSAK rettet, drift ikke avvik
4876970 docs(plan): V1-underlag — generated-feltets form etter OKF v0.2
3f18ca7 docs(plan): B1/D4 operator question — nav-golden's normative status, four costed options
29ad8ca docs(plan): amendment-underlag — frossen tekst per køpunkt, med målt fasit-effekt
a67a243 docs(plan): ordering-axes ruling — three distinct axes, as an interpretation record
381d9e5 docs(plan): guard adoption plan pins no version — v0.2 header was stale
54ca0ff docs(plan): D3 ratified — status vocabulary landed in coord register
fe6b998 docs(plan): D3 status vocabulary — canonical 7-token set for roll-up register
c66ccc3 docs(plan): D2 register-form fix — two-output model resolves ÅS#5
b641741 docs(examples): nav-golden fixture class — bundle → read-context goldens
9801d35 docs(spec): method-spec §3 Step 1 — Q3 navigation contract (hierarchy)
bfa5a9b docs(spec): ingest-spec — land ratified D1 stamp model
5f046ab docs(plan): Q3 finding — method-spec §3 silently forbids hierarchical bundles
a013e8b docs(plan): F1 direction — free-text sources belong in ingest-spec door A
84191c6 docs(plan): record commons↔implementation-repo coordination boundary
cae9972 docs(plan): llm-ingestion-guard adoption — planned at future untrusted ingest boundaries

git-subtree-dir: shared
git-subtree-split: a2b57d20fc0953c19776c21ce1311f234386a590
2026-07-31 18:37:48 +02:00

8 KiB

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.