Skip to main content

The decision records

One file per decision, numbered in the order the decision was taken. A record says why, and it names the evidence. A spec (plans/00–04) says what is true now. When the two disagree, src/ is the answer.

An accepted record is superseded, never rewritten. A later record states the change, and the earlier body stays as it was written.

Index​

Every record, in one table. Status is the only index of a supersession or an amendment — a reader who wants to know whether an ADR still holds reads this column, not each file's own frontmatter.

#TitleStatusDecides
0001temporal-polyfill for zone-aware date arithmeticacceptedtime/zone.ts wraps temporal-polyfill, so DST fold/gap is a chosen, tested policy
0002Scheduling is a plugin, not a core layeracceptedScheduling logic sits behind a plugin seam, out of the mandatory core
0003The authored record is an Entry, not a TaskacceptedThe core record is named Entry, with no task-management assumption baked in
0004The authored body of data is a Dataset, not a ProjectacceptedThe wrapping class is named Dataset, with no project-management assumption baked in
0005Fields are declared and grid columns reference themsuperseded by 0011(historical) an earlier Field design, with meta where props now sits
0006The change record is a ChangeSet, not a Transaction or a Commitaccepteddataset.on('change') carries a ChangeSet, the one delta type
0007Friend-only methods move off the class, into a WeakMap-backed functionacceptedbind/unbind are free functions closing over a private WeakMap, never public class methods
0008Progress is scheduling-plugin data, not a core Fieldamended by 0016progress is not on Entry and not in the Field registry; its Document-era wording no longer applies
0009Core raises an Error report; the consumer retains itacceptedA refusal is a typed report on an event, not a throw the consumer must catch to keep
0010The Selection holds Segments, not Entriessuperseded by 0025(historical) the Selection once held SegmentId
0011Consumer values live in propsacceptedA consumer's own values live under entry.props; the Field key is the whole address
0012Dates are optional on every kindsuperseded by 0027An Entry spans iff both start and end are present; one date without the other is legal
0013What decides that a row derives its valuesamended by 0022Structure (has children, or not) decides derivation and the default look — no stored classification
—(0014 — withdrawn before build; the number is not reused, see below)——
0015What the write door refusesamended by 0033, 0035, 0038editable governs entries.update() and the grid through one rule, one refusal
0016The library holds no save formatacceptedNo toJSON/fromJSON/Document; persistence is the consumer's own job
0017The Entry answers questions about itselfsuperseded by 0024Entry/StoredEntry are two types; the read seam reads entry.read(key)
0018A variant is a rule, not an id listacceptedA Variant is a when rule; nothing stores which rows wear it
0019One plugin, one install siteamended by 0031, 0037A chrome-only plugin installs on Gantt; a plugin with a data half installs on Dataset
0020A plugin may own the hierarchyamended by 0031A plugin states an Entry's parent through one hierarchy-source seam; the Rollup follows
0021The consumer's stylesheet winsacceptedThe base stylesheet ships in one cascade layer, so an unlayered consumer rule always wins
0022Core ships variants and a variant answers about itselfamended by 0023Core exports bar()/summary()/diamond(); gantt.variantFor(entry) is the one resolve door
0023A variant with no items follows the dataacceptedA variant with no bars key draws the structural default
0024parentId answers the stored value on every dooracceptedentry.read('parentId') is always the stored value; hierarchyParentId answers the tree question
0025The Selection holds Entries, not SegmentsacceptedThe Selection holds EntryId again, reversing 0010
0026The Segment retiresacceptedSegment is deleted as a type; a former Segment is an ordinary child Entry
0027A spanning Entry draws a BaracceptedA spanning Entry draws one Bar; a segmented parent draws no Bar of its own
0028A plugin's impossible proposal is a refusalacceptedAn extender proposing an impossible value is refused at warning with by naming it; an extender that throws is a 'error' fault
0029The app pushes the theme; the library never asksacceptedtheme is three literals; the app writes gantt.theme or pins data-fg-theme, and the library never calls back or watches attributes it does not own
0030A multi-hour tick counts real time on a DST dayacceptedOn a DST day a tick with increment > 1 keeps equal real spacing from its day start (00/07/13/19), not wall-clock multiples
0031A plugin declares its shape; its code runs on a finished objectamended by 0032A plugin declares fields/fieldTypes/aggregators/hierarchySource on its definition; data(ctx) runs only after the Dataset is built; construction checks the batch like load
0032A Gantt plugin's code runs before the first frameacceptedA finished Gantt is built and configured before any plugin's view(ctx) runs; Gantt assigns #shell, installs plugins, then paints frame 1; #constructed flips last so a view()-time Gantt write stays silent
0033An 'api' Field takes no gestureacceptedcanWrite refuses 'api'/'never'/a derived cell before the consumer or the variant rule; neither can widen it, only a per-entry lock rule can
0034Sibling order is a Fieldamended by 0035, 0038siblingIndex is an ordinary core Field; a write that moves an entry renumbers its sibling group once, at commit, so one move is one undo step, and entries.all reads depth-first tree order by it
0035Sync writes like load, and undo stays localamended by 0036entries.syncAll() and entries.syncChanges() write through load's door, not entries.update()'s: a 'never' lock does not refuse either, a derived parent cell re-rolls, no EditExtender runs, and unlike load neither records an undo step nor erases Redo. Undo and redo write onto the store's current value, so an undo can overwrite a value a sync wrote since, and redo gives the server's value back
0036An undo keeps a foreign writeacceptedA replay keeps a Field value a foreign write changed since the step recorded it, and drops every Field row of that entry; replay(cs, { overwriteForeignWrites: true }) restores last write wins; history: false builds no History so an app can own undo
0037A plugin's type argument names its own keysacceptedA plugin's TProps names the keys it reads and writes, not the consumer's; DatasetOptions.plugins/GanttOptions.plugins hold each plugin with its props erased, and fields is checked against the plugin's own type argument
0038An Entry lock is core, and it uses the plugin seamsacceptedlocked is a core Field; a core lock, remove and bar move rule, built on the ctx.edits seams and installed before every plugin, close every gesture on a locked Entry's own cells, bar and delete, in both panes; its children stay free, and app code, a cascade, sync and undo still write past it
0039A remove rule refuses a user deleteacceptedA new seam, ctx.edits.setRemoveRule, answers whether entries.remove() may take an Entry, asked once per member of its subtree with the narrowest answer winning; the core lock installs lockedEntryRemoveRule last, so a user Delete on a locked row, or on its locked descendant, refuses while entries.remove(), load, sync and undo still write past it

