Sickle Sol

Convention: cite the symbol, not the line

A layer page names the symbol a reader needs and cites a line number only where the line itself is the subject. This page states the rule, the measurements behind it, and the two clauses the measurements showed are necessary.

Adopted 2026-09-09, before the layer 02 contract starts. That contract covers 65 handler files. The saving compounds there, and reopening the question during it would be worse than either answer.

What adoption changed, and what it did not

The rule binds new and edited text from adoption onward. The existing corpus was not rewritten wholesale, for the reason the rule itself gives: an edit that carries no information is not worth a reader's attention or a reviewer's. Three things were applied at adoption, each because something forced it.

  • Layer 05's citations to the venue bindings. They were the ambiguity in clause 3, not a style preference: a bare orca.rs resolves under two search roots and was correct only because of the order of a list inside check-doc-citations.py. Body citations became symbol names, which removes the ambiguity rather than qualifying it; the audit notes kept their lines under clause 4 and were pinned instead.
  • The structures historical layer 03 note 24's rename touches, so the rename could land next.
  • Nothing else. A page is brought to this convention when a contract opens it, not in a sweep. scripts/dev/check-doc-citations.py rejects a new ambiguous citation, so the corpus cannot drift back.

What the rest of the conversion should target

"Where something forces it" got the first pass done and is no use to the next person, because it names no test. The test is whether generation will ever reach it.

If the table-generation proposal lands, it deletes the export-inventory rows it owns and every citation inside them. So:

  • Prose and step lists first. No generator of any kind will reach them, and this is where the convention's saving is.
  • Export-inventory rows never. Either generation removes them or they stay as they are, so converting them first is work that gets deleted.
  • Every other table under ## Public items: undecided. Account-meta, error-code and parser-layout tables are different schemas. Each could get its own generator and none is proposed yet, so treat those rows as prose until one is.

The instinct this corrects is that the inventory rows are the bulk of the corpus. They are the bulk of the rows; they are not the bulk of the citations.

One partition, measured on this branch, counting citations rather than rows and taking a data row as one that follows a separator row:

Where the citation sitsCitations
A row keyed by exactly one item, in the export-inventory shape185
Any other row under ## Public items574
Prose, step lists, and every table elsewhere1,668

Only the first line is what the proposed generator deletes, and the proposal defines that set by two tests, not by a column's name: the table's remaining columns must be properties of the named item, and the row must name exactly one item. Rows grouping several symbols under one shared sentence fall outside it, because the proposal keys hand-written prose by item name and such a row has no single key. The second line is every other schema, including three that key like an inventory and are not one -- two trait-to-implementor relations and the discriminator preimage table. Those move only if someone proposes and sizes a generator for each.

Narrowed counts are floors, not alternative readings. Restricting the key pattern further -- to a backticked bare identifier, say -- lowers the first line again, because such a pattern silently drops every path-keyed and qualified row. That is the same defect as the /?Cargo\.toml: suffix match and the basename match in the blast-radius measurement above, and it is why this section gives one partition with its method instead of several filters side by side. An earlier version of this section did the latter, labelled citation counts as rows, and was right about none of it.

The conclusion does not depend on where the boundary lands. Even if every schema under ## Public items eventually gets a generator, 1,668 citations sit outside all of them. That is the target, and it is why the layer 02 handler contract should convert as it goes: 65 handler files, all prose and step lists, is where this pays for itself -- and layer 02 has no export-inventory table at all, so nothing there is waiting on a generator.

This page contains line numbers that must not move

The "What it costs today" section quotes one line number of owner_rebalance.rs becoming another as an illustration of a line number moving. It is an example, not a claim about today's source. This paragraph deliberately describes it instead of repeating it, because repeating it would create a third live citation of exactly the kind it is warning about.

A content-anchored remapper run during historical layer 03 note 24's rename proposed updating it, which would have destroyed the example the page exists to give. No tool can tell an illustrative citation from a live one; both are a path and a number. Exclude this file from any citation remapper. The same caution applies to any future page that quotes a citation as a specimen.

The rule

  1. Name the symbol. A function, type, constant, or trait is found by its name. validate_rent_accounts needs no line number, and it survives every edit that does not rename it.
  2. Cite a line only where the line is the subject, not the address. An offset constant, one specific check being contrasted with another, or a claim about position such as one item preceding another in a file.
  3. Where a symbol resolves to more than one definition, name the file once for the structure that uses it, and record the ambiguity as a duplication note if one does not already exist. The second half matters. Without it the file qualifier becomes a comfortable way to live with a duplicate definition.
  4. The audit notes keep line numbers. A note cites a defect that must stay findable at the commit it was recorded against.

