Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Any agent should be able to take a product PR and update these docs without a hu
2. Find each fact's canonical page (map below, plus `git grep`). Edit only that page; update other pages' links if the fact moved, never restate it.
3. Verify every claim you write against the code (rules below). PR descriptions and existing docs prose are not sources.
4. If a file or heading moves: grep for the old path and old `#anchor`, retarget every inbound link, and update the sidebar in `vocs.config.ts` (URLs derive from file paths under `src/pages/`).
5. Check your work: `node scripts/check-prose.mjs <paths>` (findings are warnings needing judgment, not automatic failures), `pnpm build` (validates every internal link), and read the `.md` twin (`curl localhost:5173/<path>.md`); the twin is what agents consume.
5. Check your work: `node scripts/check-ste.mjs <paths>` (blocking ASD-STE100 checks), `node scripts/check-prose.mjs <paths>` (warnings needing judgment), `pnpm build` (validates every internal link), and read the `.md` twin (`curl localhost:5173/<path>.md`); the twin is what agents consume.
6. Never commit without explicit approval from the human in the session.

## Accuracy
Expand Down Expand Up @@ -58,19 +58,20 @@ If a change moves a fact's canonical home, update this table in the same PR.

## Terminology

- **Use the controlled terms in [`STE-TERMS.md`](STE-TERMS.md).** Add a product term there before you introduce it in user-facing docs. Use the listed part of speech and meaning.
- **Core concepts are Team, Member, Account** (not "Person" or "User"; those nouns don't name product entities in the docs).
- **The product is "the app"** at app.splits.org (teams.splits.org is deprecated). Write "onchain", "offchain", and "crosschain" (no hyphens). No marketing jargon: be explicit and precise about what things are.
- **Capitalization: product-named entities are proper nouns; generic concepts are lowercase.** Capitalize roles (Owner, Member), named accounts (Root, Treasury), and feature names (Earn, Automated Earn, Splits Connect, Recovery-the-settings-surface). Lowercase concepts: signer, signing key, passkey, threshold, account, team, member-the-person, module, automation, invoice, memo. The pair this enables: "member" = a person in a team; "Member" = the role.
- **"Signing key", not bare "key"**, whenever precision matters (definitions, invariants, table cells). Bare "key" is fine once the page has established context (e.g. within `/members/keys`). A **signer** is always account-relative: a signing key added to an account's signer set. Don't use "signer" for a key that isn't on an account.
- **"the Root" / "the Treasury" in prose; bare "Root" / "Treasury" in table cells.** Table cells carry no leading articles and no explanations; explanations live in surrounding prose.
- **"Wallet" means an external EOA wallet** (recovery wallets, MetaMask, hardware wallets), never a Splits account.
- **Em dashes: never, anywhere.** List items and definition lists use a colon separator (`` `command`: description ``); in prose, a colon, period, comma, semicolon, or parentheses replaces the em dash. The prose linter flags every em dash.
- **Em dashes and semicolons: never, anywhere.** List items and definition lists use a colon separator (`` `command`: description ``). In prose, use a colon, period, comma, or parentheses. The prose linters flag these characters.
- **"Email support"** (no address) is the phrasing for manual/support-gated processes.
- **"Team" → "workspace" rename is planned** in the product. Docs keep saying "team" until the product ships the rename, then migrate in one pass (prose + `/teams/` URLs + section name).

## Voice & formatting

- **Facts in declarative present tense; procedures in second person** ("you must be an Owner", "go to…").
- **Facts in declarative present tense; procedures in the imperative form** ("Go to…"). Give one instruction per numbered step unless actions occur at the same time.
- **UI elements in italics**: button and control labels (*Invite member*, *Reset signers*, *Require memos*). **Settings paths with `>`**: Settings > Members. **In-page click chains with `→`**: three dots → *Verify signer*.
- **Bold** for: the term a page defines (first use), negative invariants, and scope names in command lists (**Read** scope).
- **Callouts**: `:::note` sparingly. Beta features get exactly: "This feature is in beta. Email support to enable it for your team."
Expand All @@ -79,6 +80,18 @@ If a change moves a fact's canonical home, update this table in the same PR.
- **Cut anything that can be removed without losing meaning.** No welcome fluff, no roadmap promises, no restating what a link target already says. Answer first.
- **Every sentence must be falsifiable.** If a clause tells the reader how to feel about a fact instead of stating the fact ("seamlessly", "so you can…"), cut it or replace it with the mechanism. Test: delete the clause; if only persuasion is lost, it was jargon.

## ASD-STE100 human review

The deterministic checker enforces sentence length, paragraph length, contractions, and punctuation. `pnpm build` rejects all findings. It cannot decide whether technical prose uses words correctly. For every changed page, also verify:

1. Use words from the ASD-STE100 dictionary or technical terms from `STE-TERMS.md`. Use each word only with its approved meaning and part of speech.
2. Keep multi-word nouns to three words. If an official technical name is longer, write it in full first and define a short form.
3. Use simple verb forms and active voice. Use passive voice only in descriptive text when the agent is unknown.
4. Use an `-ing` form only as a technical noun or as a modifier in a technical noun.
5. Give each descriptive sentence one subject. Give each paragraph one topic.
6. Start each procedural step with a command. Put only one instruction in a sentence unless actions occur at the same time.
7. Read the rendered page and its `.md` twin. Confirm that headings, lists, links, and UI labels preserve the intended meaning.

## Links

- **First mention links, later mentions stay plain.** The first mention of another page's concept links to its canonical home; repeat mentions on the same page and mentions inside table cells stay plain (except a table's designated link column, e.g. "Learn more").
Expand Down
60 changes: 60 additions & 0 deletions STE-TERMS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Splits controlled terminology

