Developer Documentation Style Guide
This guide applies to every page under docs/dev/. It adapts the rules of
ASD-STE100 (Simplified Technical English) to program documentation. The
existing README.md, CLAUDE.md, and TODO.md are design and audit records.
They do not follow this guide, and they are not the audience for it.
The audience for docs/dev/ is an integrator who builds transactions against
the Sickle program. They know Solana. They have not read the EVM contracts, the
security review, or the Rust source.
1. Sentence rules
- Write one idea in one sentence.
- Use at most 20 words in a procedure sentence.
- Use at most 25 words in a description sentence.
- Use the active voice. Name the actor: the owner, the keeper, the program.
- Use the present tense to describe. Use the imperative to instruct.
- Do not use the future tense, the conditional, or "should".
- Do not use a verb in the
-ingform as a noun. Write "before you send", not "before sending". - Use the article. Write "the keeper", not "keeper".
- Do not join two sentences with a semicolon, a dash, or "which".
- Do not use a parenthesis inside a sentence. Put the aside in its own sentence or in a note.
2. Word rules
Use one name for one concept. Take the name from the glossary in section 6.
Do not use a synonym for variety.
Use one meaning for one word. "Close" is a verb. "Open" is a verb. Do not write "close to" or "open to".
Introduce a code identifier once, in backticks, after the plain name. Then use the plain name. Example: "The Sickle account (
SickleAccount) holds the owner key."Approved verbs for program actions: check, create, close, send, sign, pay, charge, transfer, return, reject, accept, emit, read, write, derive.
Do not use these words. They are design vocabulary, not integrator vocabulary:
Do not write Write instead leg instruction, step gate, gated check, checks pin, pinned the program checks that X equals Y bind, bound the program requires envelope the signed limits faithful, parity (omit, or move to a design note) strictly safer (omit) defense in depth (omit) load-bearing (omit) fail-closed the program rejects drain remove all liquidity re-add, zap-in add liquidity custody the Sickle holds twin keeper instruction machine (intent machine) authorization flow surface instruction set Write numbers as digits. Put every limit, cap, and size in a table, not in a sentence.
Write units after the number with a space: 500 bps, 1232 bytes, 256 KiB.
3. Paragraph rules
- Write one topic in one paragraph.
- Write at most 6 sentences in one paragraph.
- Start the paragraph with the sentence that carries the topic.
- Put a warning before the step it applies to, as its own paragraph, with the label Warning: or Note: in bold.
4. Page structure
Every page starts with one sentence that says what the page covers.
An instruction page uses this template, in this order, with these headings:
# <instruction_name>
<One sentence: what the instruction does.>
## Who can call it
## Arguments
## Accounts
## What the program checks
## What the program does
## Fees
## Events
## Errors
## Example
## Related
- Who can call it names the signer role. Use the glossary role names.
- Arguments is a table: name, type, meaning.
- Accounts is a table in on-chain order: index, name, signer, writable, meaning. Mark a derived address with its seeds in the meaning column.
- What the program checks is a numbered list, in the order the program runs the checks. Each item names the error the program returns.
- What the program does is a numbered list of the effects, in order.
- Fees is a table, or the sentence "This instruction charges no fee."
- Events is a table: event name, fields.
- Errors is a table: error name, cause, fix.
- Example is one code block from the builder in
scripts/builder/. - Related is a bullet list of links.
Omit a heading only if the section has nothing to say. Do not merge sections.
A layer page documents one layer of the program for a developer who reads the code next. It uses this template:
# <Layer name>
<One sentence: what the layer is responsible for.>
## Files
## Responsibilities
## Public items
## How a call flows through it
## Invariants the layer owns
## Tests
## Simplification and audit notes
## Related
- Files is a table: path, lines, one-line purpose.
- Responsibilities is a bullet list. One responsibility per bullet.
- Public items is a table per file: item, kind (fn, struct, trait, const),
one-line meaning. List every
pubitem. Group private helpers under one row per file if they matter. - How a call flows through it is a numbered list that follows one
representative call from entry to exit, with a
path:linefor each step. - Invariants the layer owns is a numbered list. Each item states the rule, the check that enforces it, and the error it returns.
- Tests is a table: test location, what it covers. Name gaps.
- Simplification and audit notes is a numbered list of concrete
observations for the refactor and audit-prep pass. Each note is a
hypothesis about the code, not an instruction to change it. Verify the
claim before acting on it. When the claim turns out to be wrong, record what
was actually true in the note, because the wrong version was derived from the
code's surface and the next reader will derive it again. Notes written this
way have been wrong in four ways that would each have caused a regression:
counting copies that were not copies, proposing a fix that was wrong rather
than blocked, and calling an item dead when its only caller was the
compiler. Each item names a
path:line, states what is there, and states why it matters. Categories: duplication, non-idiomatic Rust or Anchor, dead code, unclear ownership of a check, a check that lives in the wrong layer, a comment that no longer matches the code, missing tests. Do not propose a fix in this section. Do not speculate. If you did not read the lines, do not write the item. Every open note starts with a visible stable ID such as L03-N020. The ordered-list number is presentation only. Allocate an unused ID for a new note and carry the ID unchanged when moving that note under### Resolved; never recycle an ID. Resolved records that predate this convention have bootstrapRIDs such as L03-R001. Historical prose written before stable IDs may call out its positional numbering explicitly, but new references use the stable ID. - Related links the neighboring layer pages.
5. Comparisons with the EVM contracts
Do not compare with the EVM contracts in the main text. If a comparison
helps, put it at the end of the page under the heading ## Design note, as
one paragraph, with a link to the matching entry in CLAUDE.md.
6. Glossary
Use these names. The code identifier appears once per page, on first use. The five instruction classes partition the 65 instructions: 24 + 15 + 12 + 4 + 10.
| Plain name | Code identifier | Meaning |
|---|---|---|
| Sickle | SickleAccount | The program account that holds positions and vaults for one owner. |
| owner | SickleAccount.owner | The wallet that created the Sickle. Only the owner can call owner instructions. |
| manager | SickleAccount.manager | A second key the owner names. It can call keeper instructions only. |
| keeper | global automator, custom automator | A key the admin or the owner allows to call keeper instructions. |
| admin | GlobalConfig.admin | The key that sets fees, the allowlist, and the guardian. |
| guardian | GlobalConfig.guardian | A key that can pause the program. It cannot unpause. |
| global config | GlobalConfig | The program-wide settings account. |
| vault | token account owned by the Sickle | A token account that the Sickle owns for one mint. |
| position | venue position account | An Orca, Raydium, or Meteora liquidity position that the Sickle owns. |
| position settings | PositionSettings | The owner's record of what a keeper may do with one position. |
| venue | Protocol | A supported liquidity program: Orca, Raydium, Meteora, or Jupiter. |
| allowlist | GlobalConfig protocol program slots | The admin's list of trusted program IDs, one per venue. |
| owner instruction | 20 venue instructions (orca_* 7, raydium_* 6, meteora_* 7, without _for or _intent_), sweep_token, set_position_settings, unset_position_settings, jupiter_swap | An instruction only the owner may sign. 24 in total. Three of them also run in intent mode for the executor: orca_increase_liquidity, raydium_increase_liquidity, jupiter_swap. |
| keeper instruction | the 12 *_for instructions and the 3 meteora_intent_* legs | An instruction that acts on a position for the owner. The *_for set accepts the owner, the manager, or an authorized keeper. The meteora_intent_* legs and the intent mode above accept the executor named in the authorization. 15 in total. |
| admin instruction | initialize_global_config, migrate_global_config, transfer_admin, accept_admin, set_fee, set_fee_collector, set_guardian, pause, unpause, set_protocol_program, set_global_automator, revoke_global_automator | An instruction gated on the admin, the guardian (pause), the upgrade authority (initialize_global_config), or the pending admin (accept_admin, instructions/admin/accept_admin.rs:15). 12 in total. Never pause-gated. |
| Sickle instruction | initialize_sickle, set_manager, set_custom_automator, create_sickle_ata | Creates or configures a Sickle. create_sickle_ata is permissionless. 4 in total. Never pause-gated. |
| authorization instruction | initialize_intent_domain, set_intent_session, preauthorize_intent, preauthorize_dlmm_intent, cancel_intent_authorization, cancel_dlmm_intent_authorization, expire_intent_authorization, expire_dlmm_intent_authorization, finalize_intent, finalize_dlmm_intent | Creates, cancels, expires, or finalizes a keeper authorization. Callers vary: upgrade authority, owner, session key, executor, or anyone. 10 in total. Only the two finalizers are pause-gated. |
| authorization | IntentAuthorization | The signed limits for one keeper action. Created in transaction A. |
| action | ActionIntent | The temporary record that tracks one keeper action. Created in transaction B. |
| session key | IntentSession | A key the owner allows to sign authorizations. |
| fee | FeeAction, fee_bps | The protocol fee for one action type, in bps of the measured amount. |
| fee collector | GlobalConfig.fee_collector | The account that receives fees. |
| tick range | tick_lower_index, tick_upper_index | The price range of a CLMM position. |
| bin range | lower_bin_id, upper_bin_id | The price range of a Meteora DLMM position. |
| event | Anchor #[event] | A typed record the program emits through a self-CPI and a log line. |
6a. Citations
Name the symbol; cite a line only where the line is the subject. That is
the adopted convention, and citation-convention.md carries it with the
measurements behind it. A function, type, constant or trait is found by its
name and survives every edit that does not rename it, so a line number beside
one is a liability with no reader. Cite a line for an offset constant, for one
check being contrasted with another, or for a claim about position. Audit notes
keep their line numbers: a note records a defect that must stay findable at the
commit it was written against.
Where a symbol or filename resolves to more than one definition, qualify it and record the ambiguity as a duplication note. Without the second half the qualifier becomes a comfortable way to live with a duplicate.
A citation is path:line or path:line-line, relative to the repository root
or to a directory the reader can infer. Three rules, each learned from a defect
that reached main.
- A range must start on the line the sentence means, not near it. Citing the blank line before a function, or the closing brace after one, reads as correct and is not. The checker rejects a blank start for this reason.
- Verify a citation by reading the cited line. A line number can be inside the file and still point at a blank line, at unrelated code, or into a test module. Bounds are necessary and not sufficient.
- A bare continuation such as "colon 120 dash 140" is a citation too, and so is a bare span
in a table's Lines column and a line count in a Files table. A comma
list such as
family.rs:17,109,182is one citation with several numbers, and the checker reads every one of them. It did not until 2026-09-10: a pattern that expects the closing backtick right after the first number reads one number and ignores the rest, and five stale numbers in03-shared-runners.mdlived in that blind spot. The example above is a real citation for the same reason every other one on this page is. The checker follows continuations by inheriting the file last named on the line; a table cell whose file is only implied needs the path written out. A bare filename that resolves to two files is unresolvable, not a guess. When line numbers move, remap the continuations as well; a tool that matches on the file prefix does not see them, and a bare citation follows the previous citation, not the subject of the sentence. - Re-verify after any file move or reorganisation, not just after an edit to the cited file. Moving code in a file the page does not mention still shifts the lines the page cites.
Before a checker is allowed to drive an edit, read a sample of what it calls wrong, not a sample of what it calls right. Four checks written during this pass were wrong in the direction of manufacturing work: a span check that fired on 109 correct citations, a Lines-column check that flagged 8 correct cells because an item's span rightly includes its attributes, a rule that advanced every blank-line citation downward when four of them meant the line above, and one that reported a single-line span for a multi-line function. Each was caught by reading the failures. A check that invents work is as useless as one that never fires, and it is more expensive, because someone acts on it.
A citation under a Resolved heading is historical: it describes the code as it was before the note was closed, so it is not expected to match current source and the checker skips those sections. Do not repair one to point at today's lines; that destroys the record of what was fixed.
A change owns every derived value that describes it. The rule is wider than citations. It covers line numbers, span lengths, call-site counts, table column sums, note totals, and sentences about test coverage. One pass removed 51 lines from a single file. Repairing what that invalidated took 20 edits across four pages: 8 citations a checker can find, 9 it cannot, and 3 counts.
Those 9 are why rule 1 is manual. The clearest was a public-items table row naming two traits the same change had deleted. Its citations still landed on live code, so every structural check passed the row, and the sentence was false. A valid citation carrying a false sentence is the defect no tool finds.
Run the citation audit after the last edit to a file, not after the code edit. Per-commit is not sufficient, and the tempting order is the wrong one. One commit here inserted three helpers, ran the audit, then added the helpers' doc comments, then committed. The doc comments shifted 22 citations that the audit had just cleared, and they were caught only by the next commit's run. A doc comment feels like commentary rather than a change; to a line number it is the same thing.
Run a content remapper only against an unmodified corpus. Such a tool anchors each old citation's text and reports where it moved. It is correct only while every citation still refers to the old file. Repairing the ones you noticed and then sweeping makes it read corrected numbers as stale ones and return confident nonsense. Sweep first. Repair second.
A fix to a check is itself a check, so test it. One script reported "identical" whenever its expansion failed, and gained a guard requiring a zero exit and non-empty output. The guard was wrong. A crate that does not compile exits 0 and prints a two-word stub, which is not empty. Two people read that guard and called it correct before anyone ran it. Wiring its replacement then introduced a worse defect, an abort path that deleted the author's uncommitted work, caught by reading the diff rather than by any test.
Read what a tool declines to answer. A checker has three outputs and all three need reading: what it flags, what it clears, and what it could not resolve. The third looks like nothing happened. A line remapper built from a diff maps only the lines that survived unchanged, so the citations it skips are exactly the ones pointing at the lines the commit edited. Sixteen were skipped silently in one commit here. The unmapped set is not a residue. It is the set that most needs reading.
Read a command's own exit status, not a pipeline's. After cmd | grep ...,
$? holds grep's status. A failing command then reads as a pass.
An insertion anchored on a line lands inside whatever structure that line belongs to. Anchoring is how a sentence ends up in the wrong place, and the symptom is never at the anchor. Three instances in one pass, all the same shape: a paragraph anchored on a macro's first line landed inside its doc comment and became part of the generated documentation; a sentence anchored mid-paragraph in the README split a sentence in half; and a bullet anchored on another bullet's first line was inserted between that bullet and the continuation lines carrying its citations, which orphaned them. Anchor on the structure you mean to extend, not on a line you found inside it, and read the lines after the insertion point as well as the ones before.
A range ends where its item ends, not at the nearest line with content.
Pulling an end back off a blank only proves the new end is not blank. Most
corrections land on the item's closing brace, and a mechanical sweep gets those.
The rest have to be read, because a range can overrun onto a section separator
comment, onto the following declaration, or onto a line in the wrong file when
a bare name resolves under more than one search root. Write a leading / on
such a path, /Cargo.toml for the workspace manifest against the program's.
The counts are deliberately not given. An earlier version of this paragraph carried them, one later fix moved a correction between the two groups, and the paragraph became a stale derived value inside the rule about stale derived values.
scripts/dev/check-doc-citations.py covers the part of this that can be
checked exactly: bounds, that no citation starts or ends on a blank line, the
layer 01 instruction table against lib.rs, every Lines cell in a Files
table recomputed from its files, every open audit note that claims its own
resolution, every bare name that resolves under more than one search root,
every Sites count in the layer 06 error inventory, the total quoting them, and
any prose on that page restating either, the trybuild fixture inventory against
the suite that registers it, every item-range row against the item its row
names, and every citation that names a function against the file it cites,
which must still mention that name.
A blank start or end is the signature of a citation that drifted a line or two.
It runs in CI on every commit and from scripts/dev/hooks/pre-commit, so a
stale citation no longer depends on someone remembering to type the command.
6.1 What the gate cannot judge
Whether a citation points at the right thing is mostly rule 1, and manual. The boundary is measured, not assumed, so that nobody re-derives it. A citation that names an item immediately before the number admits one checkable claim. The stronger claims fire on correct prose.
| Claim | Citations | Failing | Gated |
|---|---|---|---|
| The cited file mentions the named function | 201 | 0 | yes |
| The cited file declares that function | 201 | 27 | no |
| The cited lines mention the item the prose names | 320 | 57 | no |
| The cited file mentions the type the prose names | 119 | 5 | no |
| The prose names no item beside the number | 2,050 | n/a | no |
The counts are citations as the checker matches them, so a comma list counts once; 2,370 match in the live pages. Read the four refusals as four different facts.
The gated claim is word presence, not definition, and that is deliberate.
Requiring the cited file to declare the function fires 27 times on a corpus
with nothing wrong in it: the pages name declare_id and cite lib.rs, name
trybuild and cite the manifest that depends on it, name checked_add and
cite the allocator that calls it, name sweep_token and cite the doc comment
that explains it. All correct. A file that merely calls or documents a function
is a legitimate thing to cite, so the weaker claim is the one that holds.
The line-level claim breaks on the ordinary idiom of naming a function and citing the line inside it that matters, so most of its 57 are correct. The type-level claim breaks because prose names a type to say what the code does with it, not where it lives: all 5 are correct prose. The last row is the majority, and it carries no mechanical claim at all.
The gated row covers two failures that were previously invisible: a function that moved to another file, and a function that was renamed. Both leave every citation in bounds and on a real line. Neither survives the file losing the name entirely, which is what this reads.
Two smaller boundaries, both measured. A name merely NEAR a citation is not the
subject of it, so only two shapes count: `name` (`file:12`) and
`name` at `file:12`. And a function generated by a macro is absent from
the file that invokes it, so the check also reads the body of each macro the
cited file uses.
7. Checklist before you publish a page
- Every sentence has one actor and one verb.
- No sentence is longer than 25 words.
- No word from the table in section 2.6 appears.
- Every number is in a table.
- Every code identifier appears once with its plain name before it.
- The page follows the template in section 4.
- Every citation you added or moved was verified by reading the cited line.
python3 scripts/dev/check-doc-citations.pypasses.- The page passes
prettier --check.