Skip to main content

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.md is the glossary, and docs/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.md and docs/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 movedModule 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 constructorLifecycle (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.tsRefusals — the Dataset door, the step table, and the report fields
view/gesture-pipeline.ts, view/live-region.ts, view/capability.tsRefusals 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.tsClass 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 pageClass 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.tsPlugin lifecycle and the plugin authoring guide
a page added to or removed from this foldernothing — 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​

  1. 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.

  2. Measure the claims that are measurable. Construction used to cost two computeFrame calls; 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.

  3. 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.

  4. 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.

  5. Four guards scan this folder. test/guards/retired-words.test.ts fails 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 reads docs/**, with the ADRs out of scope. It runs in pnpm 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.ts fails 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 since main, 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.ts fails when File inventory misses a live src/ 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.ts fails 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.

  6. 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.

  7. 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/ and src/ at build time, so a page here is the only copy. Run it against your checkout with pnpm docs (dev server) or pnpm docs:build (static build, includes the TypeDoc-generated API reference).

  • A page says when it was last made true. Set last_update.date in the front matter to the day you change it, in YYYY-MM-DD:

    ---
    id: lifecycle
    title: "Construction, render, notification"
    last_update:
    date: 2026-09-14
    ---

    showLastUpdateTime is on (the site's docusaurus.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's sidebars.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/:::warning admonitions 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's src/css/architecture-doc.css diagram 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 — except refusals.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 uses fill="currentColor" and each edge sets style="color: …", which is how one marker serves grey, amber and red arrows.

  • Every diagram sits in a .scroller so a wide SVG scrolls in its own box and the page body never scrolls sideways. Keep viewBox widths at 960.

  • diagram.md carries 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 .prettierignore lists the bare name docs, which matches any directory named docs anywhere in the tree — so pnpm format and pnpm 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 guards measures the labels, not the arrows, so look at the diagram.
  • pnpm docs:build and pnpm guards pass.