Adom Gantt
Public Made by Adomby adom
Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on working days, skipping weekends and US federal holidays, push
6600ce8
adom-gantt 0.58 to 0.60: a Setup tab, and tags on sections and rows
Date: 2026-09-29. Requested by Drew. A guide, not a contract: if an assumption below is wrong, do the smarter thing and record it under Build notes.
Built in three checkpoints. Each one is shippable on its own, is published to the wiki when its tests are green, and is reviewed while the next one is being built.
Working copy and tools
- Source: /home/adom/project/adom-gantt (files-only clone, 0.57.1). The app is
gantt.html, served byserver.js. - Pristine 0.57.1 for diffing: /home/adom/project/adom-gantt-0.57.1-ref/
- Real plan: /path/to/launch-plan.json (20 sections named like "Launch · Program and gates", "Tech · Laser workcells", "Process · Fab", "Ops · factoryOS"). Never serve that path directly, the server writes sidecars beside its data file: copy it first.
- Run:
node server.js --data <copy>.json --port <8950-8959> --no-auth. Port 8901 is a live preview, leave it alone. - Tests:
node test/ui_test.mjsis the browser suite (12 minutes for a full run).--only <group>runs a subset; group names are matched bywantAny(...)in the file (editor, live, sections, coverage, header sweep, viewport, text size, ...). Read ~/.claude/skills/app-testing/SKILL.md. The suite has a control-coverage manifest: every new control must be declared, and everysafecontrol is clicked.
Checkpoint 1 (0.58.0): the Setup tab owns section management
- A fourth tab in the lens switch, after Analytics, named Setup. It is a page like Table and Analytics are, not a modal. The legend and the chart controls that mean nothing there are hidden, the way Analytics hides them.
- Everything the header's Sections button does today moves onto that page: add a section, rename it and set its aside, recolour, reorder, remove (with its rows re-homed the way it works today), all undoable and all in the changelog exactly as now. On a page there is room to show more than the modal did: each section's row count and date range are worth showing.
- The Sections button leaves the header. Nothing else in the header moves.
The measured header fit (
fitHeader) must still fit at every width in all FOUR lenses. prefs.viewandproject.view.modeaccept the new lens. A refresh on Setup comes back to Setup.- An exported HTML snapshot has the tab too and it works there with no server, like every other edit does in a snapshot.
- Tests: the
sectionsflows drive the page instead of the modal; the coverage manifest declares the new controls; the header sweep covers the new lens. The suite stays green.
Checkpoint 2 (0.59.0): tags on sections
- Data:
phases[].tags, an array of short strings. Trimmed, de-duplicated without regard to case (first spelling wins), a sensible cap on length and count. Absent means none. Tags carry no colour of their own: colour on this chart means the section, and only that. - Persistence end to end in
server.js: the sidecar,/export/json,/import/json, the HTML snapshot, and the changelog (a tag edit is an event with a readable line in?format=md). Old data files and old sidecars load unchanged. - Setup page: edit a section's tags in place (type, Enter, remove with a click; suggest tags already in the plan). A Tags block lists every tag in the plan with how many sections (later, rows) carry it, and can rename a tag everywhere or remove it everywhere. Undo and redo cover all of it, one gesture to one Ctrl+Z.
- One click to make tags from name prefixes: for sections named "Launch · Program and gates" it offers the tag "Launch". It adds tags and never renames a section. Only offered when the plan has such names.
- Seeing them: the section header row in the chart shows its tags as small quiet chips, and the Table's Phase column can show them on hover or inline, whichever reads better.
- Filtering: the legend row gains tag chips. Choosing one or more tags shows only the sections carrying any of them, in the chart and the table alike. It composes with the per-section chips and with Collapse all. It is view state, per person, like the section filter is. With 20 sections and 4 tags the legend must stay tidy: the tags are what make a 20-chip legend manageable, so consider letting a tag chip stand in front of its sections.
- Tests for every item above, asserting effects against
/export/json.
Checkpoint 3 (0.60.0): tags on tasks and milestones (optional per row)
- Data:
items[].tags, same rules. A row's EFFECTIVE tags are its own plus its section's; the tag filter uses effective tags, so a task tagged "long-lead" in an untagged section is found by that tag. - Editing: a Tags field in the full item editor and in the New panel, a Tags column in the Table (editable in place, sortable, filterable), and the batch panel can add or remove a tag across the selection.
/export/csvand/import/csvcarry a Tags column. Copy and paste carry tags.- Analytics: a per-tag card (items, hours, share), counting effective tags.
- The Tags block on Setup counts rows as well as sections.
- Tests for every item above.
Housekeeping, every checkpoint
- Bump the version in
package.jsonandpage.json. - Update
SKILL.mdandREADME.md(the schema tables, the features list, the lens descriptions). No em dashes in prose. Match the surrounding voice. - Add an entry at the TOP of
docs/BUILD-NOTES.md(it is newest first and about 400KB: read only its head). - Do not publish, do not touch /home/adom/project/adom_modules or ~/.claude/skills, leave no processes running. The orchestrator publishes.
Build notes
Build notes - 2026-09-29, checkpoint 1
Built as planned, with these changes to the plan's assumptions:
project.view.modedid not accept Analytics either.viewDefaults()read onlyganttandtable, so "accept the new lens" meant fixing the list, not adding one value. It now reads a singleVIEW_LIST(the same listVIEWSis), so any lens, includingsetupandanalytics, can be authored as the opening lens.- The modal's class names and row markup were kept (
.sec-row,.sec-btn[data-a],.sec-sw,#secNew,#secAdd). The operations were already correct and tested; moving the frame rather than rewriting the rows kept the flows transferable and gives checkpoint 2 one obvious place to add a tag editor per row (a new element inrenderSections()), with the Tags block as a second.an-cardin.su-stack. - One row editor at a time, which the modal did not enforce. On a page with 20 rows, two half-open forms is a real state; opening one closes the other. The swatch toggles.
- Undo repaints the list. The modal never did (Ctrl+Z with it open left a stale
list). The page re-renders from
refreshSections()andscheduleSave, skipping while a row editor is open. Checkpoint 2's tag edits should end inrefreshSections()too. - The date range is read from the rows, not from
phases[].start/end, matching the Analytics per-section card and what the chart draws. - Coverage now opens the three row editors as states and declares their controls
(
.sec-nm,.sec-ld,.sec-ok,.sec-chip,.sec-hex,.sec-dest,.sec-go,.sec-no); inside the modal the gate never reached them.#secDoneis gone. - Observed while building: another process was editing
server.js,README.md,SKILL.mdandtest/server_test.mjsin this same working copy (server-side tags). Checkpoint 1 did not touchserver.js; its README and SKILL edits were made as narrow in-place replacements so as not to overwrite that work.
Build notes - 2026-09-30, checkpoint 1 review fixes
- Section order and row assignment after a reload (V-A) was wrong in both the page and
the server's merge, since 0.57.1: the row overrides were checked against the data
file's section list before the sidecar's longer list replaced it. Fixed in both
(
server.jswith the coordinator's go-ahead), with a server_test case. - An empty section range (V-B) now means "no range of its own" everywhere the page reads one: the axis scan and the start-date shift. The server already skipped it.
- Sections now have a client-only identity,
phaseKey(_k), used by the reorder undo and by every Setup row editor instead of a name or an index. Checkpoint 2's tag editor per section should resolve its section the same way (phaseIndexOfKey) and should end inrefreshSections()so an open editor is kept across repaints. - Section names must be unique (trimmed, case-insensitive). The checkpoint 2 tag rename should expect that a section name identifies one section.
- One gesture, one undo, via
withOneUndo: checkpoint 2's "rename a tag everywhere" is the obvious next user of it.
Build notes - 2026-09-30, checkpoint 2
Built as planned, with these changes to the plan's assumptions:
- "The Tags block counts sections (later, rows)" and the legend stand-in were designed
together. A tag chip in the legend stands in front of its sections literally: with
tags in the plan the section chips fold behind one "N sections" chip (open it to reach
them all, remembered as
prefs.legendSecs), and choosing tags brings back the chips of exactly the sections on screen. The plan said "consider"; without the fold, tag chips would have made a 20-chip legend a 24-chip one. - The tag filter HIDES sections; the section chips still only dim. "Shows only the sections carrying any of them" read as hide, and hiding is what makes the filter worth having on a 20-section plan. It reuses the collapsed-row class and flag, header included, so geometry needed no new code. The section chips keep their dim-in-chart, drop-from-table behaviour inside the filter.
- The section filter is NOT persisted today (
phaseVisis in memory), though the plan said the tag filter should be "like the section filter". The tag filter is persisted (prefs.tagFilter), as the brief asked; the section chips were left as they are. - Changelog lines are diffed, not posted.
phase.tagscomes out ofdiffAndLogat save time rather thanlogPhaseEvent, so undo and redo are logged too and a multi-section gesture is one POST. Itsviaissetup, not thesectionsthe other section edits use. - A new undo type,
phase.tags, instead ofphaseSnapshot, which would also have rolled back the collapse set and the legend visibility. - The prefix is the text before the first " · " (spaces both sides). Other separators (" - ", ":") were left out on purpose: "Phase 1 - Design" would have made "Phase 1" a tag.
Notes for checkpoint 3 (rows):
getState()already writesitems[].tagswhen a row has an array andadded[]carriestags;applyStatereadssv.tags. Clearing a row's tags should leaver.tags = [](not delete the key) so the sidecar says "cleared".phaseShown(i)is the ONE predicate the chart (applyCollapse) and the table (tableRows) use. Effective row tags means a per-ROW predicate; replace the per-lane checks there with onerowShown(r)(own tags plusphaseTagsOf(r)), and decide whether a section header shows when only some of its rows match (probably yes, with those rows).planTags()counts sections; the Tags card and the legend chip counts read it. Add a row count beside it rather than changing whatnmeans, since the fold and the prefix logic rely on it being per section.diffAndLogalready has the row side of the server'stagsevent to fill in (snapSemantic().byIdhas no tags yet).- The client CSV already has the
tagscolumn in the server's position.
Build notes - 2026-09-30, checkpoint 2 review fixes
- The Tags card moved ABOVE the Sections card (it is short, the list is long); the plan said "a Tags block" without a place.
- A rename carries the reader's tag filter with it, and its undo carries it back: the undo entry for a rename holds the filter as it was.
- Header chips beat the aside for room; the name comes second, the aside last.
- The changelog's tag lines are worded by direction (tagged, untagged, retagged).
- Commit on blur for the tag box. Escape discards.
- The prefix button still writes one
phase.tagsline per section; a summary line needs a new server action, left for later. - For checkpoint 3:
editTags(change, refilter)is the path for any tag gesture that must also move the filter; a row-tag rename in the Tags card should go through it too.
Build notes - 2026-09-30, checkpoint 3
Built as planned, with these changes to the plan's assumptions:
- The legend chip counts rows now, not sections. With row tags, "# long-lead 0" (no
section carries it) or "# Tech 7" (sections) no longer said how much a chip would show.
The chip's number is the rows it finds, own or inherited, the same number the Analytics
per-tag card calls items.
planTags().nstill means sections, as checkpoint 2 asked. - A section header shows when only some of its rows match (checkpoint 2 said
"probably yes"): yes, with those rows and no others.
phaseShown(i)became "the header is on screen", and the per-row check isrowShown(r). - The batch panel acts on every selected row, milestones included, and at once. Its date fields are task-only and wait for Apply; a tag means the same on a milestone and there was no reason to make an add wait on a date Apply.
- The item editor logs
via: item-editor, noteditor. Every other field that panel saves has always been logged asitem-editor;editoris the bar panel's. SKILL.md's example line was corrected rather than the via. - A row created with tags writes its own
tagsline (via: create) after itsadd, so the log says where the tag came from. A paste keeps its single summary line. - The Tags column shows the section's tags too, dashed, after the row's own. It is the one place the effective-tag rule the filter uses can be seen row by row. Only the row's own are edited there.
- Every save re-checks the plan's tag picture (a signature of
planTags()), because deletes, undos of creates and section moves change which tags exist and what a chip would show without being tag edits. - Not done: section tags are not editable from the table (they belong to Setup); the chart shows only a row's OWN tags in its label (the header carries the section's).
Build notes - 2026-09-30, checkpoint 3 review fixes
- Row tags in the chart label now take only the room the name and pills leave (
flex: 1 1 0, the gap inside their box), so the name is never cut for them. ;is not allowed inside a tag (it is the CSV separator); both normalisers turn it into a space. The CSV is documented as lossy for section and inherited tags.- The table's Tags cell saves per chip, like the Setup box, and keeps its caret in view.
- Lane header counts read "2 of 3" under a tag filter; a delete or tag edit that prunes the
filter restores it on Ctrl+Z (
viewFilteron the undo entry, kept by time travel).
# adom-gantt 0.58 to 0.60: a Setup tab, and tags on sections and rows
Date: 2026-09-29. Requested by Drew. A guide, not a contract: if an assumption
below is wrong, do the smarter thing and record it under Build notes.
Built in three checkpoints. Each one is shippable on its own, is published to
the wiki when its tests are green, and is reviewed while the next one is being
built.
## Working copy and tools
- Source: /home/adom/project/adom-gantt (files-only clone, 0.57.1). The app is
`gantt.html`, served by `server.js`.
- Pristine 0.57.1 for diffing: /home/adom/project/adom-gantt-0.57.1-ref/
- Real plan: /path/to/launch-plan.json
(20 sections named like "Launch · Program and gates", "Tech · Laser
workcells", "Process · Fab", "Ops · factoryOS"). Never serve that path
directly, the server writes sidecars beside its data file: copy it first.
- Run: `node server.js --data <copy>.json --port <8950-8959> --no-auth`.
Port 8901 is a live preview, leave it alone.
- Tests: `node test/ui_test.mjs` is the browser suite (12 minutes for a full
run). `--only <group>` runs a subset; group names are matched by
`wantAny(...)` in the file (editor, live, sections, coverage, header sweep,
viewport, text size, ...). Read ~/.claude/skills/app-testing/SKILL.md.
The suite has a control-coverage manifest: every new control must be
declared, and every `safe` control is clicked.
## Checkpoint 1 (0.58.0): the Setup tab owns section management
1. A fourth tab in the lens switch, after Analytics, named **Setup**. It is a
page like Table and Analytics are, not a modal. The legend and the chart
controls that mean nothing there are hidden, the way Analytics hides them.
2. Everything the header's **Sections** button does today moves onto that
page: add a section, rename it and set its aside, recolour, reorder,
remove (with its rows re-homed the way it works today), all undoable and
all in the changelog exactly as now. On a page there is room to show more
than the modal did: each section's row count and date range are worth
showing.
3. The Sections button leaves the header. Nothing else in the header moves.
The measured header fit (`fitHeader`) must still fit at every width in all
FOUR lenses.
4. `prefs.view` and `project.view.mode` accept the new lens. A refresh on
Setup comes back to Setup.
5. An exported HTML snapshot has the tab too and it works there with no
server, like every other edit does in a snapshot.
6. Tests: the `sections` flows drive the page instead of the modal; the
coverage manifest declares the new controls; the header sweep covers the
new lens. The suite stays green.
## Checkpoint 2 (0.59.0): tags on sections
1. Data: `phases[].tags`, an array of short strings. Trimmed, de-duplicated
without regard to case (first spelling wins), a sensible cap on length and
count. Absent means none. Tags carry no colour of their own: colour on this
chart means the section, and only that.
2. Persistence end to end in `server.js`: the sidecar, `/export/json`,
`/import/json`, the HTML snapshot, and the changelog (a tag edit is an
event with a readable line in `?format=md`). Old data files and old
sidecars load unchanged.
3. Setup page: edit a section's tags in place (type, Enter, remove with a
click; suggest tags already in the plan). A **Tags** block lists every tag
in the plan with how many sections (later, rows) carry it, and can rename
a tag everywhere or remove it everywhere. Undo and redo cover all of it,
one gesture to one Ctrl+Z.
4. One click to make tags from name prefixes: for sections named
"Launch · Program and gates" it offers the tag "Launch". It adds tags and
never renames a section. Only offered when the plan has such names.
5. Seeing them: the section header row in the chart shows its tags as small
quiet chips, and the Table's Phase column can show them on hover or
inline, whichever reads better.
6. Filtering: the legend row gains tag chips. Choosing one or more tags shows
only the sections carrying any of them, in the chart and the table alike.
It composes with the per-section chips and with Collapse all. It is view
state, per person, like the section filter is. With 20 sections and 4 tags
the legend must stay tidy: the tags are what make a 20-chip legend
manageable, so consider letting a tag chip stand in front of its sections.
7. Tests for every item above, asserting effects against `/export/json`.
## Checkpoint 3 (0.60.0): tags on tasks and milestones (optional per row)
1. Data: `items[].tags`, same rules. A row's EFFECTIVE tags are its own plus
its section's; the tag filter uses effective tags, so a task tagged
"long-lead" in an untagged section is found by that tag.
2. Editing: a Tags field in the full item editor and in the New panel, a Tags
column in the Table (editable in place, sortable, filterable), and the
batch panel can add or remove a tag across the selection.
3. `/export/csv` and `/import/csv` carry a Tags column. Copy and paste carry
tags.
4. Analytics: a per-tag card (items, hours, share), counting effective tags.
5. The Tags block on Setup counts rows as well as sections.
6. Tests for every item above.
## Housekeeping, every checkpoint
- Bump the version in `package.json` and `page.json`.
- Update `SKILL.md` and `README.md` (the schema tables, the features list,
the lens descriptions). No em dashes in prose. Match the surrounding voice.
- Add an entry at the TOP of `docs/BUILD-NOTES.md` (it is newest first and
about 400KB: read only its head).
- Do not publish, do not touch /home/adom/project/adom_modules or
~/.claude/skills, leave no processes running. The orchestrator publishes.
## Build notes
## Build notes - 2026-09-29, checkpoint 1
Built as planned, with these changes to the plan's assumptions:
- **`project.view.mode` did not accept Analytics either.** `viewDefaults()` read only
`gantt` and `table`, so "accept the new lens" meant fixing the list, not adding one
value. It now reads a single `VIEW_LIST` (the same list `VIEWS` is), so any lens,
including `setup` and `analytics`, can be authored as the opening lens.
- **The modal's class names and row markup were kept** (`.sec-row`, `.sec-btn[data-a]`,
`.sec-sw`, `#secNew`, `#secAdd`). The operations were already correct and tested; moving
the frame rather than rewriting the rows kept the flows transferable and gives
checkpoint 2 one obvious place to add a tag editor per row (a new element in
`renderSections()`), with the Tags block as a second `.an-card` in `.su-stack`.
- **One row editor at a time**, which the modal did not enforce. On a page with 20 rows,
two half-open forms is a real state; opening one closes the other. The swatch toggles.
- **Undo repaints the list.** The modal never did (Ctrl+Z with it open left a stale
list). The page re-renders from `refreshSections()` and `scheduleSave`, skipping while
a row editor is open. Checkpoint 2's tag edits should end in `refreshSections()` too.
- **The date range is read from the rows**, not from `phases[].start/end`, matching the
Analytics per-section card and what the chart draws.
- **Coverage now opens the three row editors** as states and declares their controls
(`.sec-nm`, `.sec-ld`, `.sec-ok`, `.sec-chip`, `.sec-hex`, `.sec-dest`, `.sec-go`,
`.sec-no`); inside the modal the gate never reached them. `#secDone` is gone.
- Observed while building: another process was editing `server.js`, `README.md`,
`SKILL.md` and `test/server_test.mjs` in this same working copy (server-side tags).
Checkpoint 1 did not touch `server.js`; its README and SKILL edits were made as
narrow in-place replacements so as not to overwrite that work.
## Build notes - 2026-09-30, checkpoint 1 review fixes
- Section order and row assignment after a reload (V-A) was wrong in both the page and
the server's merge, since 0.57.1: the row overrides were checked against the data
file's section list before the sidecar's longer list replaced it. Fixed in both
(`server.js` with the coordinator's go-ahead), with a server_test case.
- An empty section range (V-B) now means "no range of its own" everywhere the page reads
one: the axis scan and the start-date shift. The server already skipped it.
- Sections now have a client-only identity, `phaseKey` (`_k`), used by the reorder undo
and by every Setup row editor instead of a name or an index. Checkpoint 2's tag editor
per section should resolve its section the same way (`phaseIndexOfKey`) and should
end in `refreshSections()` so an open editor is kept across repaints.
- Section names must be unique (trimmed, case-insensitive). The checkpoint 2 tag rename
should expect that a section name identifies one section.
- One gesture, one undo, via `withOneUndo`: checkpoint 2's "rename a tag everywhere" is
the obvious next user of it.
## Build notes - 2026-09-30, checkpoint 2
Built as planned, with these changes to the plan's assumptions:
- **"The Tags block counts sections (later, rows)" and the legend stand-in were designed
together.** A tag chip in the legend stands in front of its sections literally: with
tags in the plan the section chips fold behind one "N sections" chip (open it to reach
them all, remembered as `prefs.legendSecs`), and choosing tags brings back the chips of
exactly the sections on screen. The plan said "consider"; without the fold, tag chips
would have made a 20-chip legend a 24-chip one.
- **The tag filter HIDES sections; the section chips still only dim.** "Shows only the
sections carrying any of them" read as hide, and hiding is what makes the filter worth
having on a 20-section plan. It reuses the collapsed-row class and flag, header included,
so geometry needed no new code. The section chips keep their dim-in-chart,
drop-from-table behaviour inside the filter.
- **The section filter is NOT persisted today** (`phaseVis` is in memory), though the
plan said the tag filter should be "like the section filter". The tag filter is
persisted (`prefs.tagFilter`), as the brief asked; the section chips were left as they
are.
- **Changelog lines are diffed, not posted.** `phase.tags` comes out of `diffAndLog` at
save time rather than `logPhaseEvent`, so undo and redo are logged too and a multi-section
gesture is one POST. Its `via` is `setup`, not the `sections` the other section edits use.
- **A new undo type, `phase.tags`,** instead of `phaseSnapshot`, which would also have
rolled back the collapse set and the legend visibility.
- **The prefix is the text before the first " · "** (spaces both sides). Other separators
(" - ", ":") were left out on purpose: "Phase 1 - Design" would have made "Phase 1" a tag.
Notes for checkpoint 3 (rows):
- `getState()` already writes `items[].tags` when a row has an array and `added[]` carries
`tags`; `applyState` reads `sv.tags`. Clearing a row's tags should leave `r.tags = []`
(not delete the key) so the sidecar says "cleared".
- `phaseShown(i)` is the ONE predicate the chart (`applyCollapse`) and the table
(`tableRows`) use. Effective row tags means a per-ROW predicate; replace the per-lane
checks there with one `rowShown(r)` (own tags plus `phaseTagsOf(r)`), and decide whether a
section header shows when only some of its rows match (probably yes, with those rows).
- `planTags()` counts sections; the Tags card and the legend chip counts read it. Add a
row count beside it rather than changing what `n` means, since the fold and the prefix
logic rely on it being per section.
- `diffAndLog` already has the row side of the server's `tags` event to fill in
(`snapSemantic().byId` has no tags yet).
- The client CSV already has the `tags` column in the server's position.
## Build notes - 2026-09-30, checkpoint 2 review fixes
- The Tags card moved ABOVE the Sections card (it is short, the list is long); the plan
said "a Tags block" without a place.
- A rename carries the reader's tag filter with it, and its undo carries it back: the undo
entry for a rename holds the filter as it was.
- Header chips beat the aside for room; the name comes second, the aside last.
- The changelog's tag lines are worded by direction (tagged, untagged, retagged).
- Commit on blur for the tag box. Escape discards.
- The prefix button still writes one `phase.tags` line per section; a summary line needs a
new server action, left for later.
- For checkpoint 3: `editTags(change, refilter)` is the path for any tag gesture that must
also move the filter; a row-tag rename in the Tags card should go through it too.
## Build notes - 2026-09-30, checkpoint 3
Built as planned, with these changes to the plan's assumptions:
- **The legend chip counts rows now, not sections.** With row tags, "# long-lead 0" (no
section carries it) or "# Tech 7" (sections) no longer said how much a chip would show.
The chip's number is the rows it finds, own or inherited, the same number the Analytics
per-tag card calls items. `planTags().n` still means sections, as checkpoint 2 asked.
- **A section header shows when only some of its rows match** (checkpoint 2 said
"probably yes"): yes, with those rows and no others. `phaseShown(i)` became "the header
is on screen", and the per-row check is `rowShown(r)`.
- **The batch panel acts on every selected row, milestones included, and at once.** Its
date fields are task-only and wait for Apply; a tag means the same on a milestone and
there was no reason to make an add wait on a date Apply.
- **The item editor logs `via: item-editor`, not `editor`.** Every other field that panel
saves has always been logged as `item-editor`; `editor` is the bar panel's. SKILL.md's
example line was corrected rather than the via.
- **A row created with tags writes its own `tags` line (`via: create`)** after its `add`,
so the log says where the tag came from. A paste keeps its single summary line.
- **The Tags column shows the section's tags too**, dashed, after the row's own. It is the
one place the effective-tag rule the filter uses can be seen row by row. Only the row's
own are edited there.
- **Every save re-checks the plan's tag picture** (a signature of `planTags()`), because
deletes, undos of creates and section moves change which tags exist and what a chip
would show without being tag edits.
- Not done: section tags are not editable from the table (they belong to Setup); the
chart shows only a row's OWN tags in its label (the header carries the section's).
## Build notes - 2026-09-30, checkpoint 3 review fixes
- Row tags in the chart label now take only the room the name and pills leave (`flex: 1 1 0`,
the gap inside their box), so the name is never cut for them.
- `;` is not allowed inside a tag (it is the CSV separator); both normalisers turn it into a
space. The CSV is documented as lossy for section and inherited tags.
- The table's Tags cell saves per chip, like the Setup box, and keeps its caret in view.
- Lane header counts read "2 of 3" under a tag filter; a delete or tag edit that prunes the
filter restores it on Ctrl+Z (`viewFilter` on the undo entry, kept by time travel).