Skip to content
Merged
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
39 changes: 5 additions & 34 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,23 @@

## The mobile backend and the UI are separate concerns

The mobile backend is the Rust under `rust/` and the native interfaces to it:
the UniFFI components, the Kotlin and Swift modules that bind them, and the
build systems that produce them. The UI is everything above that boundary: the
React Native screens and components, the theme, the styles, the colours, and
the user-facing copy.

Keep work on one out of the other. A change to the mobile backend carries no
colour, spacing, typography, or component-styling edit. A UI change carries no
edit to the Rust or its bindings. Where a task genuinely needs both, make them
separate commits and say which is which, so a reviewer can read one without the
other.

Before you finish backend work, read your own diff and split out anything that
is UI. A styling edit that rides along in a backend diff wastes a reviewer's
attention and gets reverted rather than reviewed. This has already cost the
project once: a batch of colour changes was undone inside a mixnet pull request
because it travelled with unrelated backend work.

While you are in either half, watch for places where the boundary is muddy. A
component that reaches into the wallet backend, a backend module that formats
prose meant for a person, a native module that decides what a screen shows:
each is worth naming. Propose the separation as its own change rather than
fixing it inline, and say what it would cost.
The mobile backend (Rust under `rust/`, its UniFFI/Kotlin/Swift bindings, and their build systems) and the UI (React Native screens, theme, styles, colours, copy) are separate concerns: keep each change to one side, and where a task needs both, split them into commits labelled backend or UI so a reviewer can read one without the other. Before finishing, scan your own diff for stray edits from the other half, and when the boundary is muddy (a component reaching into the wallet backend, a backend module formatting human prose), name it and propose the separation as its own change rather than fixing it inline.

## Writing & Code Style

Goal: produce prose and code that reads as if written by a specific, competent human, not by a model. The point is naturalness and fit, not looking exhaustive or safe. When in doubt, commit to a choice and keep it short.
Goal: produce prose and code that reads as if written by a specific, competent human, not by a model. The point is naturalness and accuracy, not looking exhaustive or safe. When in doubt, commit to a choice and keep it short.

### Prose

#### Economy and precision (adapted from Strunk's Elements of Style)

- Omit needless words. Every word must earn its place. Cut filler phrases entirely:
- "the fact that" → delete or restructure
- "in order to" → "to"
- "due to the fact that" → "because"
- "at this point in time" → "now"
- "has the ability to" → "can"
- "there is X that does Y" → "X does Y"
- "essentially" / "basically" / "fundamentally" → almost always delete
- Use the active voice. Prefer "the parser rejects malformed input" over "malformed input is rejected by the parser". Passive voice only when the actor is unknown or irrelevant.
- Put statements in positive form. Say what something is, not what it isn't. "The cache is stale" beats "the cache is not up to date". No "not un-" constructions.
- Use definite, specific, concrete language. "Sync stalls after 40k blocks" beats "performance degrades under certain conditions". If you have a number, a name, or a mechanism, state it.
- One paragraph, one topic. Don't braid two ideas together and rely on connectors to hold them.
- Place emphatic words at the end of the sentence. Don't bury the key claim mid-sentence and trail off with qualifiers.
- Don't overstate. Cut intensifiers ("very", "extremely", "incredibly"). When uncertainty is real, hedge precisely ("untested on mainnet"), not vaguely ("may or may not work").
- Do not explain too much. State the point once. If a sentence adds no new information, delete it.
- Don't be verbose: if you need ":" to explain something, shorten it instead.
- Do not explain too much. State the point once.
- Revise by deletion. When tightening prose, the default operation is removal, not substitution. A shorter draft that says the same thing is strictly better.

#### Punctuation
Expand Down Expand Up @@ -117,6 +87,7 @@ without losing meaning, the "so" was decorative. Delete it.

### Code (all languages)

- Never add inline comments.
- Comment why, not what. No line-by-line narration of obvious operations.
- No tutorial narration ("Now we...", "Step 1:", "First, let's...") and no banner comments (`// ===== HELPERS =====`).
- No docstrings that just restate the signature.
Expand Down
21 changes: 3 additions & 18 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,7 @@ _Avoid_: mode (names the feature, not the state it reports)
**Route**:
The network path a mixnet-only surface resolves to, either the Standing
Client's tunnel or clearnet. zingolib derives it from the indicator, and the
app never sees it. A price fetch that resolves to clearnet is refused, never
sent.
app never sees it.

## Price surface

Expand All @@ -256,25 +255,11 @@ clock), so a healthy cadence never dims, ceiling draws included.
Staleness is a fact about the value's accuracy, not about fetch
mechanics, and it is independent of transport.

**Price consent**:
Selecting Nym, the persisted Mixnet Mode opt-in. It is the single and
only consent for price traffic: the surface fetches on no other
authority, and no fetch failure raises a snackbar. The display currency
chooses what to show, never what may be fetched.
_Avoid_: manual fetch (removed; every fetch is app-initiated); USD
selection as consent (the seeded default authorizes nothing)