This list controls Splits-specific words that appear in user-facing documentation. Use each term only with the part of speech and meaning shown here. The list supplements the ASD-STE100 Issue 9 dictionary. It does not replace that dictionary.

An unapproved word can be a technical noun, part of a technical noun, or an eligible technical verb. Do not use a product term as another part of speech. Add a term here before you introduce it in the docs.

## Product terms

| Term | Part of speech | Meaning and usage |
| --- | --- | --- |
| account | Technical noun | A Splits smart account that holds assets. Do not call it a wallet. |
| automation account | Technical noun | An account with a policy that processes deposits. Use the full term at first use. |
| EOA | Technical noun | An externally owned Ethereum account. Define the abbreviation at first use on each page. |
| external account | Technical noun | An address outside Splits that a team watches. It has no signing authority in Splits. |
| Member | Technical noun | The product role named Member. Capitalize it. |
| member | Technical noun | A person who belongs to a team. Lowercase it. |
| module | Technical noun | An onchain program that can execute approved calls from an account. |
| operating account | Technical noun | A general-purpose account that a team uses for transactions. |
| Owner | Technical noun | The product role named Owner. Capitalize it. |
| passkey | Technical noun | A type of signing key that is stored in a password manager. |
| proposal | Technical noun | A transaction request that waits for the required approvals. |
| recovery signer | Technical noun | An EOA that controls the Root at the recovery threshold. |
| Root | Technical noun | The self-owned account that recovery signers control. Write “the Root” in prose. |
| signer | Technical noun | A signing key that is in one account's signer set. Do not use this term for all signing keys. |
| signer set | Technical noun | The signing keys that can approve transactions for one account. |
| signing key | Technical noun | A passkey or EOA that belongs to a member. Use this term instead of “key” when precision is necessary. |
| smart account | Technical noun | An account implemented by a smart contract. Use this term instead of “smart wallet.” |
| team | Technical noun | The product unit that contains members, accounts, and one set of books. |
| threshold | Technical noun | The number of signer approvals that an account requires. |
| transaction | Technical noun | An onchain action from an account. |
| Treasury | Technical noun | The primary team account. Write “the Treasury” in prose. |

## Blockchain modifiers and verbs

| Term | Part of speech | Meaning and usage |
| --- | --- | --- |
| onchain | Technical-noun modifier | Use only in a technical noun such as “onchain state.” Otherwise write “on the blockchain.” |
| offchain | Technical-noun modifier | Use only in a technical noun such as “offchain metadata.” Otherwise write “outside the blockchain.” |
| crosschain | Technical-noun modifier | Use only in a technical noun such as “crosschain transaction.” Otherwise write “between networks.” |
| bridge | Technical verb | Transfer an asset between blockchain networks. Use only for this action. |
| offramp | Technical verb | Convert a crypto asset to fiat currency and send it to a bank account. |
| onramp | Technical verb | Convert fiat currency from a bank account to a crypto asset. |
| swap | Technical verb | Exchange one token for another token. |

