Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.2.3 (2026-10-19)

- A library can keep its evidence apart in the rail. The charter's `rail: { evidence: [protocol, result] }` names genres that are audit records rather than reading: the rail's "By genre" section then lists the other genres in reading order, and the evidence genres follow in one closed group headed Evidence, which counts them while closed and opens, as any group does, on the page being read. Without the option the packs decide: a pack's `PACK.md` may name its own genres under `evidence`, and the lab pack names `protocol` and `result`, so a lab library gets this without a change to its charter. `rail: { evidence: [] }` lists every genre together in "By genre", by name, as before; an empty `evidence:` is an error asking for `[]`. `folio config get|set rail.evidence` reads and sets it, and the new gate check `rail-genres` (error) names a genre in it that does not exist or is switched off.
- The rail's reading order places the lab genres: guide, entry, report, concept, claim, question, note, survey, reading, source, map, protocol, result, then any other genre by name. A long map without sections lists its genres in this order.

## 0.2.2 (2026-10-19)

- The README's closing section describes folio, giro and lab-kit by what each does: folio keeps the documents, giro takes a spec to proof on a branch, and lab-kit runs research on top of folio. No change to the engine.
Expand Down
1 change: 1 addition & 0 deletions docs/spec/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ These run on every document. No card needs to name them.
| `placeholder` | No `{{...}}` placeholder is left in a document. | error |
| `orphan` | The no-orphans rule: a document is on a map, or linked from a map or from a document on a map, as its card's `on_map` asks. A field of type `id` counts as a link both ways, so a document named by a reachable document's field, or naming one, is reachable. Links from journal entries do not count. | error |
| `home-maps` | Every map the charter lists under `home.maps` exists and is not a `draft`. | error |
| `rail-genres` | Every genre the charter lists under `rail.evidence` exists and is switched on: its pack is on and it is not disabled. | error |
| `unique-id` | Every id is unique across the library, whatever the genre, compared without regard to case: slugs, record ids, and journal entry ids (their file stems). A record id is in its genre's prefix and is never reused, even after its document is retired. | error |
| `index-current` | The generated indices in `.folio/` match what `folio index` would write. | error |
| `stale-quote` | An open question still quotes text that is on its document. | error |
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The `--json` output of `folio cite`, `folio maps`, `folio cards`, `folio genres`
| `folio annotations resolve [--doc ..] [--label ..] --state <s> --body ".." [--author <who>]` | Moves every thread the filters select to one state, with one reply each. |
| `folio cite <id> [--json]` | Prints a document's path, title, description, status (derived for a permanent document) and the citation markup for HTML, Markdown and LaTeX, always in the same form for each. `folio cite <library>:<id>` prints the link into another library the charter names (HTML and Markdown only: a paper cites only its own library). |
| `folio search <words> [--all]` | Searches this library, or with `--all` also every library the charter lists under `search` and every library under `libraries` that is at its path. It reindexes in memory first, so a document just created is found. Titles, descriptions, ids, tags and text are searched. A word matches a whole word or the start of one, and words under two characters are ignored; with several words, a document must match all of them. Each hit shows its status, and retired documents come last. |
| `folio config get [<key>]` / `folio config set <key> <value>` | Reads or changes the charter. Keys are dotted (`home.maps`, `genres.concept.max_words`, `checks.uncited-number`, `journal.kinds`, `site_url`, `comments.identity_header`, `save.gate`, `save.review`, `save.url`); a list is given as comma-separated values. `set` keeps the charter's key order. Only set-up and configure use `set`. |
| `folio config get [<key>]` / `folio config set <key> <value>` | Reads or changes the charter. Keys are dotted (`home.maps`, `genres.concept.max_words`, `checks.uncited-number`, `journal.kinds`, `site_url`, `comments.identity_header`, `save.gate`, `save.review`, `save.url`, `rail.evidence`); a list is given as comma-separated values. `set` keeps the charter's key order. Only set-up and configure use `set`. |
| `folio pack list` | The packs available: shipped, in the library's `packs/`, or pinned. Outside a library, the packs folio ships. |
| `folio pack on <name> \| --git <url> --ref <commit>` | Switches a pack on in the charter; a git pack is fetched and pinned. |
| `folio pack off <name>` | Switches a pack off. |
Expand Down
6 changes: 5 additions & 1 deletion docs/spec/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ packs:
ref: <commit>
home:
maps: [proof-repair, solvers] # the top-level maps, in order
rail:
evidence: [protocol, result] # genres the rail lists last and apart, as evidence (§14)
genres: # optional per-genre settings
journal: { max_words: 120 }
survey: { enabled: false }
Expand Down Expand Up @@ -245,6 +247,8 @@ Defaults: no packs, the folio theme, `assets/`, `root` is the folder holding `fo

Under `libraries:`, each name is a lowercase slug, used as the scheme of a link (§4), and may not be a scheme the web already uses (`http`, `https`, `mailto` and the like). `path` is required, relative to the folder holding `folio.yaml`; `url` is optional and must be an http(s) address. `folio search --all` searches these libraries too, where their path holds one.

Under `rail:`, `evidence` lists genres the library rail shows last, apart from the reading genres (§14). Without it, the `evidence` of every pack switched on applies (the lab pack's is `protocol` and `result`); an empty list keeps every genre with the reading genres, listed by name. An empty `evidence:` (null) is an error: write `[]`. Each genre it names exists and is switched on (check `rail-genres`).

Under `genres:`, a genre takes `enabled` and any setting of its card's checks. Under `checks:`, a check takes `error`, `warning` or `off`. Check names are listed in `checks.md`.


Expand Down Expand Up @@ -313,7 +317,7 @@ Every HTML document uses the shared shell: one stylesheet, light and dark themes

A page that is one line of argument may opt into the reading column with `<meta name="layout" content="column">`; a Markdown record is always rendered in it. The shell offers colour registers, a few shared components, syntax colours for code listings (highlight.js, by each listing's `language-<name>` class or a guess) and KaTeX math (`$…$`, `\[…\]`, on a page carrying `<meta name="math" content="katex">`), all described in `shell/COMPONENTS.md`. How to choose a page's form is in `craft/layout.md`. A figure, control or animation earns its place by showing what prose cannot; a page uses one where it carries the idea, never to decorate. The shell shows an inline SVG near the size it was drawn for, so its labels do not grow with the window, and lets the reader open any figure larger in a dialog.

The library rail is on the left of every page, the home page, the journal and the Review page included. It links home, the journal and, when served, the Review page. Then it lists the charter's home maps in order, each a group of the documents it lists, and then every document other than a journal entry, grouped by genre. Inside a map's group the documents keep the map's own sections: each list of rows sits under the heading nearest above it on the map page, and consecutive lists under one heading are one section. A map with two or more such sections shows them as sub-groups, each counting its genres while closed; a long map (more than twelve rows) opens only its first section and the one being read. A map with fewer sections lists its documents by genre in reading order (guide, entry, concept, note, survey, reading, source) when it has more than six, and as one list otherwise. Each document in a map's group carries its genre's mark, a letter or two with the genre's name on hover, and a key at the foot of the rail names the marks shown. The map's first document is marked Start. It is drawn from the indices alone (`catalog.json`, `nav.json`) and the site data, so no page lists the library by hand. The current page is marked and scrolled into view, and a guide being read shows its chapters. A group the reader opens or closes stays so in that browser. On a wide screen the rail sits beside the page and the page panel; the `[` key or the top bar's library button hides or shows it, and that choice is remembered. On a narrow screen it is a drawer that the same button or key opens and Escape, a click outside or a link closes.
The library rail is on the left of every page, the home page, the journal and the Review page included. It links home, the journal and, when served, the Review page. Then it lists the charter's home maps in order, each a group of the documents it lists, and then every document other than a journal entry, grouped by genre. Inside a map's group the documents keep the map's own sections: each list of rows sits under the heading nearest above it on the map page, and consecutive lists under one heading are one section. A map with two or more such sections shows them as sub-groups, each counting its genres while closed; a long map (more than twelve rows) opens only its first section and the one being read. A map with fewer sections lists its documents by genre in reading order (guide, entry, report, concept, claim, question, note, survey, reading, source, map, protocol, result, then any other genre by name) when it has more than six, and as one list otherwise. Each document in a map's group carries its genre's mark, a letter or two with the genre's name on hover, and a key at the foot of the rail names the marks shown. The map's first document is marked Start. When the library names evidence genres (the charter's `rail.evidence`, or its packs', §10), the genre groups list the reading genres in reading order, and the evidence genres follow in one closed group of their own, headed Evidence, that counts them while closed; it opens, as any group does, when it holds the page being read. Without evidence genres, the genre groups are listed by name. It is drawn from the indices alone (`catalog.json`, `nav.json`) and the site data, so no page lists the library by hand. The current page is marked and scrolled into view, and a guide being read shows its chapters. A group the reader opens or closes stays so in that browser. On a wide screen the rail sits beside the page and the page panel; the `[` key or the top bar's library button hides or shows it, and that choice is remembered. On a narrow screen it is a drawer that the same button or key opens and Escape, a click outside or a link closes.

**Topic focus.** A reader can focus the library on one or more topics, chosen from the Focus control in the top bar. A document is in focus when it falls under a chosen topic (§5). While a focus is on, the rail (its maps, genre groups and counts), search, the journal view, the Review page, the home page's inbox line, its map cards and its recent entries show only what is in focus, and a chip in the top bar names the focus and clears it in one click. Search can still widen to the whole library for one search. Maps, genres and tags still filter inside the focus. A page outside the focus still opens by its address and says, under its header, that it is outside the focus. While it is read, the rail keeps it in its genre group, marked. The focus is remembered in that browser. It is computed from the catalog alone, so it works the same on an exported site.

Expand Down
3 changes: 3 additions & 0 deletions docs/spec/pack-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ genres: [question, protocol, result, claim, report]
workflows: [record-a-result, write-a-report]
rules: rules.md
journal_kinds: [lock, run, kill, instrument, pivot] # kinds the journal accepts while the pack is on
evidence: [protocol, result] # its genres the library rail lists last and apart, unless the charter sets rail.evidence
---
```

`evidence`, when present, lists genres from the pack's `genres`, each once. A library's charter overrides it with `rail.evidence` (model §10).

The body says, in plain sentences:

- `# <Name>`: what kind of work this pack is for, in one sentence.
Expand Down
2 changes: 2 additions & 0 deletions packs/lab/PACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ genres: [question, protocol, result, claim, report]
workflows: [record-a-result, write-a-report]
rules: rules.md
journal_kinds: [lock, run, kill, instrument, pivot]
evidence: [protocol, result]
---

# Lab
Expand All @@ -24,6 +25,7 @@ This pack is for work that tests ideas against evidence: it gives questions, pla
- **Every number cites a result** (rule, check `uncited-number`). A number that reports a measurement links the result that holds it.
- **A citation names a current result** (rule, check `cites-superseded`). A revised document that cites a superseded or retracted result is flagged.
- **Journal kinds** `lock` (a protocol locked), `run` (a run launched, checkpointed, failed or ended), `kill` (a kill rule fired), `instrument` (a change to how the lab measures) and `pivot` (a change of direction). They join the core's four (`decision`, `lesson`, `correction`, `retraction`) while the pack is on.
- **Evidence in the rail.** Protocols and results are audit records, so the library rail lists them last, in a closed Evidence group, after the reading genres. The charter's `rail.evidence` changes this. `rail: { evidence: [] }` puts them back among the other genres in the "By genre" section, every genre listed by name as before. The reading order that sorts a long map's documents by genre still places protocols and results last.

A result's evidence and its re-derive command usually sit outside the library, in the project around it. They resolve from the charter's `root`, so set `root` to the project root when the library is a folder inside it.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
dynamic = ["readme"]
name = "folio-kb"
version = "0.2.2"
version = "0.2.3"
description = "A knowledge library a coding agent builds and keeps, checked by a gate."
license = "MIT"
authors = [{ name = "Piergiuseppe Mallozzi" }]
Expand Down
6 changes: 6 additions & 0 deletions shell/folio.css
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ img, svg, video { max-width: 100%; height: auto; }
.f-rgroup[data-open] > .f-rhead > .f-rsum { display: none; }
.f-rail .f-rsec .f-rlist { margin: 0 0 .3rem .62rem; padding-left: .3rem; }
.f-rail .f-rsec .f-rlist a { padding-left: .35rem; }
/* The evidence genres, last and apart: one closed group headed like a section, which counts
its genres on a line of its own while closed. */
.f-revidence > .f-rgroup > .f-rhead { flex-wrap: wrap; }
.f-revidence > .f-rgroup > .f-rhead > .f-rname-btn { font: 650 .66rem/1.3 var(--sans); letter-spacing: .12em;
text-transform: uppercase; color: var(--ink-3); }
.f-revidence > .f-rgroup > .f-rhead:hover > .f-rname-btn { color: var(--ink-2); }
/* A genre's mark, and the map's way in. */
.f-gmark { flex: none; align-self: center; display: inline-flex; align-items: center; justify-content: center;
min-width: 1.15rem; height: 1.15rem; padding: 0 .2rem; border: 1px solid var(--rule); border-radius: 4px;
Expand Down
37 changes: 29 additions & 8 deletions shell/folio.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@

/* --------------------------------------------------------- library rail */
/* The library on the left of every page: the charter's home maps, each with the documents
it lists, then every document by genre. All of it comes from the catalog, the guides'
nav and the site data. A group the reader opens or closes is remembered in this browser. */
it lists, then every document by genre, and the evidence genres last and apart when the
library names some. All of it comes from the catalog, the guides' nav and the site data.
A group the reader opens or closes is remembered in this browser. */
var NARROW = "(max-width: 77.99rem)";
function narrow() { return !!(window.matchMedia && matchMedia(NARROW).matches); }
function plural(word) {
Expand All @@ -334,7 +335,8 @@
reading: "R", source: "S", survey: "Sv", question: "Q", protocol: "Pr", result: "Rs", claim: "Cl", report: "Rp" };
function markOf(genre) { return MARKS[genre] || human(genre).slice(0, 2); }
/* The order a reader meets genres in: the way in first, then ideas, then the evidence. */
var READING = ["guide", "entry", "concept", "note", "survey", "reading", "source", "map"];
var READING = ["guide", "entry", "report", "concept", "claim", "question", "note", "survey", "reading", "source", "map",
"protocol", "result"];
function genreRank(genre) { var i = READING.indexOf(genre); return i < 0 ? READING.length : i; }
function summary(docs) {
var n = {};
Expand Down Expand Up @@ -483,13 +485,32 @@
if (!inFocus(d) && d !== hereDoc) return; // the page being read stays in the rail, marked
(genres[d.genre] = genres[d.genre] || []).push(d);
});
var byGenre = Object.keys(genres).sort().map(function (g) {
var docs = genres[g].slice().sort(function (a, b) {
function genreDocs(g) {
return genres[g].slice().sort(function (a, b) {
return isRecordId(a.id) && isRecordId(b.id) ? a.id.localeCompare(b.id, undefined, { numeric: true }) : a.title.localeCompare(b.title);
});
return group("genre:" + g, el("button", { class: "f-rname-btn", type: "button", text: plural(g) }), docs, false);
});
section("By genre", byGenre);
}
function genreGroup(g, cls) {
return group("genre:" + g, el("button", { class: cls || "f-rname-btn", type: "button", text: plural(g) }), genreDocs(g), false);
}
/* The charter's (or its packs') evidence genres, such as a lab's protocols and results, are
audit records: they go last, in one closed group of their own, and the reading genres
above them follow reading order. Without any, the genres are listed as they always were. */
var evidence = (((D.site.rail || {}).evidence) || []).filter(function (g) { return genres[g]; });
var reading = Object.keys(genres).filter(function (g) { return evidence.indexOf(g) < 0; });
reading.sort(evidence.length ? function (a, b) { return genreRank(a) - genreRank(b) || a.localeCompare(b); } : undefined);
section("By genre", reading.map(function (g) { return genreGroup(g); }));
if (evidence.length) {
var proof = [];
evidence.forEach(function (g) { proof = proof.concat(genres[g]); });
var head = el("button", { class: "f-rname-btn", type: "button", text: "Evidence",
title: evidence.map(function (g) { return plural(g); }).join(", ") });
box.appendChild(el("div", { class: "f-rsection f-revidence" }, [group("evidence", head, proof, false, function (ul) {
evidence.forEach(function (g) { ul.appendChild(el("li", { class: "f-rsec" }, [genreGroup(g, "f-rname-btn f-rsec-btn")])); });
}, el("span", { class: "f-rsum", text: evidence.map(function (g) { // in the evidence order, as the groups inside
return genres[g].length + " " + (genres[g].length === 1 ? g : plural(g).toLowerCase());
}).join(", ") }))]));
}

var used = Object.keys(marked).sort(function (a, b) { return genreRank(a) - genreRank(b) || a.localeCompare(b); });
if (used.length) box.appendChild(el("p", { class: "f-rlegend" }, used.map(function (g) {
Expand Down
Loading
Loading