From 50883ff0d05a76b2192a9f42f5ec5fcc0102fa7a Mon Sep 17 00:00:00 2001 From: Chayanon Somatanai Date: Thu, 18 Jun 2026 02:57:23 +0700 Subject: [PATCH 1/3] template: Thai bilingual support, visuals, citations, and header polish - Thai font fallback in src/style.typ; CI installs fonts-thai-tlwg so -th sibling files render instead of showing tofu - editorial header (eyebrow, accent rule), two-tone section headings, brand/page footer, end-of-solution mark, and hyphenation - adaptive section menu with Answer / Idea / Application; equations are unnumbered by default - components: cite() for linked theorem references, diagram() for CeTZ figures, plus answer() and numbered() helpers - build-index: drop the difficulty column and skip -th siblings - docs/CONVENTIONS: bilingual sibling naming and a sourcing-results invariant --- .github/workflows/ci.yml | 5 ++ README.md | 17 ++++--- docs/CONVENTIONS.md | 28 +++++++++++ problems/_TEMPLATE.typ | 102 ++++++++++++++++++++++++++------------- scripts/build-index.sh | 28 ++++------- src/components.typ | 49 +++++++++++++++++++ src/style.typ | 7 ++- src/template.typ | 98 ++++++++++++++++++++----------------- 8 files changed, 231 insertions(+), 103 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c18556..c7667e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,11 @@ jobs: with: typst-version: ${{ env.TYPST_VERSION }} + # Thai serif faces (Norasi et al.) for bilingual problem files. Without + # this, Thai text still "compiles" but renders as blank tofu boxes. + - name: Install Thai fonts + run: sudo apt-get update && sudo apt-get install -y fonts-thai-tlwg + - name: Check template compiles run: make template-check diff --git a/README.md b/README.md index 3a2fcc1..a7f2845 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ A personal collection of university-level math problems. Each one is written up as a full solution with the reasoning spelled out and the theorems it leans on cited, then typeset in [Typst](https://typst.app) and compiled to PDF. -One problem per file. Each file keeps its own metadata (title, source, -difficulty, tags), and the problem index and progress count below are generated -from that metadata, so I don't edit them by hand. +One problem per file. Each file keeps its own metadata (title, source, tags), +and the problem index and progress count below are generated from that metadata, +so I don't edit them by hand. Problems written up in Thai live in a `-th` sibling +file and don't get their own index row. ## Topics @@ -35,9 +36,9 @@ their tags. ## Problem index -| # | Problem | Topics | Source | Difficulty | Solved | -|---|---------|--------|--------|------------|--------| -| - | *No problems yet* | | | | | +| # | Problem | Topics | Source | Solved | +|---|---------|--------|--------|--------| +| - | *No problems yet* | | | | @@ -56,6 +57,10 @@ winget install Typst.Typst winget install jqlang.jq ``` +Diagrams use [CeTZ](https://github.com/cetz-package/cetz) (pinned). The first +compile downloads it from the Typst package registry and caches it; CI does the +same automatically. + Then: ``` diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index c8f19b7..95fb854 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -60,6 +60,24 @@ You don't pick the number yourself - `make new T= S=` finds the next free one and copies the template. The pre-commit hook and CI reject any file in `problems/` that doesn't match the pattern (`_TEMPLATE.typ` is exempt). +### Bilingual (Thai) siblings + +A problem can have a Thai-language sibling. It is a separate file (one language +per file, like one problem per file) that **shares the English file's number** +and appends `-th` to the slug: + +``` +calc-001-gamma-integral.typ English (primary, write first) +calc-001-gamma-integral-th.typ Thai sibling, same problem, same number +``` + +The `-th` form still matches the file-name pattern (`-th` is part of the slug), +so no script or CI change is needed. The Thai file does **not** get a new +number from `make new`; copy the English file and rename it by hand. Thai +rendering relies on the Thai serif faces in the font fallback +(see [`../src/style.typ`](../src/style.typ)); CI installs them via +`fonts-thai-tlwg`. + ## Branches ``` @@ -102,6 +120,16 @@ refs(nt): missing number theory prerequisite links `template` and repo-wide `refs` changes can drop the `(-<###>)` part. +## Sourcing results + +A hard invariant for every solution: **any result used but not proved in the +file must be traceable.** A named theorem, rule, or technique you invoke without +proving carries a reference the reader can follow — `cite(name, url: ...)` for an +online source or `cite(name, ref: [Book, §x])` for an offline one. Steps that +follow from earlier lines or your own algebra need no citation; elementary +background (commutativity, basic arithmetic) is assumed, not cited. A `cite` with +no source renders a red `[ref?]` marker so the gap is caught before shipping. + ## Workflow ``` diff --git a/problems/_TEMPLATE.typ b/problems/_TEMPLATE.typ index b88f988..efaeb30 100644 --- a/problems/_TEMPLATE.typ +++ b/problems/_TEMPLATE.typ @@ -3,7 +3,10 @@ // // Before committing: // - compiles cleanly (make compile P=-NNN) -// - every theorem/rule used is named, no skipped steps +// - every imported theorem/rule/technique is cited (cite(name, url:)); no [ref?] +// - no skipped steps — a reader never has to ask "how did that follow?" +// - a diagram is included wherever the math is visual +// - sections in order, only the ones the problem earns (see CONVENTIONS) // - statement is in your own words if the source had prose // - source set, or "" for an original problem // - make index @@ -11,18 +14,20 @@ #import "../src/template.typ": * #import "../src/theorems.typ": theorem, lemma, corollary, proposition, definition, proof, insight, step, remark, example -#import "../src/components.typ": comparison, figure-image +#import "../src/components.typ": comparison, figure-image, cite, numbered, answer, diagram +// For a hand-drawn figure, also uncomment these (CeTZ is pinned; the first +// compile downloads it). Draw to the actual math — see #diagram example below. +// #import "../src/style.typ": default-theme +// #import "@preview/cetz:0.3.4" #show: doc.with( - title: "Problem title here", + title: "Problem title here", // plain text — no $math$ here subtitle: none, // optional one-line subtitle source: "e.g. Putnam 2019 B3", source_url: "https://...", // link to original, or none source_license: "All rights reserved", // or "CC BY-SA 4.0" (Stack Exchange), "CC BY 4.0" // author defaults from src/config.typ; override only if needed date: none, // e.g. "2026-06-16" - difficulty: "medium", // easy | medium | hard - summary: none, // optional one-line abstract under the title tags: ("calculus", "subtopic-1", "subtopic-2"), // theme: (accent: rgb("#0a7d55")), // optional per-problem color override prerequisites: ( @@ -34,53 +39,82 @@ ), ) +// SECTIONS ARE A MENU IN A FIXED ORDER, NOT A CHECKLIST. Only the statement and +// the solution are required. Add the rest *when they earn their place* and keep +// them in the order below — it follows how a reader thinks: what's asked → what's +// the answer → fix notation → the idea → the formal work → check → step back. A +// short evaluation is fine as Statement + Solution alone; don't pad with an empty +// "Reflection". Match the structure to the problem. + +// --- REQUIRED --- + = Problem Statement State the problem precisely. Pure math (equations) may be copied; restate creative word-problem prose in your own words. -= Setup and Notation - -Fix notation, list what is given and what must be shown. Recall any definition -the solution relies on. +// (optional) Answer — for evaluate / find-type problems, show the result up +// front, then prove it below. Skip it when the statement already is the claim. +// #answer[$ integral_0^oo e^(-x) dif x = 1. $] -#definition(title: "Term")[Define any non-standard term used below.] +// --- OPTIONAL, BEFORE the solution (in this order) --- -= Strategy +// = Setup and Notation +// Fix notation and recall definitions — only when the solution carries enough +// machinery that inlining it would clutter the argument. +// #definition(title: "Term")[Define any non-standard term.] -Explain the plan in plain language *before* the formal work: why this approach, -what the obstacle is, and how it is overcome. - -#insight[State the single idea that makes the problem tractable.] +// = Idea +// The plan in plain language before the formal work, ending in the key insight — +// only when the approach is non-obvious enough to deserve motivating. For a short +// problem, an #insight at the top of the solution is enough. +// #insight[The one idea that makes the problem tractable.] +// +// A diagram makes the idea visible — include one whenever the math is geometric: +// #diagram(caption: [What the picture shows.])[ +// #cetz.canvas(length: 1cm, { +// import cetz.draw: * +// let acc = default-theme.accent +// line((-2.5, 0), (2.5, 0), mark: (end: ">")) // x-axis +// line((0, 0), (0, 1.6), mark: (end: ">")) // y-axis +// // ... draw curves / regions / lattices to the actual problem ... +// }) +// ] = Solution -Break the argument into numbered steps. Justify *every* step with the rule or -theorem it uses, and don't skip steps. - -#step(reason: [definition of the object])[ - First move. Show the work. -] - -#step(reason: [name the theorem / rule applied])[ - Next move, with full intermediate algebra: - $ ... = ... $ -] +Show every move — including the algebra between two displayed lines — so the +reader never has to reconstruct a jump. Name the rule each step uses in +`reason:`, and `cite` it with a link whenever it is a named theorem or technique +(skip the link only for elementary algebra). -#step(reason: [why this is valid])[ - Continue until the result is reached. +#step(reason: [definition / first move])[ + Show the work. ] -#proof[ - Optional: a formal proof block for the central claim, ending with QED. +#step(reason: cite("Name of the theorem", url: "https://..."))[ + Next move, with the intermediate algebra spelled out line by line: + $ a &= b \ + &= c \ + &= d. $ ] #theorem[State the result that has now been established.] -= Verification +// --- OPTIONAL, AFTER the solution (in this order) --- -Sanity-check the answer: special cases, units, limits, or a numerical check. +// = Verification +// Sanity-check keyed to the task type: small cases, a limit, an equality case, +// or recomputing by another method. Worth it whenever the answer isn't +// self-evidently right. -= Reflection +// = Reflection +// Generalizations, alternative methods, and common pitfalls — within mathematics. +// Only with a genuine point to make. A reference equation can be numbered for +// back-reference: #numbered($ sum_(k=1)^n k = n(n+1)/2 $) -Can it be generalized? Alternative methods? Common pitfalls? Related results? +// = Application +// Where the result is actually used — other areas of math, science, engineering, +// computation. The real-world hook most solution write-ups skip; it's part of +// what sets this showcase apart. Cite each claimed use, and only include it when +// there's a concrete, honest application — don't stretch for one. diff --git a/scripts/build-index.sh b/scripts/build-index.sh index a6b7b9f..416bf5e 100644 --- a/scripts/build-index.sh +++ b/scripts/build-index.sh @@ -21,24 +21,19 @@ require() { } problem_files() { - local root="$1" f + local root="$1" f base shopt -s nullglob for f in "$root"/problems/*.typ; do - [ "$(basename "$f")" = "_TEMPLATE.typ" ] && continue + base="$(basename "$f")" + [ "$base" = "_TEMPLATE.typ" ] && continue + # Skip Thai siblings: they share their English file's number and would + # otherwise show as a duplicate index row. + case "$base" in *-th.typ) continue ;; esac printf '%s\n' "$f" done shopt -u nullglob } -difficulty_stars() { - case "$1" in - easy) printf '★' ;; - medium) printf '★★' ;; - hard) printf '★★★' ;; - *) printf '%s' "${2--}" ;; - esac -} - plural() { if [ "$1" -eq 1 ]; then printf '%s' "$2"; else printf '%s' "$3"; fi; } require typst jq @@ -58,7 +53,6 @@ while IFS= read -r f; do title="$(printf '%s' "$json" | jq -r '.title')" source="$(printf '%s' "$json" | jq -r '.source // "-"')" source_url="$(printf '%s' "$json" | jq -r '.source_url // empty')" - difficulty="$(printf '%s' "$json" | jq -r '.difficulty // "-"')" date="$(printf '%s' "$json" | jq -r '.date // "-"')" topics="$(printf '%s' "$json" | jq -r '(.tags // []) | join(", ")')" @@ -67,15 +61,13 @@ while IFS= read -r f; do src_cell="$source" [ -n "$source_url" ] && src_cell="[$source]($source_url)" - diff_cell="$(difficulty_stars "$difficulty")" - - rows="$rows| $num | $title | $topics | $src_cell | $diff_cell | $date | + rows="$rows| $num | $title | $topics | $src_cell | $date | " count=$((count + 1)) done < <(problem_files "$ROOT") if [ "$count" -eq 0 ]; then - rows="| - | *No problems yet* | | | | | + rows="| - | *No problems yet* | | | | " fi @@ -85,8 +77,8 @@ if [ -z "$uniq_topics" ]; then uniq_topics="-"; fi problem_word="$(plural "$count" problem problems)" -index_block="| # | Problem | Topics | Source | Difficulty | Solved | -|---|---------|--------|--------|------------|--------| +index_block="| # | Problem | Topics | Source | Solved | +|---|---------|--------|--------|--------| $rows" progress_block="- $count $problem_word solved diff --git a/src/components.typ b/src/components.typ index d45ebda..f75218f 100644 --- a/src/components.typ +++ b/src/components.typ @@ -35,3 +35,52 @@ image(path, width: width), caption: caption, ) + +// Wrap a hand-drawn diagram (a CeTZ canvas) as a captioned, numbered figure. +// The figure is the visual half of the argument, not decoration — draw it to the +// actual math. In the problem file: +// #import "@preview/cetz:0.3.4" +// #diagram(caption: [What the picture shows.])[ +// #cetz.canvas({ import cetz.draw: *; /* ... */ }) +// ] +#let diagram(body, caption: none) = figure(body, caption: caption) + +// A boxed final answer, placed right after the problem statement for +// evaluate / find-type problems that have a crisp result. State the answer up +// front, then earn it in the solution below. +// #answer[$ integral_0^oo e^(-x) dif x = 1. $] +#let answer(body) = context { + let th = theme-state.get() + block( + width: 100%, + fill: th.thm-fill, + inset: (x: 14pt, y: 11pt), + radius: (top-right: 3pt, bottom-right: 3pt), + stroke: (left: 2pt + th.primary), + breakable: true, + )[ + #smallcaps(text(weight: "bold", fill: th.primary)[Answer.])#h(0.4em)#body + ] +} + +// Number a display equation, for the few you actually refer back to. Equations +// are unnumbered by default (see template), so wrap only those in `numbered`: +// #numbered($ a^2 + b^2 = c^2 $) +// ... as @pyth shows ... +#let numbered(eq) = { + set math.equation(numbering: "(1)") + eq +} + +// Inline citation for a named theorem, rule, or technique that the solution +// invokes but does not prove. Such a result must always be traceable, so give +// either an online `url` or an offline `ref` (a book/paper, rendered as a +// footnote). With neither, a red [ref?] marker is rendered so the gap is +// impossible to miss in review — fill it before shipping. +// #step(reason: cite("Dominated Convergence Theorem", url: "https://..."))[ ... ] +// #step(reason: cite("Sylow's theorem", ref: [Dummit & Foote, §4.5]))[ ... ] +#let cite(name, url: none, ref: none) = { + if url != none { link(url)[#emph(name)] } + else if ref != none [#emph(name)#footnote(ref)] + else [#emph(name)#text(fill: rgb("#b5403a"), size: 0.85em)[~[ref?]]] +} diff --git a/src/style.typ b/src/style.typ index 7463a1b..710d228 100644 --- a/src/style.typ +++ b/src/style.typ @@ -21,7 +21,12 @@ // typography // "New Computer Modern" ships bundled with Typst, so CI and local builds // match without installing system fonts. Libertinus Serif is a safe fallback. - font: ("New Computer Modern", "Libertinus Serif"), + // Latin + math glyphs resolve from the first two; the Thai serif faces only + // kick in per-glyph for Thai text (bilingual problem files). Norasi ships in + // the `fonts-thai-tlwg` package CI installs; Leelawadee / Tahoma cover local + // Windows builds. A missing face in this list is skipped with a harmless + // warning, never a compile failure. + font: ("New Computer Modern", "Libertinus Serif", "Norasi", "Leelawadee", "Tahoma"), size: 11pt, ) diff --git a/src/template.typ b/src/template.typ index 88ac6d3..d59b8a5 100644 --- a/src/template.typ +++ b/src/template.typ @@ -19,9 +19,7 @@ author: default-author, subtitle: none, date: none, - difficulty: none, tags: (), - summary: none, prerequisites: (), resources: (), theme: (:), // per-doc overrides @@ -32,19 +30,25 @@ theme-state.update(th) set document(title: title, author: author) - set text(font: th.font, size: th.size, fill: th.ink, ligatures: true) + set text(font: th.font, size: th.size, fill: th.ink, ligatures: true, + lang: "en", hyphenate: true) set par(justify: true, leading: 0.72em, spacing: 1.2em) set heading(numbering: "1.1") - set math.equation(numbering: "(1)") + // Equations are unnumbered by default; number only the ones you reference, + // with `numbered(...)` from components. (Numbering every display is clutter.) + set math.equation(numbering: none) // level-1 headings: small caps + a rule, and reset the env counter so // numbering restarts each section (Theorem 1.1, 2.1, ...) show heading.where(level: 1): it => { section-reset() block(above: 1.7em, below: 0.85em)[ - #text(size: 12pt, weight: "bold", fill: th.primary)[#smallcaps(it)] + #text(size: 12pt, weight: "bold", fill: th.accent)[#counter(heading).display()]#h(0.6em)#text(size: 12pt, weight: "bold", fill: th.primary)[#smallcaps(it.body)] #v(3.5pt) + // two-tone underline: a short accent segment over a full hairline #line(length: 100%, stroke: 0.4pt + th.rule) + #v(-0.4pt) + #line(length: 1.8em, stroke: 1.4pt + th.accent) ] } show heading.where(level: 2): it => block(above: 1.1em, below: 0.5em)[ @@ -60,15 +64,27 @@ header: context { if counter(page).get().first() > 1 { set text(size: 8.5pt, fill: th.muted) - emph(title) - v(-7pt) + grid( + columns: (1fr, auto), + align: (left + bottom, right + bottom), + emph(title), + if tags.len() > 0 { text(tracking: 1pt)[#upper(tags.first())] }, + ) + v(2pt) line(length: 100%, stroke: 0.5pt + th.rule) } }, footer: context { - set align(center) - set text(size: 8.5pt, fill: th.muted) - counter(page).display("1 / 1", both: true) + set text(size: 8pt, fill: th.muted) + line(length: 100%, stroke: 0.5pt + th.rule) + v(3pt) + // brand left, page right; author/license live in the colophon, not here + grid( + columns: (1fr, auto), + align: (left + horizon, right + horizon), + smallcaps[math-showcase], + counter(page).display("1 / 1", both: true), + ) }, ) @@ -79,66 +95,60 @@ title: title, source: source, source_url: source_url, - difficulty: difficulty, date: date, tags: tags, )) ] - // title block - line(length: 100%, stroke: 1.2pt + th.primary) - v(2.5pt) - line(length: 100%, stroke: 0.5pt + th.primary) - v(11pt) - text(size: 20pt, weight: "bold", fill: th.primary)[#title] + // title block — editorial header: eyebrow, title, accent mark, meta, tags. + // eyebrow: the primary area in spaced caps, accent colour + if tags.len() > 0 { + text(size: 8.5pt, weight: "bold", fill: th.accent, tracking: 2pt)[ + #upper(tags.first()) + ] + v(6pt) + } + text(size: 23pt, weight: "bold", fill: th.primary)[#title] if subtitle != none { - v(4pt) + v(5pt) text(size: 12.5pt, fill: th.muted, style: "italic")[#subtitle] } - - // byline + // short accent rule — a signature mark under the title v(9pt) - { - set text(size: 9.5pt) - [by #text(weight: "medium")[#author]] - if date != none { text(fill: th.muted)[#h(0.6em)·#h(0.6em)#date] } - } + line(length: 2.6em, stroke: 2pt + th.accent) - // source + difficulty - v(6pt) + // meta: author, date, source on one tidy line, pipe-separated + v(9pt) { set text(size: 9pt, fill: th.muted) - let parts = () + let parts = ([by #text(fill: th.primary, weight: "medium")[#author]],) + if date != none { parts.push[#date] } if source != "" { parts.push[Source: #if source_url != none [#link(source_url)[#source]] else [#source]] } - if difficulty != none { - parts.push[Difficulty: #difficulty] - } - parts.join(" · ") + parts.join([#h(0.55em)#text(fill: th.rule)[\u{007C}]#h(0.55em)]) } - // Tag pills + // tag pills if tags.len() > 0 { - v(6pt) + v(8pt) box(tags.map(t => _pill(t, th)).join(h(4pt))) } - v(9pt) + v(11pt) line(length: 100%, stroke: 0.5pt + th.rule) - // summary - if summary != none { - v(12pt) - block(inset: (left: 1.2em, right: 1.2em))[ - #text(weight: "bold", fill: th.primary)[Summary. ] - #text(style: "italic")[#summary] - ] - } - v(16pt) body + // end-of-solution mark: a small accent diamond between two hairlines + v(12pt) + align(center)[ + #box(baseline: -0.1em, line(length: 2em, stroke: 0.5pt + th.rule)) + #h(0.6em) #text(size: 8pt, fill: th.accent)[#sym.diamond.filled] #h(0.6em) + #box(baseline: -0.1em, line(length: 2em, stroke: 0.5pt + th.rule)) + ] + // prerequisites + further reading if prerequisites.len() > 0 or resources.len() > 0 { v(22pt) From 3fca8852df82cadb5cfbd05898d1f7ae3a046901 Mon Sep 17 00:00:00 2001 From: Chayanon Somatanai Date: Sat, 20 Jun 2026 15:14:45 +0700 Subject: [PATCH 2/3] docs: add authoring spec --- docs/AUTHORING.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/AUTHORING.md diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md new file mode 100644 index 0000000..2462b6c --- /dev/null +++ b/docs/AUTHORING.md @@ -0,0 +1,113 @@ +# Authoring + +What goes inside a problem file. Naming, branches, and git are in +[CONVENTIONS.md](CONVENTIONS.md). + +Start from [`../problems/_TEMPLATE.typ`](../problems/_TEMPLATE.typ). + +## Sections + +Eight sections, always in this order when present: + +``` +Problem Statement → Answer → Setup & Notation → Idea → +Solution → Verification → Reflection → Application +``` + +Only Problem Statement and Solution are required. Add the others when they +pull their weight — a two-section write-up is fine if the problem is short. + +- **Answer** — for evaluate/find problems: box the result with `answer[...]` + up front, then prove it. +- **Idea** — the `#insight` before the formal work. +- **Reflection** — generalizations, alternative proofs, pitfalls. Keep it + within mathematics. +- **Application** — a concrete real-world use (statistics, physics, + computation). Include a cite for each. Skip if there's nothing honest to say. + +## Writing the solution + +- **Cite every result you use but don't prove.** Any named theorem, rule, or + technique needs a `cite("…", url: "…")` (online) or `cite("…", ref: [Book, §x])` + (offline). This covers claims in any section — Setup, Idea, Verification, + Application — not just `step` reasons. ("no elementary antiderivative" → + cite Liouville; "the tail converges" → cite the comparison test.) A `cite` + without a source prints a red `[ref?]`, so missing references are visible. + Elementary facts (commutativity, basic arithmetic) need no cite. A `#theorem` + you prove in the file is yours — don't cite your own result. + +- **Show every step.** Include the algebra between displayed lines. If a reader + has to ask "how did that follow?", the step is missing. Put the rule in + `reason:`. + +- **Concrete before abstract.** Before stating the general case, show a small + instance — a specific `n`, a picture, a number. + +- **Lead with the idea.** Put an `#insight` naming the key move before the + formal steps. + +- **Diagram when the math is visual.** If the problem has geometry or a + visualizable structure, draw it with `diagram(...)` (CeTZ `@preview/cetz:0.3.4`, + pinned) — a shaded region, a lattice, a transformed domain. Make it specific + to the actual problem; theme with `default-theme.accent`. Skip it only when + nothing visual would help. + +- **Don't flatten the result.** If the answer is surprising or the method is + non-obvious, say so briefly. Skip this if there's nothing real to add. + +- **End with the right environment.** A prove task ends with `#theorem`. An + evaluate/find task ends at the answer — don't re-box a value as a `#theorem`. + +- **Verify.** Prove → check an adversarial step. Evaluate → recompute a second + way or numerically. Find-all → show sufficiency *and* "no others". Optimize → + give the witness and the equality case. Count → check small cases by hand. + +## Honesty + +Don't fabricate a step you can't fill. Report the gap. For an original or +extended problem, if a statement turns out to be unprovable, you mis-stated it +— give the counterexample, then the corrected statement. A wrong proof is worse +than no proof. + +## Bilingual (Thai) + +Thai siblings share the English file's number with `-th` on the slug +(`calc-001-x.typ` → `calc-001-x-th.typ`; see CONVENTIONS → Bilingual siblings). +Write English first. + +- Translate the **prose** — headings, explanations, `reason:` text, insights. + Keep all math, symbols, and proper nouns (Cauchy–Schwarz, Fubini). `cite` + links stay; only the display name is translated. +- Short sentences. One idea each. Don't stack `ซึ่ง…ที่…โดยที่…` clauses. +- **Register: semi-formal (กึ่งทางการ).** Use "เรา" (เราจะแสดงว่า…, + เราจึงได้…) and natural connectives (พิจารณา, สังเกตว่า, จะได้ว่า, ดังนั้น, + นั่นคือ). Not stiff textbook prose. + +## Tools (`src/components.typ`, `src/theorems.typ`) + +| Tool | For | +|------|-----| +| `theorem` / `lemma` / `corollary` / `proposition` | a numbered result you prove | +| `definition` | a definition | +| `step(reason: …)` | a justified solution step | +| `proof` | a formal proof block (auto QED) | +| `insight` | the key idea callout | +| `answer` | boxed final answer (evaluate/find problems) | +| `cite(name, url:/ref:)` | link a named result you use | +| `diagram(body, caption:)` | a CeTZ figure | +| `numbered(eq)` | number an equation you refer back to | +| `remark` / `example` / `comparison` | asides, tables | + +Math is always Typst `$…$` — never plain-text a formula. Equations are +unnumbered by default; wrap only the ones you reference back in `numbered(...)`. +Titles and the summary field are plain strings — no `$math$`. + +## Before committing + +- `make compile P=-NNN` passes, and so does the `-th` sibling if one exists. +- No `[ref?]` markers — every used theorem/rule/technique has a cite. +- No skipped steps. +- Diagram included if the math is visual. +- Problem statement is in your own words if the source had prose; `source` set + (or `""` for an original problem). +- `make index` run. From be8fb222037da5e0e87dd5937b2f7ce72661d1c7 Mon Sep 17 00:00:00 2001 From: Chayanon Somatanai Date: Sat, 20 Jun 2026 16:02:31 +0700 Subject: [PATCH 3/3] fix: address coderabbit review feedback --- docs/AUTHORING.md | 4 ++-- docs/CONVENTIONS.md | 4 ++-- problems/_TEMPLATE.typ | 2 +- src/style.typ | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md index 2462b6c..b1371b9 100644 --- a/docs/AUTHORING.md +++ b/docs/AUTHORING.md @@ -9,7 +9,7 @@ Start from [`../problems/_TEMPLATE.typ`](../problems/_TEMPLATE.typ). Eight sections, always in this order when present: -``` +```text Problem Statement → Answer → Setup & Notation → Idea → Solution → Verification → Reflection → Application ``` @@ -100,7 +100,7 @@ Write English first. Math is always Typst `$…$` — never plain-text a formula. Equations are unnumbered by default; wrap only the ones you reference back in `numbered(...)`. -Titles and the summary field are plain strings — no `$math$`. +Titles are plain strings — no `$math$`. ## Before committing diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 95fb854..80a87fe 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -66,13 +66,13 @@ A problem can have a Thai-language sibling. It is a separate file (one language per file, like one problem per file) that **shares the English file's number** and appends `-th` to the slug: -``` +```text calc-001-gamma-integral.typ English (primary, write first) calc-001-gamma-integral-th.typ Thai sibling, same problem, same number ``` The `-th` form still matches the file-name pattern (`-th` is part of the slug), -so no script or CI change is needed. The Thai file does **not** get a new +so no file-name validation change is needed. The Thai file does **not** get a new number from `make new`; copy the English file and rename it by hand. Thai rendering relies on the Thai serif faces in the font fallback (see [`../src/style.typ`](../src/style.typ)); CI installs them via diff --git a/problems/_TEMPLATE.typ b/problems/_TEMPLATE.typ index efaeb30..bdc6a1a 100644 --- a/problems/_TEMPLATE.typ +++ b/problems/_TEMPLATE.typ @@ -3,7 +3,7 @@ // // Before committing: // - compiles cleanly (make compile P=-NNN) -// - every imported theorem/rule/technique is cited (cite(name, url:)); no [ref?] +// - every imported theorem/rule/technique is cited (cite(name, url:) or cite(name, ref:)); no [ref?] // - no skipped steps — a reader never has to ask "how did that follow?" // - a diagram is included wherever the math is visual // - sections in order, only the ones the problem earns (see CONVENTIONS) diff --git a/src/style.typ b/src/style.typ index 710d228..a86e171 100644 --- a/src/style.typ +++ b/src/style.typ @@ -24,8 +24,8 @@ // Latin + math glyphs resolve from the first two; the Thai serif faces only // kick in per-glyph for Thai text (bilingual problem files). Norasi ships in // the `fonts-thai-tlwg` package CI installs; Leelawadee / Tahoma cover local - // Windows builds. A missing face in this list is skipped with a harmless - // warning, never a compile failure. + // Windows builds. A missing face is silently substituted (fallback enabled by + // default); glyphs with no match render as tofu boxes. font: ("New Computer Modern", "Libertinus Serif", "Norasi", "Leelawadee", "Tahoma"), size: 11pt, )