Page anatomy
Each section of the published page, top to bottom, with the events and config keys it is built from and the exact wording it uses when something is missing.
The page is rendered from one Tera template into a single HTML file, .madelog/site/index.html, with inline CSS and about 30 KB for a small project. Everything the template can show is first collected into one context from the log and config.toml; a private note or a redacted prompt is dropped there, once, so it cannot reach the page by accident. The template has a small inline script for click-to-expand and works without it.
Two things about the whole page: the <title> is <project.name> · how it came together, and a <meta name="robots" content="noindex"> tag asks search engines to stay out. Colour is not used to carry meaning; "verified", "generated" and "hidden" are words.
Header
The project name from project.name (the repository directory name unless you changed it), and the date range of the record: the day of the earliest event to the day of the latest, or one date when both fall on the same day. A commit counts by when it was made — its committed_at — rather than by when madelog recorded it, so a repository madelog joined halfway through still opens on the day the work started:
<a class="mark" href="#top">ledger<small>24 Aug 2026 – 18 Sep 2026</small></a>After the dates, a status chip: project.status when you set one (active, paused, delivered, archived), otherwise derived from the log — delivered when project.delivered_url is set, active when a commit landed in the last 14 days, dormant after that. Hovering the chip says which of the two it is. A log with no commits yet reads active.
The badge at the foot of the page does not follow this: it stays on the chain's own timestamps, because what it certifies is the chain. Concepts has the full division. On a published page a "Verified record" link points at the badge, with the first eight characters of the root beside it; a local render has no link there.
1. Intent
The first screen is built to be judged in a few seconds, the way a CV is: one line saying what was built, the numbers in the largest type on the page, and three plain facts under them. Everything on it is derived from the log; nothing is written by hand.
| Element | Source |
|---|---|
| Headline | what from the newest intent event, cut to one clause: the project's own name is dropped from the front ("ledger, a tool that…" becomes "A tool that…"), the text stops at the first colon or semicolon, and anything past 72 characters is cut at a word with an ellipsis. Empty: "How <name> came together" |
| Line under it | The full what, only when the headline is a shortened form of it |
| Line under that | for_whom from the same event. Empty: omitted |
| Delivered | project.delivered_url, shown without its scheme. Unset: the row is omitted, so the page never opens on a "not yet" |
| Built with | tools from the intent event, one tag each. Empty: "not recorded" |
| Screenshot | project.screenshot, as a link, only when set |
| Record | "Hash-chained log of N events" where N is every event in the chain, the same number madelog verify prints — more than the timeline shows, because intents, patterns, summaries, hides and private decisions are chained but not drawn — followed by ", root confirmed by the server on <date>" or ", rendered on this machine". When a rebase, an amend or a history purge has removed commits the chain recorded, a sentence says how many "from before a history rewrite stay in the chain but are not counted here": they still count towards N, and nowhere else on the page. Then a link to the badge |
1b. At a glance
Under the intent, full width:
| Element | Source |
|---|---|
| Lead figure | "N% AI-written lines": of the lines added by commits that carry a recorded AI session, the share a session proposed, kept or reworked. It is the largest number on the page. When some commits carry no session the label says "where a session was recorded". Omitted when no session was recorded at all |
| Four figures | Commits; days active, with the span of the work ("17 Sep – 21 Sep", or one date) beside it; public decisions (private ones are not counted); AI sessions, hidden ones included |
| Up to three lines | In this order, each only when the log supports it: "Reworked N of M AI-written lines by hand" (or "Kept all M AI-written lines as they came"); "Decided: <the newest public decision>", cut at 96 characters; "N commits recorded as they happened, M events hash-chained", with ", root confirmed by the server" on a published page |
Days active are distinct local dates that carry a commit, counted by committed_at where a commit has one; the span is the earliest and latest moment the record covers.
2. Timeline
An inline SVG, 1000 units wide, with one axis and three lanes: decisions above the line, commits on it, AI sessions below. Each entry is placed by its timestamp between the earliest and latest moment the record covers (0 to 1 along the axis, rounded to four decimals). A commit uses its committed_at for both the position and the date shown in its record, so a backfilled history spreads across the axis instead of piling up at one instant. Day ticks sit under the axis at every midnight, Mondays heavier and labelled with the month; past 24 ticks the axis is thinned to every second, third or more day.
The records below the axis read in the order the work happened, not the order madelog recorded it. The two differ after a madelog backfill, which appends a year of history to the end of the chain.
| Timeline entry | Built from | Point | Record below |
|---|---|---|---|
| commit | every commit event | a circle on the axis | committed_at (or ts when it has none), commit, short sha, subject line; opens to the diff excerpts, one block per file that has one, or "No diff excerpt recorded for this commit (ignored, binary or redacted paths)." |
| decision | every decision event with private: false | a diamond above | timestamp, decision, the sentence; no detail |
| AI session | every ai_session event that is not hidden | a circle below | timestamp, AI session, short sha, "<tool> session"; opens to the prompt in a quote block, or "No prompt was recorded for this session (not captured, or redacted)." |
| hidden session | an ai_session named by a hide event | a dashed circle below | "hidden session", "AI session hidden by the author"; opens to "The author hid this session before publishing. The event stays in the hash chain; only its contents are withheld." |
intent, pattern, summary and hide events have no point of their own. Clicking a point opens its record in the "Every record (N)" list, which is collapsed by default. Without JavaScript the point is a plain link to the record's anchor.
Between the axis and the record list sit the weekly summaries: one block per week, headed "<d Mon> – <d Mon>", holding the newest summary event for that week and its marker. With no summaries: "No weekly summaries have been generated for this project yet."
3. Moments
Headed "One moment", "Two moments", up to "Five moments", from the candidates described in Concepts. Each is a title, a body of two to four sentences, and a row of links to the records it was built from, labelled by date and kind ("1 Sep commit · 1 Sep AI session · 1 Sep commit"). A moment with generated: true would carry a "generated" marker; in this version none does. With no candidates the section says: "Nothing in this record stood out as a moment worth retelling. The timeline above is the whole story."
4. Code attribution
Shown only when at least one ai_session event exists, hidden or not. Otherwise: "No AI session was recorded for this project, so there is no per-line record of what a model proposed. This page does not guess."
Only commits that carry a recorded session are attributed. A commit without one says nothing about who wrote its lines, so those lines are neither claimed for a model nor for a person; the paragraph says how many such commits there are ("N other commits carry no recorded session; their lines are not attributed either way").
| Figure | Computed as |
|---|---|
| total | ai_lines + human_lines, over the covered commits only |
| AI-proposed, accepted unchanged | sum of ai_lines over every session and file, minus reworked |
| AI-proposed, reworked by hand | sum of ai_lines_modified_by_human, capped at ai_lines |
| no recorded session proposed them | sum of added over the files of commits that carry a session, minus ai_lines (never below zero). Not called hand-written: a session madelog could not see may have written them |
The three percentages are rounded so they add up to 100. The bar is followed by a fixed note: "Lines are counted at commit time, so a line the AI proposed and the author rewrote in the same session counts as reworked. A line no recorded session proposed is not claimed as hand-written: a session madelog could not see may have written it. Deleted lines are not counted." The reworked figure counts lines a session wrote that the committed file does not contain; AI sessions has the rule.
Commits that a history rewrite removed from git are left out of all of this, the same way they are left out of the figures at the top.
5. Badge
Published:
Verified record. Records from 18 Sep 2026, 07:53 UTC to 18 Sep 2026, 07:53 UTC, root
6cc1406d8249, confirmed by the server on 18 Sep 2026, 07:53 UTC. Every event carries the SHA-256 of the one before it. Editing any record after the fact changes the root, and the root on the server would no longer match. Check the root
The full 64-character root follows on its own line, then the public address of the page. "Check the root" links to /v1/verify/<slug>.
Local render:
Not yet published. Records from
<first>to<last>, root<12 characters>. This page was rendered locally withmadelog publish --local. The root has not been confirmed by a server, so the record cannot be checked by a third party yet.
Either variant adds, when it applies: "N session(s) hidden by the author before publishing. Hiding is itself a recorded event; the chain stays whole."
Colophon
Three fixed lines close the page: "Generated by madelog from a local hash-chained log.", "Paragraphs marked generated were written by a model and reviewed by the author.", and "Raw prompts and full diffs never left the author's machine."
Where each word comes from
| Wording | Condition |
|---|---|
"generated locally (<model>)" | summary with generated_by: ollama:<model> |
| "generated from the log" | summary with generated_by: heuristic |
"summary generated by remote model (<model>)" | summary with generated_by: remote:<model>; not produced in this version |
| ", edited" appended to any of the above | summary with edited: true and a generated generated_by; a summary rewritten by you has generated_by: user and no marker at all |
| "1 session hidden" | one hide event for a distinct session; two hide events for the same session still count one |
status chip derived / set (in the tooltip) | project.status unset / set. Derived: delivered with a delivered link, active with a commit in the last 14 days, else dormant |
Printing
A print stylesheet sets white paper, black ink, 9.5 pt body text and keeps sections from breaking across pages, so the page can be saved as a PDF from the browser with the badge visible. A printed copy ends with a stamp line the screen never shows: the project name, its status, the timestamp of the last record, when and where it was published (or "unpublished local render"), and the first twelve characters of the root. madelog export --report writes the same facts as JSON, with the moment of export.
On madelog Pro the server renders the same PDF for you: https://api.madelog.app/p/<slug>.pdf is the published page printed through a real browser engine, with · exported <date, time UTC> added to the stamp at the moment you fetch it. The PDF is cached per published version and per day, so a re-publish gives a fresh one. A page on the free plan answers 402 with pro_required; the HTML page itself is never gated.