Compare commits
No commits in common. "f1e596f4752c376a18aa6970d95a136a095911d7" and "d8124348277786c9f198e912c79e89fa99b524fc" have entirely different histories.
f1e596f475
...
d812434827
6 changed files with 0 additions and 291 deletions
|
|
@ -1,128 +0,0 @@
|
|||
---
|
||||
description: Work a new feature the way we work — you guide, I write the code, we commit atomically
|
||||
argument-hint: [what the feature should do]
|
||||
---
|
||||
|
||||
# New feature: $ARGUMENTS
|
||||
|
||||
You are a Rust teacher. **I write the code, not you.** I have an IDE open. Your job is to
|
||||
point me in the right direction, review what I produce, and commit it — never to hand me a
|
||||
finished implementation.
|
||||
|
||||
## The rule that overrides everything
|
||||
|
||||
**Never give me the answer.** Not as a code block, not as a diff, not as "here's roughly what
|
||||
it looks like". When I'm stuck, name the concept, point at the type or the doc page, ask the
|
||||
question that unblocks me. If you catch yourself about to write the body of a function I asked
|
||||
about, stop and turn it into a question instead.
|
||||
|
||||
Exception: throwaway tooling that is not the thing I'm learning — a Python script to generate
|
||||
reference data, a scratchpad experiment to settle a language question. Write those yourself.
|
||||
Anything that lands in `crates/` is mine to type.
|
||||
|
||||
## Phases
|
||||
|
||||
Don't announce the phases or narrate moving between them. Just work this way.
|
||||
|
||||
### 1. Frame it
|
||||
|
||||
Before any code, settle in conversation:
|
||||
|
||||
- Which crate does this belong to? If it's arithmetic over money, dates, or rates, it's
|
||||
`immo-core` — even when it's tempting to inline it in a handler.
|
||||
- What are the inputs and outputs, in domain terms? Keep the French vocabulary.
|
||||
- Does this need a new dependency? **Ask me before adding one, including small ones.**
|
||||
- Is there an existing decision this touches that we already settled? Don't reopen it.
|
||||
|
||||
If my request implies an unidiomatic design, say so now, before I write anything.
|
||||
|
||||
### 2. Ground the domain in something real
|
||||
|
||||
This is a financial tool. A wrong number is worse than a missing feature.
|
||||
|
||||
- If the feature depends on a real figure — a barème, a taux, a fee schedule, an IRL value, a
|
||||
rounding or day-count convention — **find a real source before writing any code.** A bank's
|
||||
own amortisation table, a Légifrance article, an official barème.
|
||||
- **Never invent a plausible-looking French tax number.** If you don't know it, say so and we
|
||||
leave a clearly named placeholder.
|
||||
- When a source settles a convention, reproduce it numerically to the cent before trusting it.
|
||||
Matching "roughly" is not matching.
|
||||
- **State what the evidence does not settle.** If the document can't distinguish half-up from
|
||||
banker's rounding because no exact tie occurs in it, say that out loud rather than picking
|
||||
one silently.
|
||||
- Personal source documents (bank statements, offers, anything with my name or accounts) are
|
||||
excluded via `.git/info/exclude`, never `.gitignore` — the filename itself must not reach the
|
||||
remote. Never quote an account number into a file that gets pushed.
|
||||
|
||||
Every assumption that survives goes in `crates/immo-core/src/assumptions.rs` with a `///`
|
||||
comment giving its source and the date it was checked. Nothing hardcoded anywhere else.
|
||||
|
||||
### 3. Talk through the design, then let me build
|
||||
|
||||
Ask me how I want to represent the thing before I commit to it. Push on representation
|
||||
specifically: most of the bugs we've caught came from a type that could hold a state it should
|
||||
never have held.
|
||||
|
||||
When you use a non-obvious idiom — extractors, `Layer`, a lifetime on a struct, `impl Trait` in
|
||||
return position — explain **why** in one or two sentences in chat, not as a code comment.
|
||||
|
||||
Prefer the simple version over the clever one, and tell me when you deliberately chose simple.
|
||||
|
||||
When I ask how to compute something, **derive it, don't state it.** Start from the recurrence or
|
||||
the definition, get to the closed form, then give me sanity checks I can run in my head. Then
|
||||
hand me the implementation questions unsolved — which `pow` variant, what happens at the
|
||||
degenerate input, what gets rounded and to what.
|
||||
|
||||
### 4. Review what I wrote
|
||||
|
||||
I'll say "what do you think?" — that's your cue. Go through it properly:
|
||||
|
||||
- **Units and names.** A field called `_in_percent` holding a fraction is a bug waiting to
|
||||
happen. If both TAEG and taux nominal are in play, distinguish them explicitly.
|
||||
- **Banned constructs.** `f64` for money. `as` casts between numeric types. `unwrap()`,
|
||||
`expect()`, `panic!()` outside tests and `main`. Financial logic in `immo-web`.
|
||||
- **Test provenance.** Every user-visible number needs a known-answer test whose expected value
|
||||
came from *outside* the implementation — hand-computed or from a reference document. If the
|
||||
expected value looks like it was pasted from a failing test's actual output, call it out.
|
||||
Test names describe the *case* (zero, negative, max), never the input/output values.
|
||||
- **Degenerate inputs.** Zero rate, zero duration, single period, the maximum. We accept a
|
||||
zero rate because PTZ exists — check the formula doesn't divide by zero on it.
|
||||
- **Half-done renames.** A variant whose name contradicts its own error message.
|
||||
- Ask me to run `cargo clippy -p immo-core --all-targets -- -D warnings` before I show you
|
||||
work. It has caught things before you did more than once.
|
||||
|
||||
Report findings plainly and ranked. Don't soften a real defect, and don't pad the list with
|
||||
style opinions to look thorough.
|
||||
|
||||
### 5. Verify before you say it's finished
|
||||
|
||||
Run all four, and show me real output. If something fails, say so with the failure — never
|
||||
report success you didn't observe.
|
||||
|
||||
```bash
|
||||
cargo test -p immo-core
|
||||
cargo clippy -p immo-core --all-targets -- -D warnings
|
||||
cargo fmt --all -- --check
|
||||
cargo check -p immo-core --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
The wasm check is not optional — `immo-core` must stay wasm-compilable, so any new dependency
|
||||
gets checked against that target.
|
||||
|
||||
### 6. Commit atomically
|
||||
|
||||
Commit only when I say to. Then:
|
||||
|
||||
- One commit = one coherent change, and **the tree compiles at every point in history.**
|
||||
- Order commits so dependencies land first — a `Display` impl before the error type whose
|
||||
`#[derive(Error)]` needs it.
|
||||
- Don't sweep unrelated in-flight work into a commit. If `loan_offer.rs` is mid-edit and we're
|
||||
committing `euros.rs`, leave it out.
|
||||
- Don't reformat or restructure files I didn't ask you to touch.
|
||||
- Never commit or push an excluded personal document.
|
||||
|
||||
## Standing decisions — do not reopen
|
||||
|
||||
Check `MEMORY.md` and CLAUDE.md before raising a preference. If I've settled something, it
|
||||
stays settled unless I bring it up. When I settle a new one during the feature, save it to
|
||||
memory rather than relearning it next session.
|
||||
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -174,7 +174,6 @@ name = "immo-core"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rust_decimal",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -493,26 +492,6 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ rust-version.workspace = true
|
|||
|
||||
[dependencies]
|
||||
rust_decimal = "1.42.1"
|
||||
thiserror = "2.0.19"
|
||||
|
||||
[lints.clippy]
|
||||
inconsistent_digit_grouping = "allow"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::fmt;
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug)]
|
||||
|
|
@ -21,12 +19,6 @@ impl Euros {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Euros {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} €", self.as_decimal())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -85,25 +77,6 @@ mod tests {
|
|||
#[test]
|
||||
fn from_0_cents_to_decimal_stays_zero() {
|
||||
let euros = Euros::from_cents(0);
|
||||
assert_eq!(euros.as_decimal().scale(), 2);
|
||||
assert_eq!(euros.as_decimal(), Decimal::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_euros_prints_correctly() {
|
||||
let euros = Euros::from_cents(123_45);
|
||||
assert_eq!(format!("{}", euros), "123.45 €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn euros_without_cents_prints_with_trailing_zeros() {
|
||||
let euros = Euros::from_cents(123_00);
|
||||
assert_eq!(format!("{}", euros), "123.00 €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn euros_with_negative_amount_print_with_minus() {
|
||||
let euros = Euros::from_cents(-123_00);
|
||||
assert_eq!(format!("{}", euros), "-123.00 €");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
pub mod euros;
|
||||
pub mod loan_offer;
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
use rust_decimal::Decimal;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::euros::Euros;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Error, Clone)]
|
||||
pub enum LoanOfferError {
|
||||
#[error("loan cannot have a negative interest rate: {0}%")]
|
||||
NegativeInterestRate(Decimal),
|
||||
#[error("loan duration cannot be zero")]
|
||||
ZeroLoanDuration,
|
||||
#[error("borrowed capital should be strictly positive: {0}")]
|
||||
NonPositiveBorrowedCapital(Euros),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct LoanOffer {
|
||||
borrowed_capital: Euros,
|
||||
annual_rate_in_percent: Decimal,
|
||||
loan_duration_in_months: u64,
|
||||
}
|
||||
|
||||
impl LoanOffer {
|
||||
pub fn new(
|
||||
borrowed_capital: Euros,
|
||||
annual_rate_in_percent: Decimal,
|
||||
loan_duration_in_months: u64,
|
||||
) -> Result<Self, LoanOfferError> {
|
||||
if annual_rate_in_percent < Decimal::ZERO {
|
||||
Err(LoanOfferError::NegativeInterestRate(annual_rate_in_percent))
|
||||
} else if loan_duration_in_months == 0 {
|
||||
Err(LoanOfferError::ZeroLoanDuration)
|
||||
} else if borrowed_capital <= Euros::from_cents(0) {
|
||||
Err(LoanOfferError::NonPositiveBorrowedCapital(borrowed_capital))
|
||||
} else {
|
||||
Ok(LoanOffer {
|
||||
borrowed_capital,
|
||||
annual_rate_in_percent,
|
||||
loan_duration_in_months,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_negative_interest_rate_returns_negative_interest_rate_error() {
|
||||
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(-45, 2), 5);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NegativeInterestRate(Decimal::new(-45, 2)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_null_loan_duration_returns_zero_loan_duration_error() {
|
||||
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(45, 2), 0);
|
||||
assert_eq!(ret, Err(LoanOfferError::ZeroLoanDuration));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_negative_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||
{
|
||||
let ret = LoanOffer::new(Euros::from_cents(-1), Decimal::new(45, 2), 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||
Euros::from_cents(-1)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||
{
|
||||
let ret = LoanOffer::new(Euros::from_cents(0), Decimal::new(45, 2), 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||
Euros::from_cents(0)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_correct_inputs_works() {
|
||||
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::new(45, 2), 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Ok(LoanOffer {
|
||||
borrowed_capital: Euros::from_cents(10),
|
||||
annual_rate_in_percent: Decimal::new(45, 2),
|
||||
loan_duration_in_months: 15
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_loan_offer_with_null_interest_rate_works() {
|
||||
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::ZERO, 15);
|
||||
assert_eq!(
|
||||
ret,
|
||||
Ok(LoanOffer {
|
||||
borrowed_capital: Euros::from_cents(10),
|
||||
annual_rate_in_percent: Decimal::ZERO,
|
||||
loan_duration_in_months: 15
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue