Maintaining these pages
Instructions for an agent, or a person, who keeps this folder true as src/ changes. Read this
before you edit any page here.
Edit the page in docs/architecture/. The Docusaurus site serves this folder directly, so there is
no copy to publish and no second page to hold in step.
What these pages are, and are not
- Are: a map of what the code currently does — files, classes, call order, and data flow, each claim traceable to a line someone read.
- Are not: the spec. The spec states what the code should do,
CONTEXT.mdis the glossary, anddocs/adr/holds the decisions. Never restate a rule from those here as if this folder were its home — state what the code does and move on. If a page and the spec disagree, the spec is right and the page is stale. - Are not the consumer API. An app author reads
README.mdanddocs/05-consumer-api.md. The Overview page carries a short usage summary and links out; nothing deeper than that belongs here. - Are not a place for aspirations. A section describing a class that does not exist yet is worse than no section.
- Are not a tracker. A defect, a design doubt, or work someone means to do next goes in the issue tracker, where it gets triaged and closed. Findings written here go stale silently, and a stale finding costs the next reader more than it ever saved.
What to update when a file changes
Each page states, near its top, which files it was derived from. If you change a file, update every page that lists it.
grep -rln 'gantt-shell' docs/architecture/*.md # which pages cover this file?
| If you change… | Update |
|---|---|
any new or deleted file in src/ | File inventory, and Layers's layer map if the layer changed shape |
enough of src/ that a layer's size moved | Module map diagrams — three diagrams state a per-directory N lines · M files, and nothing guards them. Re-measure with find src/<dir> -name '*.ts' ! -name '*.test.ts', and set all three to the same number |
.dependency-cruiser.cjs or eslint/rules/ | Layers — the import table and the lint-rule note |
view/gantt-shell.ts constructor | Lifecycle (the construction diagram and the "why the order" table), and Class map |
view/gantt-shell.ts render() | Lifecycle (the render diagram) — and re-measure the pass count (see the recipe below) |
layout/frame.ts, layout/rows/*, layout/bars/* | Lifecycle (the pipeline list, the GeometryFrame box, the coordinate rules), and Class map |
layout/viewport/* | Lifecycle (the viewport diagram, the four-layer table, the batch nesting), and Class map |
data/* | Layers (the import table), File inventory, and Class map if the reader, the plugin store, or the transaction flow changed |
data/transaction.ts, data/error-reporting.ts, data/event-bus.ts | Refusals — the Dataset door, the step table, and the report fields |
view/gesture-pipeline.ts, view/live-region.ts, view/capability.ts | Refusals if a gesture veto or the live region changed |
time/* | Class map (the time diagram). If temporal-polyfill is upgraded, re-test the diffDays workaround and delete that callout if it is no longer needed |
model/* | Class map — the type table |
time/input.ts or data/entry-reader.ts | Class map's consumer boundary and its type table |
render/dom/* | Class map (the DOM tree, the backend card), and Timeline render if the header or ticks moved |
api/* or extensions/* | Class map, Layers if the public surface's allowed imports moved, File inventory, Refusals if the error helper or a plugin report changed, and Overview if the consumer call site changed |
harness/main.ts or a harness page | Class map's harness reading — reviewed every commit whether or not it changed — and Overview's demo table |
api/plugin.ts, api/dataset-plugin.ts, view/plugin-ports.ts, layout/bars/produce-bars.ts | Plugin lifecycle and the plugin authoring guide |
| a page added to or removed from this folder | nothing — the site rebuilds from this folder on every push to main, and the sidebar needs nothing, because this section's entry is { type: 'autogenerated' } (the site's sidebars.ts) |
Rules for the content
-
Read the file, do not infer it. Every claim should be traceable to a line you actually read in this session. Docstrings in
src/are unusually good and are a legitimate source — but they state intent, and these pages state behaviour. Where the two differ, say so, and take the difference to the tracker. -
Measure the claims that are measurable. Construction used to cost two
computeFramecalls; a later re-measure got 1. If you assert a count, a complexity, or a firing order, prove it and write the number down. If you cannot prove it, write "appears to" and say why you could not. -
Delete what the code no longer does. A sentence that stopped being true is not history, it is a wrong answer with a confident tone. Remove it in the same commit that makes it wrong.
-
Vocabulary is
CONTEXT.md's. Entry, not Task. Dataset, not its earlier name. Bar, not Item — Item is retired, so read every older "Item" as "Bar". A row that draws more than one Bar draws Segments, and a Segment is a reading of a Bar, never a type. Row, not line. Field is what a value is; a grid column is where a Gantt shows it. No scheduling framing in core descriptions — a shift roster and units-sold-per-week are as much the intended use as a construction plan. -
Four guards scan this folder.
test/guards/retired-words.test.tsfails on a retired word — that file holds the current list and the ADR behind each one, and it is the only place the list lives. It readsdocs/**, with the ADRs out of scope. It runs inpnpm verify, so prose here breaks CI exactly like code does. Add a word to that guard in the same commit that deletes the type it named. A name that outlives its type keeps teaching a concept the library dropped, and four pages here taught one for months because no guard read them.test/guards/spec-labels.test.tsfails when a new line cites a slice decision id, a review finding id, or a question id. State the rule instead.plans/and the ADRs keep those ids. The check reads lines added sincemain, so an old citation that this page still holds does not fail until that line changes.The other two read what the pages measure rather than what they say.
test/guards/file-inventory.test.tsfails when File inventory misses a livesrc/file, keeps a row for a deleted one, or credits a file with an export it does not contain — that page claims to list every non-test file, and the claim was false for 24 of them.test/guards/diagram-text-fits.test.tsfails when a diagram label draws past its own box. Text overflows silently: nothing marks it, and the page still builds. Both are pure Node, so they cost nothing to run. -
Match the existing structure. Prefer extending a diagram to adding a new one. When a page grows past roughly a thousand lines, split it into another page rather than adding a section nobody scrolls to.
-
State the rule; cite no internal id. A page here names what the code does. It does not cite a slice, a decision id, an issue number or a plan section as the record — a reader of this site has no way to look one up, and doesn't need to.
Mechanics of the pages
Derived from docusaurus.config.ts, sidebars.ts and src/css/architecture-doc.css in
freegantt/docs.
-
The Docusaurus site shell lives in the freegantt/docs repository. It reads this repository's
docs/andsrc/at build time, so a page here is the only copy. Run it against your checkout withpnpm docs(dev server) orpnpm docs:build(static build, includes the TypeDoc-generated API reference). -
A page says when it was last made true. Set
last_update.datein the front matter to the day you change it, inYYYY-MM-DD:---id: lifecycletitle: "Construction, render, notification"last_update:date: 2026-09-14---showLastUpdateTimeis on (the site'sdocusaurus.config.ts), so the date prints at the foot of the page. A page with no such date falls back to its last commit date, which moves for a typo fix as readily as for a re-derivation. State the date by hand, and state it only when you checked the page against the code. -
Navigation is generated, not typed. The sidebar for this section is
{ type: 'autogenerated', dirName: 'architecture' }in the site'ssidebars.ts— a page added to or removed from this folder needs no other list edited, and Docusaurus renders previous/next pagination from the same sidebar order. -
Prose is plain Markdown. Headings, tables, code fences, and
:::note/:::warningadmonitions all render with Docusaurus's own theme. Docusaurus builds the in-page table of contents from the page's headings — there is no hand-written section nav to maintain. -
Diagrams are the one exception, and stay hand-authored inline SVG. A diagram figure is wrapped in
<div class="fg-architecture-doc">…</div>, which scopes the site'ssrc/css/architecture-doc.cssdiagram classes (.d,.bx,.bx.pure/.dom/.api/.warn/.hot,.edge,.edge.ret,.edge.hot,.life,.t,.s,.xs) to that figure alone — nothing outside the wrapper is affected. Colours come from CSS custom properties, so both themes work with no second definition. Never hardcode a hex value in an SVG — exceptrefusals.md, whose sequence diagram keeps the diagram-skill tokens on a light paper frame in both themes. -
Each SVG defines its own arrowhead marker with a unique id (
a3,a4,a5,a6,a7,a8) — marker ids are document-global, so a new diagram needs a new one, unique within its page. The marker usesfill="currentColor"and each edge setsstyle="color: …", which is how one marker serves grey, amber and red arrows. -
Every diagram sits in a
.scrollerso a wide SVG scrolls in its own box and the page body never scrolls sideways. KeepviewBoxwidths at 960. -
diagram.mdcarries its own scoped<style>block for a palette none of the other pages need; check both themes before committing a change to it. -
Root Prettier does not reach this folder. The repo root's
.prettierignorelists the bare namedocs, which matches any directory nameddocsanywhere in the tree — sopnpm formatandpnpm format:check, run from the repo root, both skip every page here. Formatting is by hand: match the file you are editing. -
A diagram's raw-HTML block tolerates no blank line inside it. From the opening
<div class="fg-architecture-doc">to its matching</div>, CommonMark treats the whole span as one raw-HTML block, and a blank line anywhere inside — even between</defs>and an SVG's first<rect>— ends that block early. Everything after gets re-parsed as fresh Markdown, and shapes that should nest inside<svg>come out as siblings next to it and never render. Write a diagram's markup with no blank line from wrapper open to wrapper close. This rule applies only inside a diagram's own wrapper — the Markdown prose around it takes blank lines normally.
Recipe: re-measuring the construction pass count
Construction costs one computeFrame call, and a later no-op render() costs one. To re-verify
after touching render(), Viewport or ScrollAxis, spy FrameLayout.prototype.computeFrame
around new GanttShell(…) (use the same dataset fixture as src/view/gantt-shell.test.ts — a
store view, not a plain array). Read the numbers from a throwaway file under test/dom/, then
delete it.
npx vitest run --project dom test/dom/tmp-render-count.test.ts
rm test/dom/tmp-render-count.test.ts
If the construction count is greater than 1, the notify path is rendering inside the first pass again: that is a bug, and it belongs in the tracker rather than on a page here.
Before you commit
- Every "derived from" line lists files that still exist.
- Every file path named in prose still exists (
git ls-files src). - Every cross-page link resolves.
- The page renders in both light and dark, and no diagram makes the body scroll sideways.
- A diagram you moved still connects: widen a column and every arrow into it moves too, and free
text below it can fall off the
viewBox.pnpm guardsmeasures the labels, not the arrows, so look at the diagram. pnpm docs:buildandpnpm guardspass.