The gap at 0014​

There is no ADR 0014. The number is not reused.

0014 — the plugin-author surface was split out of ADR 0011 on 2026-09-09. It stayed a draft. The author withdrew it on 2026-09-11, before its build started, and no line of it ever reached src/. The file was deleted on 2026-09-11 so that no reader can mistake a withdrawn draft for the design.

Why it was withdrawn, and why that was right. The draft renamed entries.fieldValue to entries.read and made a plugin key prefix a rule the registry checks. Neither earned its price:

  • The prefix. A prefix is a convention a plugin follows — scheduling:progress (ADR 0008) — not a rule core enforces. Enforcement bought a reserved-name list and two write-door refusals, and it protected nothing that a declaration collision does not already catch.
  • The rename. ADR 0016 deleted the save format, which removed the draft's own stated price for deciding late. The rename then had no deadline, and a better door arrived: ADR 0017 puts the read on the row itself, as entry.read(key). One door on the object that holds the value beats a second by-key door beside fieldValue.

What survived the withdrawal, and where it lives now. Three things the draft held were real, so they were rehomed rather than dropped:

WhatWhere it lives now
entry.read(key) is the one Field read, and #274's duration door closes with itADR 0017
A plugin that installs over values it did not writeADR 0019
An undeclared key is never written by the libraryADR 0011, and CONTEXT.md's props entry

The full close-out is in plans/field-redesign/CLOSE-OUT.md. Working material under plans/field-redesign/ still names ADR 0014, because it records what was thought at the time. Read it as history.