## Do not use

| Avoid | Use |
| --- | --- |
| bootstrap | create, set up |
| finicky | does not operate correctly, or state the observed behavior |
| flip side | result, limitation |
| flops | fails |
| folks | people, members, team members |
| misbehaves | fails, disconnects, or state the observed behavior |
| smart wallet | smart account |
| workspace | team (until the product rename ships) |

## Human review

The deterministic checker cannot validate approved meanings, active voice, noun clusters, or technical-term eligibility. For each changed page, complete the human STE review in `CLAUDE.md` after the checker passes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vocs dev",
"build": "vocs build",
"build": "node scripts/check-ste.mjs && vocs build",
"preview": "vocs preview",
"start": "node dist/serve-node.js"
},
Expand Down
149 changes: 149 additions & 0 deletions scripts/check-ste.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env node
// Deterministic ASD-STE100 checks for user-facing MDX prose.
//
// Usage:
// node scripts/check-ste.mjs <path...>
// node scripts/check-ste.mjs --changed <base-ref>
//
// This checker intentionally leaves vocabulary, noun clusters, voice, meaning,
// and one-action procedure review to a human. See STE-TERMS.md and CLAUDE.md.

import { execFileSync } from 'node:child_process'
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'

const CONTRACTION = /\b(?:aren['’]t|can['’]t|couldn['’]t|didn['’]t|doesn['’]t|don['’]t|hasn['’]t|haven['’]t|isn['’]t|it['’]s|shouldn['’]t|that['’]s|there['’]s|they['’](?:re|ve|ll)|wasn['’]t|we['’](?:re|ve|ll)|weren['’]t|won['’]t|wouldn['’]t|you['’](?:re|ve|ll))\b/gi
const sentenceSegmenter = new Intl.Segmenter('en', { granularity: 'sentence' })

function walk(path, files = []) {
if (!statSync(path).isDirectory()) return path.endsWith('.mdx') ? [...files, path] : files
for (const entry of readdirSync(path)) {
const item = join(path, entry)
if (statSync(item).isDirectory()) walk(item, files)
else if (item.endsWith('.mdx')) files.push(item)
}
return files
}

function changedFiles(base) {
const output = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACMR', `${base}...HEAD`], { encoding: 'utf8' })
return output.split('\n').filter((path) => path.startsWith('src/pages/') && path.endsWith('.mdx'))
}

function cleanInline(value) {
return value
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/`[^`]+`/g, ' IDENTIFIER ')
.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, ' UI_LABEL ')
.replace(/“[^”]*”|"[^"]*"/g, ' QUOTED_TEXT ')
.replace(/\([^()]*\)/g, ' PARENTHETICAL_TEXT ')
.replace(/<[^>]+>/g, ' ')
.replace(/[>*_~]/g, '')
.replace(/\s+/g, ' ')
.trim()
}

function extractBlocks(path) {
const lines = readFileSync(path, 'utf8').split('\n')
const blocks = []
let paragraph = []
let frontmatter = false
let code = false

function flush() {
if (paragraph.length) {
blocks.push({ type: 'descriptive', paragraph: true, line: paragraph[0].line, text: cleanInline(paragraph.map(({ text }) => text).join(' ')) })
}
paragraph = []
}

lines.forEach((raw, index) => {
const line = index + 1
const trimmed = raw.trim()
if (line === 1 && trimmed === '---') { frontmatter = true; return }
if (frontmatter) { if (trimmed === '---') frontmatter = false; return }
if (/^\s*```/.test(raw)) { flush(); code = !code; return }
if (code || /^\s*(?:import|export)\b/.test(raw)) return
if (!trimmed || /^\s*#{1,6}\s/.test(raw) || /^\s*:::\w*/.test(raw) || /^\s*\|/.test(raw) || /^\s*<\/?[A-Z]/.test(raw)) {
flush()
return
}

