// Simple numbering for non-book documents #let equation-numbering = "(1)" #let callout-numbering = "1" #let subfloat-numbering(n-super, subfloat-idx) = { numbering("1a", n-super, subfloat-idx) } // Theorem configuration for theorion // Simple numbering for non-book documents (no heading inheritance) #let theorem-inherited-levels = 0 // Theorem numbering format (can be overridden by extensions for appendix support) // This function returns the numbering pattern to use #let theorem-numbering(loc) = "1.1" // Default theorem render function #let theorem-render(prefix: none, title: "", full-title: auto, body) = { if full-title != "" and full-title != auto and full-title != none { strong[#full-title.] h(0.5em) } body } // Some definitions presupposed by pandoc's typst output. #let content-to-string(content) = { if content.has("text") { content.text } else if content.has("children") { content.children.map(content-to-string).join("") } else if content.has("body") { content-to-string(content.body) } else if content == [ ] { " " } } #let horizontalrule = line(start: (25%,0%), end: (75%,0%)) #let endnote(num, contents) = [ #stack(dir: ltr, spacing: 3pt, super[#num], contents) ] #show terms.item: it => block(breakable: false)[ #text(weight: "bold")[#it.term] #block(inset: (left: 1.5em, top: -0.4em))[#it.description] ] // Some quarto-specific definitions. #show raw.where(block: true): set block( fill: luma(230), width: 100%, inset: 8pt, radius: 2pt ) #let block_with_new_content(old_block, new_content) = { let fields = old_block.fields() let _ = fields.remove("body") if fields.at("below", default: none) != none { // TODO: this is a hack because below is a "synthesized element" // according to the experts in the typst discord... fields.below = fields.below.abs } block.with(..fields)(new_content) } #let empty(v) = { if type(v) == str { // two dollar signs here because we're technically inside // a Pandoc template :grimace: v.matches(regex("^\\s*$")).at(0, default: none) != none } else if type(v) == content { if v.at("text", default: none) != none { return empty(v.text) } for child in v.at("children", default: ()) { if not empty(child) { return false } } return true } } // Subfloats // This is a technique that we adapted from https://github.com/tingerrr/subpar/ #let quartosubfloatcounter = counter("quartosubfloatcounter") #let quarto_super( kind: str, caption: none, label: none, supplement: str, position: none, subcapnumbering: "(a)", body, ) = { context { let figcounter = counter(figure.where(kind: kind)) let n-super = figcounter.get().first() + 1 set figure.caption(position: position) [#figure( kind: kind, supplement: supplement, caption: caption, { show figure.where(kind: kind): set figure(numbering: _ => { let subfloat-idx = quartosubfloatcounter.get().first() + 1 subfloat-numbering(n-super, subfloat-idx) }) show figure.where(kind: kind): set figure.caption(position: position) show figure: it => { let num = numbering(subcapnumbering, n-super, quartosubfloatcounter.get().first() + 1) show figure.caption: it => block({ num.slice(2) // I don't understand why the numbering contains output that it really shouldn't, but this fixes it shrug? [ ] it.body }) quartosubfloatcounter.step() it counter(figure.where(kind: it.kind)).update(n => n - 1) } quartosubfloatcounter.update(0) body } )#label] } } // callout rendering // this is a figure show rule because callouts are crossreferenceable #show figure: it => { if type(it.kind) != str { return it } let kind_match = it.kind.matches(regex("^quarto-callout-(.*)")).at(0, default: none) if kind_match == none { return it } let kind = kind_match.captures.at(0, default: "other") kind = upper(kind.first()) + kind.slice(1) // now we pull apart the callout and reassemble it with the crossref name and counter // when we cleanup pandoc's emitted code to avoid spaces this will have to change let old_callout = it.body.children.at(1).body.children.at(1) let old_title_block = old_callout.body.children.at(0) let children = old_title_block.body.body.children let old_title = if children.len() == 1 { children.at(0) // no icon: title at index 0 } else { children.at(1) // with icon: title at index 1 } // TODO use custom separator if available // Use the figure's counter display which handles chapter-based numbering // (when numbering is a function that includes the heading counter) let callout_num = it.counter.display(it.numbering) let new_title = if empty(old_title) { [#kind #callout_num] } else { [#kind #callout_num: #old_title] } let new_title_block = block_with_new_content( old_title_block, block_with_new_content( old_title_block.body, if children.len() == 1 { new_title // no icon: just the title } else { children.at(0) + new_title // with icon: preserve icon block + new title })) align(left, block_with_new_content(old_callout, block(below: 0pt, new_title_block) + old_callout.body.children.at(1))) } // 2023-10-09: #fa-icon("fa-info") is not working, so we'll eval "#fa-info()" instead #let callout(body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white) = { block( breakable: false, fill: background_color, stroke: (paint: icon_color, thickness: 0.5pt, cap: "round"), width: 100%, radius: 2pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: background_color, width: 100%, inset: 8pt)[#if icon != none [#text(icon_color, weight: 900)[#icon] ]#title]) + if(body != []){ block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // arxivai — Quarto Typst template partial. // Replaces Quarto's default `article()` with the arxivai preprint layout. // Both packages are importable from raw ```{=typst}``` blocks in the body: // #arxivai.operation-box(...) #arxivai.app-figure(...) #lettreai.badge(...) #import "lettreai.typ" as lettreai #import "arxivai.typ" as arxivai // --------------------------------------------------------------------------- // Re-theme Quarto's automatic callouts (same contract as lettreai: block // nesting kept structurally identical to Quarto's default so the crossref // figure show rule in definitions.typ keeps working). // --------------------------------------------------------------------------- #let callout( body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white, ) = { let accent = if icon_color == black { lettreai.colors.oxford-blue } else { icon_color } block( breakable: false, fill: lettreai.tint(accent, 0.94), stroke: (left: 3pt + accent, rest: 0.5pt + lettreai.tint(accent, 0.5)), width: 100%, radius: 3pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: lettreai.tint(accent, 0.84), width: 100%, inset: 8pt, )[#if icon != none [#text(accent, weight: 900)[#icon] ]#text(fill: lettreai.shade(accent, 0.25), weight: 700)[#title]]) + if (body != []) { block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // --------------------------------------------------------------------------- // arXiv preprint article — same signature contract as Quarto's default, // extended with keywords and running-title. // --------------------------------------------------------------------------- #let article( title: none, subtitle: none, authors: none, keywords: (), running-title: none, book: false, date: none, abstract-title: none, abstract: none, thanks: none, cols: 1, lang: "en", region: "US", font: none, fontsize: 10.5pt, title-size: 1.7em, subtitle-size: 1.15em, heading-family: none, heading-weight: "bold", heading-style: "normal", heading-color: black, heading-line-height: 0.65em, mathfont: none, codefont: none, linestretch: 1, sectionnumbering: none, linkcolor: none, citecolor: none, filecolor: none, toc: false, toc_title: none, toc_depth: none, toc_indent: 1.5em, doc, ) = { // PDF metadata. set document(title: title) set document( author: authors.map(a => lettreai.to-string(a.name)).join(", ", last: " & "), ) if authors != none and authors != () set document( keywords: keywords.map(k => lettreai.to-string(k)), ) if keywords != none and keywords != () set par(justify: true, leading: linestretch * 0.65em) set text(lang: lang, region: region, size: fontsize) show math.equation: set text(font: mathfont) if mathfont != none set heading(numbering: sectionnumbering) // Running header + footer; book mode gets chapter-aware heads and the // O'Reilly outer-margin geometry. let rt = if running-title != none { running-title } else if title != none { title } else { [] } set page( header: if book { arxivai.book-header() } else { arxivai.arxiv-header(rt) }, footer: if book { arxivai.book-footer() } else { arxivai.arxiv-footer() }, ) set page(margin: arxivai.book-margin) if book // Resolve fonts: explicit Quarto fonts win, else theme defaults. let resolved-font = if font != none { font } else { lettreai.body-font } let resolved-code = if codefont != none { codefont } else { lettreai.mono-font } let resolved-heading-color = if heading-color != black { heading-color } else { lettreai.colors.oxford-blue } let resolved-link = if linkcolor != none { rgb(lettreai.to-string(linkcolor)) } else { lettreai.colors.oxford-blue } // Paint everything with the shared theme, then the figure rules. show: lettreai.theme-rules.with( fonts: resolved-font, code-font: resolved-code, heading-color: resolved-heading-color, link-color: resolved-link, ) show: arxivai.arxiv-figure-rules // Book chapter rule is the innermost show, so it transforms level-1 // headings FIRST (inner rules apply first), replacing them with chapter // openers before the theme's h1 rule could wrap them in a container. // Synthetic non-outlined headings (e.g. the outline's own title) are // excluded inside book-heading-rules. show: doc2 => if book { arxivai.book-heading-rules(doc2) } else { doc2 } show ref: set text(fill: rgb(lettreai.to-string(citecolor))) if citecolor != none // Preprint front matter. arxivai.arxiv-title-block( title: title, subtitle: subtitle, authors: authors, date: date, ) if abstract != none { arxivai.arxiv-abstract( abstract, abstract-title: if abstract-title == none { [Abstract] } else { abstract-title }, ) } arxivai.arxiv-keywords(keywords) // Book mode: the front matter is a title page of its own. if book { pagebreak(weak: true) } // Optional table of contents (its own page in book mode). if toc { block(above: 0.5em, below: 2em, lettreai.lettreai-outline( title: if toc_title == none { auto } else { toc_title }, depth: toc_depth, indent: toc_indent, )) if book { pagebreak(weak: true) } } if cols > 1 { columns(cols, doc) } else { doc } } #import "@preview/fontawesome:0.5.0": * #let brand-color = (:) #let brand-color-background = (:) #let brand-logo = (:) #set page( paper: "a4", margin: (x: 2.4cm,y: 2.6cm,), numbering: "1", columns: 1, ) #show: doc => article( title: [Literate Coding with AI Agents: A Methodology for Computational Analysis of Scientific Objects], subtitle: [Complexity Reduction through Structured Context, Executable Specification, and Closed-Loop Refinement], authors: ( ( name: [Gerd Graßhoff], affiliation: [Humboldt-Universität zu Berlin, Deutsches Museum], email: [gerd.grasshoff\@hu-berlin.de], orcid: [0000-0002-4450-357X] ), ), keywords: ([literate coding],[AI agents],[executable specification],[reproducible research],[complexity reduction],[computational archaeology],[scientific methodology],), running-title: [Literate Coding with AI Agents], abstract: [We present a computational research methodology that unifies Knuth's literate programming with autonomous AI agent capabilities for the analysis of complex scientific objects. The central challenge in agent-assisted research is context fragmentation: mathematical derivations live in papers, algorithms in repositories, observational data in archives, and success criteria in the researcher's head. When an AI agent operates across these disjoint silos, it inevitably generates code that is syntactically valid but physically or historically incoherent. We establish the Principle of Complexity Reduction: before delegating analytical tasks, the researcher constructs a single executable document containing domain theory, data schemas, quantitative thresholds, and code. The methodology defines three operational levels: (1) a living reference background under permanent revision, (2) a structured planning substrate, and (3) autonomous agentic execution with a closed feedback loop that updates the domain theory upon experimental failure. We demonstrate the methodology on the 3D reconstruction of ancient Hellenistic conic sundials. ], abstract-title: "Abstract", font: ("Source Sans 3",), sectionnumbering: "1.1", toc: true, toc_title: [Table of contents], toc_depth: 3, doc, ) #block[ #callout( body: [ Supplement S7 to the Ancient Sundials repository (Berlin Sundial Collaboration, ancient-sundials.lettre.ai). This draft documents how the repository's measurement pipeline was built and checked. It is a preprint draft and has not been peer reviewed. Where a figure in the repository differs from a figure here, the repository, which is rebuilt from the current database, is authoritative. ] , title: [ Supplement S7 ] , background_color: rgb("#dae6fb") , icon_color: rgb("#0758E5") , icon: fa-info() , body_background_color: white ) ] = Introduction: The Problem of Context in AI-Assisted Research The computational analysis of complex scientific objects --- such as 3D scans of fragmentary archaeological instruments, multi-spectral manuscripts, or astronomical spectra --- requires the simultaneous coordination of four distinct knowledge domains: + #strong[Domain Theory:] The physical laws, spherical astronomy, and mathematical theorems that govern the object @grasshoff2026conic. + #strong[Observational Data:] Sensor point clouds, mesh representations, and coordinate metrology @bsdp2014dialface18. + #strong[Algorithms:] Numerical optimization routines, curve fitters, and signal filters @bard1974. + #strong[Success Criteria:] Quantitative tolerance thresholds derived from physical error models rather than convenience. In conventional computational science, these four domains are scattered across disjoint media: journal articles, git repositories, remote databases, and undocumented researcher intuition @claerbout1992@peng2011. When large language model (LLM) agents are introduced into such fragmented workflows, they inherit this structural incoherence. An AI agent given access to code and data without the governing theoretical derivations will hallucinate plausible-sounding but geometrically invalid algorithms. #arxivai.concept-box(title: [The Principle of Complexity Reduction])[ Before delegating an analytical problem to an AI agent, reduce the problem to its essential structure by constructing a single literate document that embeds: (1) the domain theory and derivations, (2) the observational data schema, (3) the quantitative success criteria, and (4) the runnable code blocks. The literate document is the agent's complete and bounded world. ] This principle builds on Donald Knuth's classical #emph[literate programming] @knuth1984literate. However, where traditional literate programming focused on interleaving code with software documentation for human readers, #strong[literate coding for AI agents] extends the document into a living, #emph[executable specification] that actively constrains and guides autonomous agent reasoning. = The Three Levels of Agentic Operation The literate coding methodology establishes three hierarchically coupled modes of interaction between the researcher, the document, and the AI agent: #arxivai.method-box(title: [The three-level operational model])[ - Level 1 (Reference Background): Domain theory under permanent revision. - Level 2 (Planning Substrate): Formal derivation of agent execution plans. - Level 3 (Agentic Action): Autonomous code execution and closed-loop validation. ] == Level 1: The Living Reference Background At Level 1, the literate Quarto document serves as the authoritative repository of domain definitions, coordinate conventions, and analytical proofs. Unlike a static published paper, the Level 1 specification is under #strong[permanent revision]: - When an analytical experiment at Level 3 fails, the failure is diagnosed to determine whether the error lies in code or in an oversimplified theoretical assumption. - If the theory was deficient, the Level 1 document is directly updated. - This revision immediately updates the agent's prompt context, preventing repeated hallucinations and enforcing newly discovered physical constraints. == Level 2: The Structured Planning Substrate At Level 2, the AI agent consumes the Level 1 specification to synthesize an actionable plan before generating executable code: - The agent maps each high-level research goal to specific mathematical constraints in the text. - It verifies that the required data formats and coordinate frames are aligned. - It establishes quantitative acceptance thresholds (e.g.~"Fit RMS $lt.eq 0.4 thin upright(m m)$ governed by the $plus.minus 0.3 thin upright(m m)$ scan noise floor"). == Level 3: Autonomous Agentic Execution and Feedback At Level 3, the agent executes the plan in a sandboxed runtime environment: - It runs Python or Julia scripts against the 3D data. - It compares numerical outputs against the Level 2 criteria. - If a criterion fails, it logs an explicit diagnostic trace. - The closed feedback loop either adjusts algorithmic parameters or triggers a theoretical revision at Level 1. = The Document as a Directed Acyclic Graph (DAG) An executable specification is structured as a directed acyclic graph of formal dependencies: $ upright("Domain Theory ") cal(T) arrow.r^(upright("constrains")) upright("Methods ") cal(M) arrow.r^(upright("implements")) upright("Executable Code ") cal(C) arrow.r^(upright("produces")) upright("Results ") cal(R) $ $ upright("Domain Theory ") cal(T) arrow.r^(upright("derives")) upright("Criteria ") cal(K) arrow.r^(upright("evaluates")) upright("Results ") cal(R) arrow.r^(upright("feedback")) cal(T) $ #emph[Reading.] The theoretical domain model $cal(T)$ constrains the algorithmic methods $cal(M)$ and derives the quantitative success criteria $cal(K)$. Code $cal(C)$ executes $cal(M)$ on data to produce results $cal(R)$, which are evaluated against $cal(K)$. Discrepancies feed back directly into revising $cal(T)$. = Case Study: Reconstructing Ancient Conic Sundials We illustrate the complete three-level methodology on the reconstruction of ancient Hellenistic conic sundials from photogrammetric 3D surface scans @bsdp2014dialface18. == Step 1: Embedding the Domain Theory (Level 1) The literate specification incorporates the complete construction geometry from #cite(, form: "prose"): - Coordinate systems: East--North--Up world frame and equatorial polar frame. - Forward shadow projection: ray--cone intersection quadratic equations. - The equinox invariant: $ell_E = d$. - The apex-free marking ratio $Q =\(L_S - L_W\)\/\(L_S - L_E\)$ and its exact inversion $alpha = arctan\(\(2 - Q\)\/\(Q tan epsilon\)\)$. == Step 2: Agentic Planning (Level 2) An AI sub-agent parses the Quarto document and constructs a 5-step execution plan: + Ingest PLY point clouds for Dialface ObjID 18 (British Museum inv. 2547). + Slice cross-sectional bands and extract medial groove centrelines. + Fit 3D circular arcs to hour lines and coaxial circles to day-lines. + Extract noon marks $P_S\,P_E\,P_W$ and apply closed-form $Q$-inversion. + Combine front plane, cone axis, and solstitial hour divisions into consensus latitude $phi$. == Step 3: Execution, Anomaly Detection, and Theory Refinement (Level 3) During execution, the agent encounters two major physical anomalies: + #strong[Straight Line Failure:] Straight lines leave systematic bow residuals of $2.2$--$7.6 thin upright(m m)$ on outer hour lines. The agent diagnoses that ancient artisans carved hour lines as circular arcs ($0.20$--$0.71 thin upright(m m)$ RMS), triggering a Level 1 update in the document. + #strong[Depth Notch Failure:] The minimum depth estimator ($upright(a r g m i n)_z$) diverges due to $plus.minus 0.3 thin upright(m m)$ scan noise. The agent substitutes the medial-line filter and updates the specification. #arxivai.operation-box( title: [Literate Coding Agent Closed-Loop Execution], question: [Execute reconstruction cascade on ObjID 18 with autonomous anomaly handling], steps: ( (agent: "spec-reader", does: [Reads domain theory from conic-sundial-geometry.qmd and extracts constraint catalog]), (agent: "code-generator", does: [Synthesizes Python pipeline utilizing MedialSliceExtractor and ApexFreeInverter]), (agent: "test-runner", does: [Evaluates fit residuals against $sigma_0 = 0.3$ mm threshold; detects straight-line bow anomaly]), (agent: "theory-updater", does: [Amends Level 1 specification to include circular arc model class and re-runs fit]), (agent: "consensus-builder", does: [Verifies exact closure: $alpha = 34.40 degree$, $d = 101.3$ mm, $phi = 41.45 degree$ ($s=53;02$)]), ), latency: "2.1 s", answer: [All criteria passed; Level 1 specification amended; parameter deduction verified within 0.6 standard errors], ) = Epistemic Standards and Reproducibility The literate coding methodology enforces three foundational epistemic guarantees: + #strong[Traceability:] Every parameter in the final paper is programmatically derived from raw observation data through documented code blocks. + #strong[Constraint Enforcement:] AI agents are forbidden from introducing free parameters that violate the labeled constraints in the Level 1 catalog. + #strong[Reproducibility:] Re-rendering the literate document executes the entire computational pipeline from 3D point cloud to publication-ready PDF tables and figures. = Conclusion As autonomous AI agents assume greater responsibility in scientific data analysis, prompt engineering must be replaced by formal, executable specifications. Literate coding bridges the gap between high-level domain theory and low-level code execution. By embedding mathematical derivations, data contracts, and quantitative criteria into a unified living document, scientific discoveries can be conducted with unprecedented speed while maintaining absolute epistemological rigor. #heading(level: 1, numbering: none)[References] #bibliography(("references.bib"))