What it costs today

Contract 3 changed 348 lines of Rust across 13 files and resolved 15 of layer 03's 37 notes. Repairing what those changes invalidated in docs/dev took 770 insertions and 524 deletions across the same eleven commits.

CommitRustdocs/dev
remove the pending conformance state3 / 4843 / 30
drop the Into<PositionDrainIdentity> bound29 / 34178 / 146
route two active-flow checks through the helper11 / 13186 / 176
consolidate four duplications247 / 106400 / 229
share the compound claim credit85 / 7136 / 11
FeesCollected.action9 / 288 / 66
owner intent-tail rule30 / 5183 / 172
pin the Meteora fee classes41 / 17182 / 147

Most of that is not information. Across the pass, the large majority of the edits were line numbers moving with code that did not otherwise change: no reader learns anything from owner_rebalance.rs:440 becoming :432. The edits that carried meaning were the derived counts and the note text.

The measurement that settles it

The run_owner_rebalance step list is the most citation-dense structure in docs/dev. It broke completely rather than partially when a commit shifted the file by eight lines, because each of its 22 entries cited the lines of the step it named, so every entry ended up describing its neighbour. It was rewritten in both forms.

FormCitations
A line per step31
The function once, symbols per step2

The two that remain are the function itself and one helper in another module. Ten helper names replace the rest. Nothing was lost: sequence is carried by the numbering, and an auditor who wants a line opens the function already cited.

Worked example of what the decision buys

Historical layer 03 note 24 records that four types named for the owner rebalance are used by the keeper and exit flows, so keeper_exit.rs reads as though it were about a rebalance. The rename is mechanical and changes no behavior. It was deferred on cost rather than merit: 84 occurrences across 8 files spanning three layers, which under the old convention moved lines in files that four layer pages cite.

Under this convention a page that names validate_drain_receipt does not care which line it is on, so the rename landed immediately after adoption.

The cost was measured before the ordering was trusted, and it was smaller than the note implied. Applying the rename and running cargo fmt reflows argument lists in all 8 files, but changes the length of only three of them, by a handful of lines each. So the old convention would have cost a re-sweep of the citations below three shift points rather than a rewrite of all four pages. The ordering still pays, and the honest reason is narrower than "the rename is expensive": it is that a re-sweep is exactly the class of work this convention exists to stop paying for, and one of the three shifts lands in the most heavily cited file in docs/dev.

Why clause 3 is necessary rather than defensive

Two collisions in this tree, found by different routes, are the same failure: an identity that is not unique resolves to the wrong thing and lands on real code, so nothing reports an error.

  • A bare symbol. Rewriting the step list in symbol form named ten helpers. Two of them, validate_closed_accounts and dispose_protocol_rent, are defined in both owner_rebalance.rs and decrease_close.rs. That duplication is historical layer 03 notes 2 and 3. The symbol form surfaced it at the moment of writing; the line form had concealed it, because two citations to two files look like two different things being described.

  • A bare filename. A citation remapper written during this pass keyed files by basename. rebalance_for.rs and exit_for.rs exist under both instructions/meteora/ and instructions/raydium/, so it remapped 21 Raydium citations using the Meteora line map. Every one still landed on real code. Two were caught because they happened to land on a blank line; the other nineteen were silent.

  • The same defect, inside the measurement of its own cost. Sizing historical layer 03 note 24's rename meant counting the citations it would invalidate. That count matched files by basename and returned 710; resolved properly it is 545, because layer 05's orca.rs is the venue binding and layer 04's is the adapter. The trap recurred in the tool measuring the trap. That is a stronger argument for the enforced check than any count of how many citations were ambiguous: knowing about this failure is not sufficient protection against it.

This repository deliberately mirrors its venues, so leaf names collide by design and will keep colliding. Clause 3 is what stops the convention reintroducing, through a bare symbol, the ambiguity the line numbers were accidentally preventing.

What does not change

scripts/dev/check-doc-citations.py still checks every remaining citation for bounds, a blank start or end, the layer 01 instruction table, every Files-table line count, and every open audit note. Fewer citations do not mean weaker checking; they mean the ones that remain are the ones that carry information.

It also now rejects a bare name that resolves under more than one search root, which is clause 3 made enforceable. Before that check existed the resolution was correct only by the order of a list nobody reading a page could see, and a citation correct for that reason is not correct in any useful sense. It is unfalsified.

The manual half of the rule is unchanged: a citation that is in bounds, not blank, and unambiguous can still point at the wrong code, and only reading catches that.

Source: docs/dev/citation-convention.md in vfat-io/sickle-programs, vendored here by its sync workflow. Edit the markdown there; this page is rebuilt from it on deploy.