const list = raw.match(/^\s*(?:[-+*]|\d+\.)\s+(.*)$/)
if (list) {
flush()
blocks.push({
type: /^\s*\d+\./.test(raw) ? 'procedural' : 'descriptive',
vertical: true,
line,
text: cleanInline(list[1]),
})
return
}
paragraph.push({ line, text: trimmed })
})
flush()
return blocks.filter(({ text }) => text)
}

function sentences(text, splitColon) {
// Under STE Rule 8.4, a colon in a vertical list ends a sentence for word count.
return [...sentenceSegmenter.segment(splitColon ? text.replaceAll(':', '.') : text)]
.map(({ segment }) => segment.trim())
.filter(Boolean)
}

function wordCount(text) {
return (text.match(/\b[\p{L}\p{N}]+(?:[-'][\p{L}\p{N}]+)*\b/gu) || []).length
}

function inspect(path) {
const findings = []
for (const block of extractBlocks(path)) {
const parts = sentences(block.text, block.vertical)
if (block.paragraph && parts.length > 6) {
findings.push({ line: block.line, rule: '6.6', message: `paragraph has ${parts.length} sentences; maximum is 6`, source: block.text })
}
for (const sentence of parts) {
const limit = block.type === 'procedural' ? 20 : 25
const count = wordCount(sentence)
if (count > limit) {
const rule = block.type === 'procedural' ? '5.1' : '6.3'
findings.push({ line: block.line, rule, message: `sentence has ${count} words; maximum is ${limit}`, source: sentence })
}
if (sentence.includes(';')) findings.push({ line: block.line, rule: '8.1', message: 'semicolon is not permitted', source: sentence })
for (const match of sentence.matchAll(CONTRACTION)) {
findings.push({ line: block.line, rule: '4.2', message: `contraction is not permitted: ${match[0]}`, source: sentence })
}
}
}
return findings
}

const args = process.argv.slice(2)
let files
if (args[0] === '--changed') {
if (!args[1]) throw new Error('--changed requires a base ref')
files = changedFiles(args[1])
} else {
const targets = args.length ? args : ['src/pages']
files = targets.flatMap((target) => walk(target))
}

const findings = []
for (const path of [...new Set(files)].sort()) {
for (const finding of inspect(path)) {
findings.push({ ...finding, path })
}
}

for (const finding of findings) {
console.error(`${finding.path}:${finding.line}: ASD-STE100 ${finding.rule}: ${finding.message}`)
}

if (findings.length) {
console.error(`\n${findings.length} deterministic ASD-STE100 finding(s).`)
process.exit(1)
}
console.log(`ASD-STE100 deterministic checks passed for ${new Set(files).size} file(s).`)
6 changes: 3 additions & 3 deletions src/pages/accounting/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: View, filter, and export all of a team's transactions on the Accoun

# Accounting [View, filter, and export all activity across a team's accounts]

The *Accounting* page shows every inbound and outbound transaction across the team's accounts, filterable and exportable to CSV. To annotate transactions for the books, use [memos](/transactions/memos); add them when creating each transaction.
The *Accounting* page shows every inbound and outbound transaction across the team's accounts, filterable and exportable to CSV. To annotate transactions for the books, use [memos](/transactions/memos). Add them when creating each transaction.

## Filtering

Expand All @@ -18,8 +18,8 @@ The main *Export as CSV* action downloads a transaction-and-tax-lot timeline mat
- **Open tax lots**: lots still open as of the selected period's end date (or now, when there is no end date), matching the account, network, and token filters.
- **Realized gains**: disposals in the selected date range, matching the account filter, with proceeds, cost basis, gain or loss, and holding term.

Large reports are generated in the background and emailed when ready; keep the page open to also download them automatically when they finish. To control which tokens appear in the books at all, see [Spam & tokens](/accounting/spam).
Large reports are generated in the background and emailed when ready. Keep the page open to also download them automatically when they finish. To control which tokens appear in the books at all, see [Spam & tokens](/accounting/spam).

## Historical balances

Setting a date range shows the starting balance, ending balance, and net change for that period; combine with the account(s) filter to scope it to specific accounts. The starting and ending balances break down into per-token holdings on that date.
Setting a date range shows the starting balance, ending balance, and net change for that period. Combine with the account(s) filter to scope it to specific accounts. The starting and ending balances break down into per-token holdings on that date.
Loading