**Cadence**:
The consented fetch rhythm: boot, the Nym selection turning on, and
every gate-open return from the background fetch at once, and each
further fetch follows the last at a uniform random delay of five to ten
minutes.

**Ready follow-up**:
The one-shot fetch armed when an unattended fetch is refused during
bootstrap; it fires when the Indicator turns `ready` and is dropped on a
`died` or `off` verdict or on the next background transition. A transient
`unknown` is one failed status poll, not a verdict, and does not drop it.
`died` status or on the next background transition. A transient `unknown`
is one failed status poll, not a settled status, and does not drop it.

## Biometric gate

Expand Down
25 changes: 9 additions & 16 deletions __tests__/priceSurface.cue.tsx → __tests__/priceDisplay.ring.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
/**
* Evidence tests for the PR 1343 review findings in the staleness cue and
* the display-only ring's accessibility. Each test encodes the behavior
* the finding says the surface should have: it fails on the broken code
* and passes once the finding is fixed.
*
* F8 (ring half): the muted arc must stay distinguishable from the track.
* F9: the display-only ring is not an unnamed disabled tap stop, and the
* staleness cue reaches screen readers as a label.
* The staleness cue and the display-only ring's accessibility.
*/
jest.mock('../app/walletBackend', () => ({
__esModule: true,
Expand Down Expand Up @@ -69,7 +62,7 @@ const collect = (
(node.children ?? []).forEach(child => collect(child, hits, pick));
};

test('F8: the stale arc keeps a color of its own, distinct from the track', () => {
test('the stale arc keeps a color of its own, distinct from the track', () => {
const staleCtx = makeCtx({
zecPrice: { zecPrice: 33.33, date: Date.now() - 11 * 60_000 },
});
Expand All @@ -79,7 +72,7 @@ test('F8: the stale arc keeps a color of its own, distinct from the track', () =
expect(ring.props.ringColor).not.toBe(ring.props.trackColor);
});

test('F9: the display-only ring exposes no disabled tap stop', () => {
test('the display-only ring exposes no disabled tap stop', () => {
const freshCtx = makeCtx({
zecPrice: { zecPrice: 33.33, date: Date.now() },
});
Expand All @@ -94,23 +87,23 @@ test('F9: the display-only ring exposes no disabled tap stop', () => {
expect(disabledStops).toEqual([]);
});

test('F9: a stale price reaches screen readers as a label', () => {
test('a stale price reaches screen readers as a label', () => {
const staleCtx = makeCtx({
zecPrice: { zecPrice: 33.33, date: Date.now() - 11 * 60_000 },
});
const view = render(fetcherUi(staleCtx));
expect(view.getByLabelText('price-ring-stale')).toBeTruthy();
});

test('F9: a current price reaches screen readers as a label too', () => {
test('a current price reaches screen readers as a label too', () => {
const freshCtx = makeCtx({
zecPrice: { zecPrice: 33.33, date: Date.now() },
});
const view = render(fetcherUi(freshCtx));
expect(view.getByLabelText('price-ring-live')).toBeTruthy();
});

test('R4: the ring restarts on every refresh cycle, failed ones included', async () => {
test('the ring restarts on every refresh cycle, failed ones included', async () => {
jest.useFakeTimers();
const view = render(fetcherUi(makeCtx())); // every fetch here is refused
await jest.advanceTimersByTimeAsync(0);
Expand All @@ -126,21 +119,21 @@ test('R4: the ring restarts on every refresh cycle, failed ones included', async
jest.useRealTimers();
});

test('R5: a price that never arrived is announced as absent, not stale', async () => {
test('a price that never arrived is announced as absent, not stale', async () => {
const view = render(fetcherUi(makeCtx())); // no price has ever existed
await waitFor(() =>
expect(view.getByLabelText('price-ring-none')).toBeTruthy(),
);
});

test('N8: without the Nym consent no ring counts down to nothing', () => {
test('without the Nym opt-in no ring counts down to nothing', () => {
const view = render(fetcherUi(makeCtx({ nym: false })));
// A countdown beside a surface that will never fetch misleads; the
// unconsented state renders no ring at all.
expect(view.queryByTestId('pricefetcher.ring')).toBeNull();
});

test('P4: a refusing transport hides the ring for the same reason', () => {
test('a refusing transport hides the ring for the same reason', () => {
const view = render(
fetcherUi(
makeCtx({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/**
* Spec tests for the ratified price cadence (ADR 0008, Consequences):
* turning Nym on, a boot, and every gate-open return from the
* background fetch at once, and each further fetch follows the last at
* a uniform random delay of five to ten minutes.
* The ratified price cadence.
*/
jest.mock('../app/walletBackend', () => ({
__esModule: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
/**
* The store's observable contract: snapshot shape and identity, deps
* writes, the disposition switch, and the display-only ring,
* distilled from the reviews of PR 1343. Each test encodes the
* behavior a finding says the surface should have: it fails on the
* broken code and passes once fixed.
*
* H1: withdrawing the consent takes the ring down with the cadence.
* H2: an entry flight with no armed deadline never reads as full.
* H3: (in Send.priceCta.unit) the USD-entry derived ZEC dims when the
* price is stale.
* H4: the ready follow-up never re-arms itself.
* H5: every status key classifies under the exhaustive disposition.
* H6: the driver writes deps when an input moves, not per render.
* H7: the snapshot carries exactly its four read fields.
* H8: the snapshot keeps its identity between emits.
* H9: no any-casts in the price suites and no null in the store.
* H10: the ring is display-only.
* The store's observable contract.
*/
jest.mock('../app/walletBackend', () => ({
__esModule: true,
Expand Down Expand Up @@ -108,7 +92,7 @@ afterEach(() => {
jest.useRealTimers();
});

test('H1: withdrawing the consent takes the ring down', async () => {
test('withdrawing the opt-in takes the ring down', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: 42, error: '' });
const setZecPrice = jest.fn();
Expand All @@ -128,7 +112,7 @@ test('H1: withdrawing the consent takes the ring down', async () => {
expect(view.queryByTestId('pricefetcher.ring')).toBeNull();
});

test('H2: an entry flight with no armed deadline never reads full', async () => {
test('an entry flight with no armed deadline never reads full', async () => {
jest.useFakeTimers();
price.mockImplementation(() => new Promise(() => {})); // in flight
const setZecPrice = jest.fn();
Expand All @@ -147,7 +131,7 @@ test('H2: an entry flight with no armed deadline never reads full', async () =>
expect(ring.props.startProgress).toBeLessThan(1);
});

test('H4: the ready follow-up never re-arms itself', async () => {
test('the ready follow-up never re-arms itself', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: -1, error: 'refused' });
const setZecPrice = jest.fn();
Expand All @@ -168,7 +152,7 @@ test('H4: the ready follow-up never re-arms itself', async () => {
expect(price).toHaveBeenCalledTimes(4);
});

test('H5: every status key classifies under the disposition switch', () => {
test('every status key classifies under the disposition switch', () => {
const dispositions = MIXNET_STATUS_KEYS.map(key => transportDisposition(key));
dispositions.forEach(d =>
expect(['refusing', 'possibleBootstrap', 'serving']).toContain(d),
Expand All @@ -178,7 +162,7 @@ test('H5: every status key classifies under the disposition switch', () => {
expect(dispositions).toContain('serving');
});

test('H6: the driver writes deps when an input moves, not per render', async () => {
test('the driver writes deps when an input moves, not per render', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: 42, error: '' });
const setDepsSpy = jest.spyOn(priceFetcherStore, 'setDeps');
Expand All @@ -198,7 +182,7 @@ test('H6: the driver writes deps when an input moves, not per render', async ()
setDepsSpy.mockRestore();
});

test('H7: the snapshot carries exactly its four read fields', () => {
test('the snapshot carries exactly its four read fields', () => {
expect(Object.keys(priceFetcherStore.snapshot()).sort()).toEqual([
'loading',
'nextFetchAt',
Expand All @@ -207,22 +191,22 @@ test('H7: the snapshot carries exactly its four read fields', () => {
]);
});

test('H8: the snapshot keeps its identity between emits', () => {
test('the snapshot keeps its identity between emits', () => {
const first = priceFetcherStore.snapshot();
expect(priceFetcherStore.snapshot()).toBe(first);
});

test('H9: no any-casts in the price suites and no null in the store', () => {
test('no any-casts in the price suites and no null in the store', () => {
const suites = [
'priceSurface.cadence.tsx',
'priceSurface.cue.tsx',
'priceSurface.lifecycle.tsx',
'priceSurface.trafficGuards.tsx',
'priceSurface.wedges.tsx',
'priceSurface.soleConsent.tsx',
'priceSurface.landings.tsx',
'priceSurface.storeContract.tsx',
'priceSurface.recovery.tsx',
'priceStore.cadence.tsx',
'priceDisplay.ring.tsx',
'priceStore.lifecycle.tsx',
'priceStore.trafficGuards.tsx',
'priceStore.wedgeGuards.tsx',
'priceStore.optIn.tsx',
'priceStore.nativeCall.tsx',
'priceStore.contract.tsx',
'priceStore.recovery.tsx',
'Send.priceCta.unit.tsx',
'PriceFetcher.snapshot.tsx',
];
Expand All @@ -240,7 +224,7 @@ test('H9: no any-casts in the price suites and no null in the store', () => {
expect(new RegExp('\\bnul' + 'l\\b').test(store)).toBe(false);
});

test('H10: the ring is display-only', () => {
test('the ring is display-only', () => {
const ringSource = fs.readFileSync(
path.join(__dirname, '../components/Components/QuoteRefreshRing.tsx'),
'utf8',
Expand Down
Loading
Loading