Sickle Sol

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

  1. Write one idea in one sentence.
  2. Use at most 20 words in a procedure sentence.
  3. Use at most 25 words in a description sentence.
  4. Use the active voice. Name the actor: the owner, the keeper, the program.
  5. Use the present tense to describe. Use the imperative to instruct.
  6. Do not use the future tense, the conditional, or "should".
  7. Do not use a verb in the -ing form as a noun. Write "before you send", not "before sending".
  8. Use the article. Write "the keeper", not "keeper".
  9. Do not join two sentences with a semicolon, a dash, or "which".
  10. Do not use a parenthesis inside a sentence. Put the aside in its own sentence or in a note.

2. Word rules

  1. Use one name for one concept. Take the name from the glossary in section 6.

  2. Do not use a synonym for variety.

  3. Use one meaning for one word. "Close" is a verb. "Open" is a verb. Do not write "close to" or "open to".

  4. 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."

  5. Approved verbs for program actions: check, create, close, send, sign, pay, charge, transfer, return, reject, accept, emit, read, write, derive.

  6. Do not use these words. They are design vocabulary, not integrator vocabulary:

    Do not writeWrite instead
    leginstruction, step
    gate, gatedcheck, checks
    pin, pinnedthe program checks that X equals Y
    bind, boundthe program requires
    envelopethe signed limits
    faithful, parity(omit, or move to a design note)
    strictly safer(omit)
    defense in depth(omit)
    load-bearing(omit)
    fail-closedthe program rejects
    drainremove all liquidity
    re-add, zap-inadd liquidity
    custodythe Sickle holds
    twinkeeper instruction
    machine (intent machine)authorization flow
    surfaceinstruction set
  7. Write numbers as digits. Put every limit, cap, and size in a table, not in a sentence.

  8. Write units after the number with a space: 500 bps, 1232 bytes, 256 KiB.

3. Paragraph rules

  1. Write one topic in one paragraph.
  2. Write at most 6 sentences in one paragraph.
  3. Start the paragraph with the sentence that carries the topic.
  4. 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 pub item. 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:line for 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 bootstrap R IDs 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 nameCode identifierMeaning
SickleSickleAccountThe program account that holds positions and vaults for one owner.
ownerSickleAccount.ownerThe wallet that created the Sickle. Only the owner can call owner instructions.
managerSickleAccount.managerA second key the owner names. It can call keeper instructions only.
keeperglobal automator, custom automatorA key the admin or the owner allows to call keeper instructions.
adminGlobalConfig.adminThe key that sets fees, the allowlist, and the guardian.
guardianGlobalConfig.guardianA key that can pause the program. It cannot unpause.
global configGlobalConfigThe program-wide settings account.
vaulttoken account owned by the SickleA token account that the Sickle owns for one mint.
positionvenue position accountAn Orca, Raydium, or Meteora liquidity position that the Sickle owns.
position settingsPositionSettingsThe owner's record of what a keeper may do with one position.
venueProtocolA supported liquidity program: Orca, Raydium, Meteora, or Jupiter.
allowlistGlobalConfig protocol program slotsThe admin's list of trusted program IDs, one per venue.
owner instruction20 venue instructions (orca_* 7, raydium_* 6, meteora_* 7, without _for or _intent_), sweep_token, set_position_settings, unset_position_settings, jupiter_swapAn 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 instructionthe 12 *_for instructions and the 3 meteora_intent_* legsAn 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 instructioninitialize_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_automatorAn 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 instructioninitialize_sickle, set_manager, set_custom_automator, create_sickle_ataCreates or configures a Sickle. create_sickle_ata is permissionless. 4 in total. Never pause-gated.
authorization instructioninitialize_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_intentCreates, 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.
authorizationIntentAuthorizationThe signed limits for one keeper action. Created in transaction A.
actionActionIntentThe temporary record that tracks one keeper action. Created in transaction B.
session keyIntentSessionA key the owner allows to sign authorizations.
feeFeeAction, fee_bpsThe protocol fee for one action type, in bps of the measured amount.
fee collectorGlobalConfig.fee_collectorThe account that receives fees.
tick rangetick_lower_index, tick_upper_indexThe price range of a CLMM position.
bin rangelower_bin_id, upper_bin_idThe price range of a Meteora DLMM position.
eventAnchor #[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.

  1. 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.
  2. 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.
  3. 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,182 is 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 in 03-shared-runners.md lived 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.
  4. 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.

ClaimCitationsFailingGated
The cited file mentions the named function2010yes
The cited file declares that function20127no
The cited lines mention the item the prose names32057no
The cited file mentions the type the prose names1195no
The prose names no item beside the number2,050n/ano

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.py passes.
  • The page passes prettier --check.

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