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.
| # | Title | Status | Decides |
|---|---|---|---|
| 0001 | temporal-polyfill for zone-aware date arithmetic | accepted | time/zone.ts wraps temporal-polyfill, so DST fold/gap is a chosen, tested policy |
| 0002 | Scheduling is a plugin, not a core layer | accepted | Scheduling logic sits behind a plugin seam, out of the mandatory core |
| 0003 | The authored record is an Entry, not a Task | accepted | The core record is named Entry, with no task-management assumption baked in |
| 0004 | The authored body of data is a Dataset, not a Project | accepted | The wrapping class is named Dataset, with no project-management assumption baked in |
| 0005 | Fields are declared and grid columns reference them | superseded by 0011 | (historical) an earlier Field design, with meta where props now sits |
| 0006 | The change record is a ChangeSet, not a Transaction or a Commit | accepted | dataset.on('change') carries a ChangeSet, the one delta type |
| 0007 | Friend-only methods move off the class, into a WeakMap-backed function | accepted | bind/unbind are free functions closing over a private WeakMap, never public class methods |
| 0008 | Progress is scheduling-plugin data, not a core Field | amended by 0016 | progress is not on Entry and not in the Field registry; its Document-era wording no longer applies |
| 0009 | Core raises an Error report; the consumer retains it | accepted | A refusal is a typed report on an event, not a throw the consumer must catch to keep |
| 0010 | The Selection holds Segments, not Entries | superseded by 0025 | (historical) the Selection once held SegmentId |
| 0011 | Consumer values live in props | accepted | A consumer's own values live under entry.props; the Field key is the whole address |
| 0012 | Dates are optional on every kind | superseded by 0027 | An Entry spans iff both start and end are present; one date without the other is legal |
| 0013 | What decides that a row derives its values | amended by 0022 | Structure (has children, or not) decides derivation and the default look — no stored classification |
| — | (0014 — withdrawn before build; the number is not reused, see below) | — | — |
| 0015 | What the write door refuses | amended by 0033, 0035, 0038 | editable governs entries.update() and the grid through one rule, one refusal |
| 0016 | The library holds no save format | accepted | No toJSON/fromJSON/Document; persistence is the consumer's own job |
| 0017 | The Entry answers questions about itself | superseded by 0024 | Entry/StoredEntry are two types; the read seam reads entry.read(key) |
| 0018 | A variant is a rule, not an id list | accepted | A Variant is a when rule; nothing stores which rows wear it |
| 0019 | One plugin, one install site | amended by 0031, 0037 | A chrome-only plugin installs on Gantt; a plugin with a data half installs on Dataset |
| 0020 | A plugin may own the hierarchy | amended by 0031 | A plugin states an Entry's parent through one hierarchy-source seam; the Rollup follows |
| 0021 | The consumer's stylesheet wins | accepted | The base stylesheet ships in one cascade layer, so an unlayered consumer rule always wins |
| 0022 | Core ships variants and a variant answers about itself | amended by 0023 | Core exports bar()/summary()/diamond(); gantt.variantFor(entry) is the one resolve door |
| 0023 | A variant with no items follows the data | accepted | A variant with no bars key draws the structural default |
| 0024 | parentId answers the stored value on every door | accepted | entry.read('parentId') is always the stored value; hierarchyParentId answers the tree question |
| 0025 | The Selection holds Entries, not Segments | accepted | The Selection holds EntryId again, reversing 0010 |
| 0026 | The Segment retires | accepted | Segment is deleted as a type; a former Segment is an ordinary child Entry |
| 0027 | A spanning Entry draws a Bar | accepted | A spanning Entry draws one Bar; a segmented parent draws no Bar of its own |
| 0028 | A plugin's impossible proposal is a refusal | accepted | An extender proposing an impossible value is refused at warning with by naming it; an extender that throws is a 'error' fault |
| 0029 | The app pushes the theme; the library never asks | accepted | theme 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 |
| 0030 | A multi-hour tick counts real time on a DST day | accepted | On a DST day a tick with increment > 1 keeps equal real spacing from its day start (00/07/13/19), not wall-clock multiples |
| 0031 | A plugin declares its shape; its code runs on a finished object | amended by 0032 | A plugin declares fields/fieldTypes/aggregators/hierarchySource on its definition; data(ctx) runs only after the Dataset is built; construction checks the batch like load |
| 0032 | A Gantt plugin's code runs before the first frame | accepted | A 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 |
| 0033 | An 'api' Field takes no gesture | accepted | canWrite refuses 'api'/'never'/a derived cell before the consumer or the variant rule; neither can widen it, only a per-entry lock rule can |
| 0034 | Sibling order is a Field | amended by 0035, 0038 | siblingIndex 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 |
| 0035 | Sync writes like load, and undo stays local | amended by 0036 | entries.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 |
| 0036 | An undo keeps a foreign write | accepted | A 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 |
| 0037 | A plugin's type argument names its own keys | accepted | A 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 |
| 0038 | An Entry lock is core, and it uses the plugin seams | accepted | locked 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 |
| 0039 | A remove rule refuses a user delete | accepted | A 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 besidefieldValue.
What survived the withdrawal, and where it lives now. Three things the draft held were real, so they were rehomed rather than dropped:
| What | Where it lives now |
|---|---|
entry.read(key) is the one Field read, and #274's duration door closes with it | ADR 0017 |
| A plugin that installs over values it did not write | ADR 0019 |
| An undeclared key is never written by the library | ADR 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.