diff --git a/skills/dump-chat-history/SKILL.md b/skills/dump-chat-history/SKILL.md index dd5c2e5..0e51b44 100644 --- a/skills/dump-chat-history/SKILL.md +++ b/skills/dump-chat-history/SKILL.md @@ -132,7 +132,11 @@ typst compile report.typ report.pdf The template uses A4, quiet phase bands, serif prose, sans-serif headings, and monospaced original text, with portable font fallbacks. It permits long messages -to continue across pages. Add phases and concise annotations when useful; do not +to continue across pages. Front matter is short: title, optional subtitle, and +one date line, so the first prompt appears on the first page; scope, coverage, +and sources sit at the end. Change fonts with `--input heading-font="..."` and +`--input body-font="..."`; pass `Libertinus Serif` for both to build with only +Typst's bundled fonts. Add phases and concise annotations when useful; do not force a fixed page count, prompt ranking, productivity ratio, or research taxonomy. Ground outcome claims in the adjacent conversation or verifiable artifacts; distinguish reported historical checks from checks performed now. diff --git a/skills/dump-chat-history/assets/report.typ b/skills/dump-chat-history/assets/report.typ index f92318a..011e981 100644 --- a/skills/dump-chat-history/assets/report.typ +++ b/skills/dump-chat-history/assets/report.typ @@ -1,7 +1,9 @@ // Copy beside history.json; compile with: typst compile report.typ report.pdf +// Optional: --input heading-font="..." --input body-font="..." (installed fonts; +// pass "Libertinus Serif" for both to build with only Typst's bundled fonts) #let report = json("history.json") -#let sans = ("Avenir Next", "Libertinus Serif") -#let serif = ("Charter", "Libertinus Serif") +#let sans = (sys.inputs.at("heading-font", default: "Avenir Next"), "Libertinus Serif") +#let serif = (sys.inputs.at("body-font", default: "Charter"), "Libertinus Serif") #let mono = "DejaVu Sans Mono" #let ink = rgb("24282b") #let muted = rgb("63707b") @@ -10,7 +12,7 @@ #set document(title: report.title, description: "Conversation history field note") #set page(paper: "a4", margin: (x: 23mm, y: 20mm), footer: context { set text(font: sans, size: 8pt, fill: muted) - [#report.title #h(1fr) #counter(page).display()] + grid(columns: (1fr, auto), gutter: 12pt, report.title, counter(page).display()) }) #set text(font: serif, size: 10pt, fill: ink) #set par(leading: 0.45em, spacing: 0.7em) @@ -19,27 +21,26 @@ #show raw: set text(font: mono, size: 8.7pt, hyphenate: false, ligatures: false) #show raw.where(block: true): set block(width: 100%, fill: rgb("f2f3f4"), inset: 7pt, radius: 2pt, breakable: true) -#text(font: sans, size: 8pt, tracking: 1pt, fill: rust)[CONVERSATION FIELD NOTE] -#v(7pt) -// The topic is the title. Counts belong in metadata, never the headline. -#text(font: sans, size: 27pt, weight: "bold", report.title) -#v(7pt) -#text(size: 12pt, report.at("subtitle", default: "")) -#v(8pt) -#text(font: sans, size: 9pt, fill: muted)[ - #report.window.start – #report.window.end · #report.window.timezone +// Short front matter: the transcript starts on the first page. Scope, coverage, +// and sources sit at the end. The topic is the title; counts never are. +#block(breakable: false, below: 10pt)[ + #text(font: sans, size: 20pt, weight: "bold", report.title) + #if report.at("subtitle", default: "") != "" [ + #v(4pt) + #text(size: 11pt, report.subtitle) + ] + #v(4pt) + #text(font: sans, size: 8.5pt, fill: muted)[ + #report.window.start – #report.window.end · #report.window.timezone + ] ] -#v(5pt) -#report.scope -#v(5pt) -#report.coverage -= Transcript #let visible = report.entries.filter(e => e.at("include_in_report", default: e.role == "user")) #for entry in visible { let phase = entry.at("phase", default: none) if phase != none { - block(width: 100%, fill: rgb("f6f3ef"), radius: 3pt, inset: 10pt, breakable: false)[ + // sticky keeps the phase label with the message that follows it. + block(width: 100%, fill: rgb("f6f3ef"), radius: 3pt, inset: 10pt, sticky: true, above: 12pt)[ #text(font: sans, weight: "semibold", phase) ] } @@ -73,8 +74,11 @@ ] = Sources and limits +#report.scope + +#report.coverage #for source in report.sources [ - #block(breakable: true, above: 5pt)[ + #block(breakable: true, above: 9pt)[ #text(font: sans, size: 9pt, weight: "semibold", source.id) #linebreak() #raw(source.location, block: true) diff --git a/skills/how-to-write-ideas-report/references/typst-reference.md b/skills/how-to-write-ideas-report/references/typst-reference.md index 6ad5567..881b854 100644 --- a/skills/how-to-write-ideas-report/references/typst-reference.md +++ b/skills/how-to-write-ideas-report/references/typst-reference.md @@ -10,9 +10,18 @@ Reference files: ~/Documents/private-note/notes/typst-learn/ (typst-tricks.typ, ### Page Setup - Standalone figures: `#set page(width: auto, height: auto, margin: 5pt)` -- Standard notes: `#set page(margin: 2cm)` + `#set text(size: 10pt)` + `#set heading(numbering: "1.1.")` +- Reports: A4, 18 mm margins, 10 pt justified body text, 0.55 em leading, and a short running footer with the page number. Use the survey template as a working example. - Numbered equations: `#set math.equation(numbering: "(1)")` +### Report hierarchy and legibility +- Give the document title its own text block, not a numbered heading. Use about 18–20 pt for the title, 13–14 pt for sections, and 11 pt for subsections. No kicker line or decorative front matter: the first content paragraph belongs on the first page. +- Use serif body text and a consistent sans-serif heading font. The report templates accept `--input heading-font="Your installed font"` (the chat-history template also `body-font`); `Libertinus Serif` works without system fonts. Do not shrink body text below 10 pt to meet a page count. +- Justify body prose; keep table cells ragged right so narrow columns keep natural word spacing. Use at least 9 pt for tables and captions, and 8 pt for supporting metadata. +- Reserve one dark accent and a light tint for navigation and brief assessments. Name categories and priorities explicitly so they remain readable in grayscale. +- Use horizontal table rules, brief cells, and `table.header(repeat: true, ...)`. Keep long tables outside unbreakable figures or boxes. Match column widths to actual content, including the longest header and priority label. +- Use `block(breakable: true, ...)` for long callouts and transcript entries. Keep a short label with its following content using `block(sticky: true, ...)`. +- Inspect rendered pages with realistic paragraphs, long titles, source paths, and a table or message that crosses a page. Check that annotations cannot be mistaken for quoted text. + ### Common Packages - CeTZ: `@preview/cetz:0.4.0`, `@preview/cetz-plot:0.1.2` - Algorithms: `@preview/algorithmic:1.0.3` — `Function`, `For`, `While`, `If`, `ElseIf`, `Else`, `Assign`, `Return`, `Comment` @@ -39,7 +48,7 @@ Reference files: ~/Documents/private-note/notes/typst-learn/ (typst-tricks.typ, - `dict.at(key, default: 0)` — dict with default ### Content Helpers -- Infobox: `rect(stroke: color, inset: 8pt, radius: 4pt, width: 100%, [*Title:*\ body])` +- Short assessment: `block(stroke: (left: 2pt + color), inset: 10pt, width: 100%, breakable: true, body)` - Inline image alignment: `box(image(...), baseline: (size - 20pt) / 2 + offset)` - Image clipping: `box(clip: true, img, inset: (top: -top, bottom: -bottom, ...))` - Two columns: `grid(columns: (1fr, 1fr), gutter: 20pt, left, right)` diff --git a/skills/survey/SKILL.md b/skills/survey/SKILL.md index 605d7d5..080febc 100644 --- a/skills/survey/SKILL.md +++ b/skills/survey/SKILL.md @@ -78,7 +78,10 @@ Follow `skills/how-to-technical-writing/SKILL.md` for sentence- and paragraph-le - Check `CLAUDE.md`/`AGENTS.md` for a deliverables-location convention before choosing an output path. - Tailor technical depth to the user's role from `docs/discussion/user-profile.md` or available context. - Save to `articles/YYYY-MM-DD--review.{md,typ,tex}` or a project-specific path if the user prefers. -- For Typst, start from `skills/survey/template.typ` with `skills/survey/template.bib`. The scaffold provides `section_box`, `stage`, `proscons`, `compare_table`, and `problem_table`; delete unused helpers from the copied document. +- For Typst, start from `skills/survey/template.typ` with `skills/survey/template.bib`. The scaffold provides `stage`, `proscons`, `compare_table`, and `problem_table`; delete unused helpers from the copied document. +- Put only the review date beneath the title. Open with the single **Overview** described below, and end the body with **Open Problems**, followed by references. Do not add a separate scope block, assessment box, or closing "Next step" section. +- Keep the template's dense, restrained layout: a plain title line, 10 pt justified body text, one accent color, and horizontal table rules. `compare_table` and `problem_table` accept custom `headers` and `columns`; choose them together, since a count mismatch fails the compile with a message. Write urgency labels as text. Keep long tables in the page flow so headers repeat across pages, and inspect the rendered result with realistic cell text. +- The heading font can be changed with `--input heading-font="Your installed font"`. For a build using only Typst's bundled fonts, use `--input heading-font="Libertinus Serif"`. ### Gap-filling focus @@ -93,17 +96,26 @@ When the source set comes from an existing, recent `NOTES.md` (the normal case), Organize the review **by technical approach**. State of the art and trade-offs live inside each approach, not in separate global sections. Do not add standalone global "Pros and Cons" or "State of the Art" sections. -#### 1. What and Why +#### 1. Overview -Define the topic in 2–3 paragraphs for a new reader: +Write two connected paragraphs for a new reader, without inline labels: -- What it is and what problem it solves -- Why it matters now -- How it differs from the dominant or prior approach +- Define the topic and problem, explain why it matters, and state the report's scope and intended audience. Distinguish it from the prior approach where that helps define the topic. +- State the principal finding and its supporting evidence, then identify the unresolved constraint that motivates the key questions below. Cite the claims. Include a diagram only when it clarifies the architecture, data flow, or problem framing. Lay approaches side by side only when they solve the same task and are genuinely comparable; otherwise show their relationship or omit the figure. -#### 2. Technical Approaches +#### 2. Key Questions + +Give one subsection to each subtopic or open question the field is trying to settle (typically 2–5). For each, cover: + +- **The question** — one sentence, stated as a question. +- **Why it matters** — what answering it gives: deeper understanding (what it would settle or unify) and/or practical value (what it would enable). Say which applies; a question with neither does not belong in the review. +- **Where it stands** — the best partial answer and its limits, cited. + +Questions are ends, not means: do not give them strengths and limitations. + +#### 3. Technical Approaches Identify the main method families (typically 3–6) and give one subsection per approach. Optionally begin with a short field-wide timeline or landscape. @@ -115,13 +127,13 @@ For each approach, cover: Optionally finish with a cross-approach comparison table when several approaches share meaningful criteria. Choose columns that actually discriminate this field (for example scalability, verifiability/cost, maturity, and best-fit use case). Skip it for a single-approach topic. -#### 3. Open Problems +#### 4. Open Problems End with a ranked table of 4–8 problems: number, problem, why it matters, who could solve it, and urgency (Critical / High / Medium). Cite the work that defines each gap or the closest existing result. Do not add business strategy, product fit, or investor sections to the neutral report. ### Visualization guidelines -- Typst: use CeTZ for timelines and dependency diagrams; use native `grid`, `rect`, and fixed-width `box()` for text-heavy comparisons and role diagrams. See `skills/how-to-write-ideas-report/references/typst-reference.md`. +- Typst: use CeTZ for timelines and dependency diagrams; use native `grid`, `block`, and fixed-width `box()` for text-heavy comparisons and role diagrams. See `skills/how-to-write-ideas-report/references/typst-reference.md`. - Use a native table for cross-approach comparisons. - Wrap multiline CeTZ content in a fixed-width box and use string identifiers for `name:`. - Compile after each figure; every claim in technical and open-problem tables needs at least one citation. diff --git a/skills/survey/template.typ b/skills/survey/template.typ index 8330176..f86034c 100644 --- a/skills/survey/template.typ +++ b/skills/survey/template.typ @@ -1,183 +1,204 @@ -// survey report Typst template — copy to articles/YYYY-MM-DD--review.typ -// and replace the placeholder content. Compile with: typst compile .typ -// Citations use a sibling bib (see the bibliography() call at the bottom); -// copy .knowledge/references.bib beside the document, point bibliography() at -// that canonical file, or adapt template.bib. - -#set page(margin: 1.6cm) -#set text(size: 10pt) -#set par(justify: true, leading: 0.62em) -#set heading(numbering: "1.") - -#let title = [TODO: Review Title] -#let authors = [TODO: author / "review draft"] - -#show link: set text(fill: blue.darken(30%)) - -// ---- reusable helpers --------------------------------------------------- - -#let section_box(title, body, fill: rgb("f7f8fc"), stroke: rgb("d9deeb")) = rect( - width: 100%, inset: 10pt, radius: 6pt, fill: fill, stroke: stroke, - [#strong[#title] #v(0.3em) #body], -) - -// horizontal strip cell (use in a grid for a flow/era/landscape diagram) -#let stage(name, body, fill) = rect( - width: 100%, inset: 7pt, radius: 5pt, fill: fill, stroke: rgb("c7cfe0"), - align(center)[#text(weight: "semibold", size: 9pt)[#name] #v(0.2em) #text(size: 8pt)[#body]], +// Copy beside template.bib, then replace the example content and bibliography. +// The canonical source bibliography remains .knowledge/references.bib. +// Compile: typst compile review.typ review.pdf +// Optional: --input heading-font="Your installed sans-serif font" + +#let title = [TODO: Review title] +#let review_date = [TODO: YYYY-MM-DD] +#let short_title = [Research review] // Short running title; keep to one line. +#let serif = "Libertinus Serif" +#let sans = (sys.inputs.at("heading-font", default: "Avenir Next"), serif) +#let ink = rgb("24282b") +#let muted = rgb("53616b") +#let accent = rgb("245b65") +#let tint = rgb("f2f6f6") +#let rule = rgb("cbd4d7") + +#set document(title: title) +#set page( + paper: "a4", margin: (x: 18mm, y: 18mm), + footer: context { + set text(font: sans, size: 8pt, fill: muted) + grid(columns: (1fr, auto), gutter: 12pt, short_title, counter(page).display()) + }, ) +#set text(font: serif, size: 10pt, fill: ink) +#set par(justify: true, leading: 0.55em, spacing: 0.65em) +#set heading(numbering: "1.1") +#set list(indent: 1em, body-indent: 0.5em, spacing: 0.35em) +#show heading: set text(font: sans, weight: "bold") +#show heading.where(level: 1): set text(size: 13pt) +#show heading.where(level: 2): set text(size: 11pt) +#show heading.where(level: 1): set block(above: 1.3em, below: 0.55em) +#show heading.where(level: 2): set block(above: 1em, below: 0.45em) +#show link: set text(fill: accent) +#show figure.caption: set text(size: 9pt, fill: muted) +#set figure(gap: 6pt) + +// Cell for a flow, architecture, or timeline grid. Use arrows only for an +// actual sequence or dependency. Names and descriptions must carry the +// relationship on their own; `fill` is optional emphasis. +#let stage(name, body, fill: tint) = block( + width: 100%, inset: 7pt, fill: fill, stroke: 0.5pt + rule, +)[ + #align(center)[ + #text(font: sans, size: 9pt, weight: "bold", name) + #v(3pt) + #text(size: 8.5pt, body) + ] +] -// per-approach strengths / limitations (two UNPAIRED bullet lists). -// USE ONLY when the approaches are competing solutions to the SAME problem -// ("which should I pick?"). For complementary capabilities / platform branches, -// write a prose assessment paragraph instead and delete this helper. +// Two independent lists, only for competing solutions to the same problem. +// Complementary capabilities need a prose assessment instead. #let proscons(pros, cons) = grid( - columns: (1fr, 1fr), gutter: 0.6em, - rect(width: 100%, inset: 7pt, radius: 5pt, fill: rgb("eef6ef"), stroke: rgb("bcd9c4"))[ - #text(weight: "semibold", fill: green.darken(30%))[Strengths] - #pros - ], - rect(width: 100%, inset: 7pt, radius: 5pt, fill: rgb("fbecec"), stroke: rgb("e3c6c6"))[ - #text(weight: "semibold", fill: red.darken(20%))[Limitations] - #cons - ], + columns: (1fr, 1fr), gutter: 12pt, + ..(([Strengths], pros), ([Limitations], cons)).map(((label, body)) => block( + width: 100%, inset: (top: 5pt), stroke: (top: 0.5pt + rule), + breakable: true, + )[ + #block(sticky: true, below: 3pt)[ + #text(font: sans, size: 9pt, weight: "bold", label) + ] + #body + ]), ) -// optional cross-approach comparison matrix. -// `rows` is an ARRAY OF ROW-ARRAYS — one inner (…) per row — so a stray comma -// or paren breaks only that row, with an error pointing at it, instead of -// silently mis-nesting the whole table. -#let compare_table(rows) = table( - columns: (20%, 18%, 22%, 16%, 24%), - stroke: (x, y) => if y == 0 { 0.8pt + black } else { 0.4pt + rgb("d7dbe6") }, - inset: 6pt, - table.header( - [#strong[Approach]], [#strong[Scalability]], [#strong[Verifiability / cost]], [#strong[Maturity]], [#strong[Best-fit use case]], - ), - ..rows.flatten(), -) +// Rows are arrays of cells; a row whose length differs from `columns` fails +// with a message naming the row. Keep text brief; explain qualifications in +// prose. Horizontal rules separate records without boxing in every cell. +// Headers repeat when a table continues onto another page. +#let report_table(columns, headers, rows) = { + assert(headers.len() == columns.len(), message: "report_table: " + + str(headers.len()) + " headers but " + str(columns.len()) + " columns") + for (i, row) in rows.enumerate() { + assert(row.len() == columns.len(), message: "report_table: row " + str(i + 1) + + " has " + str(row.len()) + " cells but the table has " + str(columns.len()) + " columns") + } + set text(size: 9pt) + set par(justify: false, leading: 0.45em) + table( + columns: columns, align: left + top, + inset: (x: 5pt, y: 5pt), + stroke: (x, y) => (bottom: if y == 0 { 0.8pt + accent } else { 0.4pt + rule }), + fill: (x, y) => if y == 0 { tint }, + table.header(repeat: true, ..headers.map(h => text(weight: "bold", h))), + ..rows.flatten(), + ) +} + +// Adapt both headers and widths to criteria that discriminate this field. +#let compare_table( + rows, + columns: (1.1fr, 1fr, 1.25fr, 0.85fr, 1.4fr), + headers: ([Approach], [Scalability], [Verification / cost], [Maturity], [Best use]), +) = report_table(columns, headers, rows) + +// Rank and urgency get enough room for their labels, not a fixed tiny fraction. +#let problem_table( + rows, + columns: (auto, 1.2fr, 1.6fr, 1fr, auto), + headers: ([No.], [Problem], [Why it matters], [Who can act], [Urgency]), +) = report_table(columns, headers, rows) + +// Title is not a numbered section or an outline entry. +#block(breakable: false, below: 10pt)[ + #text(font: sans, size: 18pt, weight: "bold", title) + #v(5pt) + #text(font: sans, size: 9pt, fill: muted)[#review_date] +] + += Overview + +// Two connected paragraphs, without inline labels or an assessment box. +TODO: define the topic and problem, explain why it matters, and state the +report's scope and intended audience @Example2024. -// `rows` is an array of row-arrays — see compare_table. -#let problem_table(rows) = table( - columns: (5%, 27%, 38%, 20%, 10%), - stroke: (x, y) => if y == 0 { 0.8pt + black } else { 0.4pt + rgb("d7dbe6") }, - inset: 6pt, - table.header( - [#strong[No.]], [#strong[Problem]], [#strong[Why it matters]], [#strong[Who could solve it]], [#strong[Urgency]], +TODO: state the principal finding and its supporting evidence, then identify +the unresolved constraint that motivates the key questions below @Example2025. + +// Optional: keep only when it clarifies the topic or relationships. +#figure( + grid( + columns: (1fr, auto, 1fr, auto, 1fr), gutter: 6pt, + align: center + horizon, + stage([Concept A], [one-line role]), + [→], + stage([Concept B], [one-line role]), + [→], + stage([Concept C], [one-line role]), ), - ..rows.flatten(), + caption: [TODO: explain the relationship shown and the point it establishes.], ) -// ---- title -------------------------------------------------------------- += Key questions -#align(center)[#heading(numbering: none)[#title]] +// One subsection per subtopic or open question the field is trying to settle, +// typically 2–5. A question earns its place by what answering it gives: +// deeper understanding, practical value, or both. Do not list strengths and +// limitations here; those belong to techniques. -#align(center)[ - #text(size: 12pt, weight: "semibold")[State-of-the-Art Review] - #v(0.25em) - #text(fill: gray.darken(20%))[#authors] - #v(0.25em) - #text(fill: gray.darken(10%))[TODO: Generated YYYY-MM-DD from the knowledge base.] -] +== TODO: Key question one -#v(0.9em) +*The question.* TODO: state it in one sentence. -#section_box( - [Scope], - [TODO: one paragraph — what this report assesses, who it is for, and what it deliberately excludes. State that the review is organized #emph[by technical approach].], -) +*Why it matters.* Understanding: TODO what an answer would settle or unify. +Value: TODO what it would enable in practice @Example2024. -= What and Why +*Where it stands.* TODO: the best partial answer and its limits @Example2025. -// TODO: 2–3 paragraphs. (1) What it is + the problem it solves. -// (2) Why it matters now — the motivation/stakes. (3) How it differs from the -// dominant or prior approach. Cite real claims with @citekey. +== TODO: Key question two -TODO: define the topic for a reader who has never heard of it @Example2024. +*The question.* TODO. -#v(0.5em) -#figure( - grid( - columns: (30%, 5%, 30%, 5%, 30%), - align: center + horizon, - stage([Concept A], [one-line role], rgb("eef3ff")), - align(center + horizon)[#text(size: 14pt)[→]], - stage([Concept B], [one-line role], rgb("e9f7ee")), - align(center + horizon)[#text(size: 14pt)[→]], - stage([Concept C], [one-line role], rgb("fdf2e9")), - ), - caption: [TODO: a concept / architecture / problem-framing diagram.], -) +*Why it matters.* Understanding: TODO. Value: TODO @Example2025. -= Technical Approaches +*Where it stands.* TODO @Example2024. -// Identify the main approaches / method families (typically 3–6) and give one -// subsection each. Optionally open with a short field-wide timeline/landscape. += Technical approaches -#figure( - grid( - columns: (30%, 5%, 30%, 5%, 30%), - align: center + horizon, - stage([Era 1 (years)], [what defined it], rgb("eef3ff")), - align(center + horizon)[#text(size: 13pt)[→]], - stage([Era 2 (years)], [what defined it], rgb("eef6ef")), - align(center + horizon)[#text(size: 13pt)[→]], - stage([Era 3 (years)], [what defined it], rgb("fdf2e9")), - ), - caption: [Optional field landscape / timeline.], -) +// Give each method family a subsection, typically 3–6 in total. Strengths and +// limitations apply here, to techniques, not to questions. Add a timeline +// (a `stage` grid of eras) only if chronology explains a technical change. -== TODO: Approach One +== TODO: Approach one -*What it is.* TODO: the mechanism at one level of detail — the representation, objective, or trick that defines it. +*Mechanism.* TODO: the representation, objective, or operation that defines it. -*State of the art.* TODO: the strongest current results, leading groups, maturity — lead with the best result, each cited @Example2024 @Example2025. +*Best evidence.* TODO: the strongest supported result and its conditions @Example2024 @Example2025. #proscons( list([TODO strength @Example2024.], [TODO strength @Example2025.]), list([TODO limitation @Example2024.], [TODO limitation @Example2025.]), ) -== TODO: Approach Two +== TODO: Approach two -*What it is.* TODO. +*Mechanism.* TODO. -*State of the art.* TODO @Example2025. +*Best evidence.* TODO @Example2025. #proscons( list([TODO strength @Example2025.]), list([TODO limitation @Example2025.], [TODO limitation @Example2024.]), ) -// ... repeat one == subsection per approach (3–6 total). Optionally add a final -// cross-cutting subsection (e.g. shared infrastructure) that is not itself an -// approach but every approach depends on. - -== At-a-glance comparison +== Comparison -// Optional: include when the field has several comparable approaches; drop for a -// single-approach topic. +// Optional. Compare only approaches with shared assumptions and criteria. +// Keep these native tables in the page flow so long tables can paginate. #compare_table(( - ([Approach One], [TODO @Example2024], [TODO], [TODO], [TODO]), - ([Approach Two], [TODO @Example2025], [TODO], [TODO], [TODO]), + ([Approach one], [TODO @Example2024], [TODO], [TODO], [TODO]), + ([Approach two], [TODO @Example2025], [TODO], [TODO], [TODO]), )) -= Open Problems += Open problems +// Rank 4–8 evidence-backed problems in a completed review. Priority is written +// explicitly, so neither color nor a legend is needed to read it. #problem_table(( - ([1], [TODO problem], [TODO why it matters @Example2024], [TODO who], [#text(fill: red.darken(10%))[Critical]]), - ([2], [TODO problem], [TODO why it matters @Example2025], [TODO who], [#text(fill: orange.darken(20%))[High]]), - ([3], [TODO problem], [TODO why it matters], [TODO who], [#text(fill: olive.darken(10%))[Medium]]), + ([1], [TODO problem], [TODO why it matters @Example2024], [TODO who], [Critical]), + ([2], [TODO problem], [TODO why it matters @Example2025], [TODO who], [High]), + ([3], [TODO problem], [TODO why it matters @Example2024], [TODO who], [Medium]), )) -#v(0.8em) - -#section_box( - [Bottom line], - [TODO: one tight paragraph — the synthesis / recommendation the reader should leave with @Example2024.], - fill: rgb("eef6ef"), stroke: rgb("bcd9c4"), -) - -#v(0.6em) - #bibliography("template.bib", title: "References", style: "ieee") diff --git a/tests/test_typst_reports.py b/tests/test_typst_reports.py new file mode 100644 index 0000000..ed06c96 --- /dev/null +++ b/tests/test_typst_reports.py @@ -0,0 +1,113 @@ +"""Render checks for report content that crosses page boundaries.""" + +import json +import re +import shutil +import sys +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +HISTORY = ROOT / "skills/dump-chat-history/helpers/history.py" +pytestmark = pytest.mark.skipif( + not shutil.which("typst") or not shutil.which("pdftotext"), + reason="PDF toolchain not installed", +) + + +BUNDLED = ["--ignore-system-fonts", "--input", "heading-font=Libertinus Serif"] + + +def compile_pdf(source, pdf, *extra): + compiled = subprocess.run( + ["typst", "compile", *BUNDLED, *extra, str(source), str(pdf)], + capture_output=True, text=True, + ) + assert compiled.returncode == 0, compiled.stderr + assert "warning:" not in compiled.stderr, compiled.stderr + return subprocess.check_output(["pdftotext", "-layout", str(pdf), "-"], text=True) + + +def test_survey_tables_paginate_with_repeated_headers_and_complete_rows(tmp_path): + for name in ("template.typ", "template.bib"): + shutil.copy(ROOT / "skills/survey" / name, tmp_path / name) + source = tmp_path / "template.typ" + with source.open("a") as stream: + stream.write(''' +#pagebreak() +#problem_table(range(45).map(i => ( + [#i], [Row-#i], + [Evidence-#i: the result depends on the evaluation protocol and input distribution.], + [Evaluation team], [Critical], +))) +#pagebreak() +#compare_table( + (([Method A], [Evidence A]), ([Method B], [Evidence B])), + columns: (1fr, 2fr), headers: ([Method], [Evidence]), +) +#stage([Custom stage], [Named fill override], fill: rgb("eef3ff")) +#stage([Default stage], [Fill omitted]) +''') + extracted = compile_pdf(source, tmp_path / "report.pdf") + table_pages = [page for page in extracted.split("\f") if "Row-" in page] + assert len(table_pages) >= 2 + for page in table_pages: + for header in ("Problem", "Why it matters", "Who can act", "Urgency"): + assert header in page + assert re.findall(r"Row-(\d+)", extracted) == [str(i) for i in range(45)] + assert re.findall(r"Evidence-(\d+)", extracted) == [str(i) for i in range(45)] + for text in ("Method A", "Evidence A", "Method B", "Evidence B", + "Custom stage", "Named fill override", "Default stage", "Fill omitted"): + assert text in extracted + + +@pytest.mark.parametrize("call, message", [ + ("compare_table((([A], [B]),), columns: (1fr, 2fr), headers: ([H1], [H2], [H3]))", "3 headers but 2 columns"), + ("problem_table((([1], [P], [Why], [Who], [High]), ([2], [P], [Why], [Who])))", "row 2 has 4 cells but the table has 5 columns"), +]) +def test_survey_table_shape_mismatch_fails_with_named_location(tmp_path, call, message): + for name in ("template.typ", "template.bib"): + shutil.copy(ROOT / "skills/survey" / name, tmp_path / name) + source = tmp_path / "template.typ" + with source.open("a") as stream: + stream.write("\n#" + call + "\n") + compiled = subprocess.run( + ["typst", "compile", *BUNDLED, str(source), str(tmp_path / "bad.pdf")], + capture_output=True, text=True, + ) + assert compiled.returncode != 0 + assert message in compiled.stderr + + +def test_chat_history_pdf_opens_with_first_prompt_and_closes_with_sources(tmp_path): + def entry(identifier, text, **extra): + return {"id": identifier, "source": "codex", "session_id": "s1", "timestamp": "2026-09-07T00:00:00+08:00", + "role": "user", "kind": "prompt", "text": text, "source_id": "S1", + "provenance": "S1, record " + identifier, **extra} + data = { + "title": "A Long Topic Title That Should Not Collide With The Page Number In The Footer", + "subtitle": "Where the plan changed", + "window": {"start": "2026-09-03T00:00:00+08:00", "end": "2026-09-10T00:00:00+08:00", "timezone": "Asia/Shanghai"}, + "scope": "SCOPE-SENTINEL synthetic sessions.", + "coverage": "COVERAGE-SENTINEL fixtures only.", + "sources": [{"id": "S1", "location": "fixture.jsonl", "note": "Synthetic source."}], + "entries": [entry("P1", "FIRST-PROMPT-SENTINEL design the site.", phase="Phase 1: Scoping"), + entry("P2", "\n".join(f"Line {i:03}" for i in range(120)))], + } + source = tmp_path / "source.json" + source.write_text(json.dumps(data)) + out = tmp_path / "report" + subprocess.run([sys.executable, str(HISTORY), "render", str(source), "--outdir", str(out)], check=True) + extracted = compile_pdf(out / "report.typ", out / "report.pdf", "--input", "body-font=Libertinus Serif") + pages = extracted.split("\f") + assert len(pages) >= 2 + assert "FIRST-PROMPT-SENTINEL" in pages[0] + assert "Phase 1: Scoping" in pages[0] + assert "SCOPE-SENTINEL" not in pages[0] and "COVERAGE-SENTINEL" not in pages[0] + tail = extracted[extracted.index("Sources and limits"):] + assert "SCOPE-SENTINEL" in tail and "COVERAGE-SENTINEL" in tail + assert extracted.rindex("Line 119") < extracted.index("Sources and limits") + assert re.search(r"Page Number In The Footer\s+1\s*$", pages[0], re.M)