Scheduling is a plugin, not a core layer
Vocabulary note. The text below is preserved as written and says
Taskthroughout. That record was renamed toEntryby ADR 0003 — read everyTaskhere asEntry. ADR 0003 exists in large part because this ADR's boundary kept being undone by this ADR's noun. Separately,ProjectPluginbelow (the edit-extension contract design from issue #15) was renamed toDatasetPluginby ADR 0004, following the sameProject→Datasetrename applied to the class it names.PluginContext.layout.registerItemEmitterbelow was later renamed toregisterItemProducer(Q16, S5.9). Read everyregisterItemEmitterhere asregisterItemProducer.
Superseded in one clause (2026-09-04, S5.10, D-S5-23). "A plugin may occupy it exclusively" below is now read as arity, never ownership: the hook has one occupant at a time, and a scheduling plugin is one candidate occupant with no special claim on it. Installing composes —
ctx.edits.setExtender((next) => …)hands a plugin the current occupant, so a second plugin adds to the first's writes instead of evicting them (plans/s5-extensibility-and-editing/s5.10-dataset-plugins.md, closingplans/s2-data-core/OPEN-QUESTIONS.mdOQ8). What exclusivity protected is intact:data/still holds one field, calls it at one site, once per transaction. Everything else in this ADR stands.
D3/D4 (plans/00-overview.md) treated scheduling/ as one of the five fixed, universal, DOM-free core layers, with only its SchedulingPolicy pluggable. In practice this had already leaked further than the docs admitted: Task carried a required scheduling: 'auto' | 'pinned' field, Dependency/DependencyType/DependencyId were defined in model/ (the zero-dependency, supposedly domain-neutral layer), and the layer diagram had a structural data/ --> scheduling/ edge backing the "every transaction runs one scheduling pass, no exceptions" rule. This directly contradicted the project's own stated goal (00-overview.md): "no assumptions about the user's planning methodology... everything opinionated is a pluggable policy or extension, never a baked-in rule." A host building a pure resource/manpower view — no task-to-task dependencies at all — paid for and inherited Gantt-specific scheduling machinery it never used, and every future feature built against Task/data/ would have inherited the same assumption by default.
We pulled scheduling — the propagation engine, its policy, the Dependency entity, and the per-task pin flag — out of the mandatory core layers and into the plugin boundary. Core exposes one generic (not "scheduling"-named) synchronous extension hook that may add extra field writes to a proposed edit; a plugin may occupy it exclusively, and when nothing does, it adds nothing, decided once at setup rather than branched per call. Dependency and the pin flag move into scheduling-plugin-owned storage, not Task/model/. FreeGantt still ships an official bars+dependencies scheduler as a first-party default plugin — this is about where the boundary sits, not about de-prioritizing scheduling.
Considered options
- Keep scheduling as a fixed core layer, pluggable policy only (status quo, D3/D4 as originally written). Rejected: "fixed, universal engine" already privileges precedence-graph propagation as the methodology, which is exactly the assumption the project claims not to make, and doesn't explain why
Taskneeds a scheduling-specific field regardless of whether scheduling is even used. - Move
scheduling/bodily intoextensions/. Rejected:extensions/is DOM-touching; scheduling's purity is load-bearing (speculativeschedule()calls, once per animation frame during drag preview, must stay synchronous and side-effect-free, and the engine needs to keep running headless in Node and, later, in a worker). Relocating the directory would have broken that for no benefit — the fix needed is architectural (who's allowed to assume scheduling exists), not geographic (which folder the code sits in). - A second, bespoke "pure extension" contract, separate from
GanttPlugin. Initially proposed, then retracted: the kind-seam precedent already in the spec (PluginContext.layout.registerItemEmitter— a pure, DOM-free function registered once through the DOM-side plugin host, then invoked repeatedly by pure code) is the exact shape scheduling needs. One plugin system, generalized, beats two.
Consequences
Task's public shape lost theschedulingfield;Dependency/DependencyType/DependencyIdleftmodel/. This touched D3, D4, thedata/ --> scheduling/edge in theplans/01layer diagram, and S3's scope — all rewritten, not just the code (tracked and closed via #13).- Confirmed by grep before this ADR: nothing outside
model/referencedDependency,DependencyId, orTask.scheduling—data/andscheduling/were still 3-line stubs (pre-S2/S3). The removal cost nothing at the time it landed. That window would have closed once transactions (S2) or the propagation engine (S3) were built on top of the old shape, so the cleanup was time-sensitive, not just correct-in-principle. - The generic extension hook's exact contract was deliberately not settled by this ADR: where per-plugin per-task data lives (a reserved store, not
Task.meta, to avoid host/plugin collisions in the same field), and how the hot-path preview call and the commit-time call share one resolution so a user never sees an edit land in the "wrong" spot before a plugin corrects it moments later — both needed dedicated plugin-system design work. This ADR fixed the boundary (scheduling is not privileged core); the contract shape was tracked separately and landed as issue #15 (ProjectPlugin/EditExtender/reservedPluginStore— the design work opened by #12) and issue #16 (the re-scoped bars+dependencies default plugin spec, built on #15's contract — the design work opened by #14). #16 also surfaced one small follow-up against this ADR's own boundary —GeometryFrame.links(plans/01§4) still needs a plugin-contributed link-emission seam, since itsidfield can no longer be amodel/-ownedDependencyIdbrand. - #16 is closed, superseded by #111 and #136 (2026-09-02). The S5.0 grill on issue #111 split #16's single bars+dependencies plugin into two:
entryDependencies()(theDependencystore, the link-create gesture, link rendering) andscheduling()(propagation, pins, policy, diagnostics), one-way —scheduling()requiresentryDependencies(), never the reverse (plans/s5-extensibility-and-editing/s5.0-grill-issue-111.md,s5.10-dataset-plugins.mdD-S5-30/D-S5-31). #16'sGeometryFrame.linksfollow-up carries forward at #136, now owned byentryDependencies(). #16's reusable design (theDependencyshape, its removal cascade, pin storage) also carries forward at #136, reassigned across the two plugins.ProjectPlugin/setResolver/declareStoreare pre-ADR-0004 names forDatasetPlugin/setExtender/ctx.store.reserve().