Skip to content

fix(host): keep the board read alive when a meeting timestamp does not parse - #2590

Merged
Birfy merged 1 commit into
mainfrom
fix/2587-meeting-date-guard
Nov 7, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/2587-meeting-date-guard

Conversation

@Birfy

@Birfy Birfy commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Fixes #2587
Owner: Dev
Reviewer: Reviewer
Tip SHA: bb8d309
Base: 45f52b3

What changed

meetingsOf() in core/src/host/project-board.js guarded a stored meeting stamp with text() only — a non-empty string — then fed it to new Date(...).toISOString(). A decidedAt (decided list, :403) or createdAt (pending list, :417) that an older version or a hand edit left unparseable made the whole Object.projectBoard read throw RangeError: Invalid time value: no rows, no queues, no decisions, no board.

One helper beside the sibling guards (stampOf, the Number.isFinite(lease.expiresAt) check) now parses the stored value first and formats only a finite instant; a value that does not parse reads as no date, the way the other unreadable fields already do. Both call sites use it. A readable timestamp still formats exactly as before: same parse, same ISO day.

core/test/project-board-meeting-date.test.js — new test seeded from core/test/project-board.test.js: a decided meeting with decidedAt: 'not-a-date', a pending meeting with createdAt: 'not-a-time' and one readable date. It asserts the two unreadable records list with date: '', the readable one keeps 2026-11-01, the board's rows still render, and the project-board endpoint answers ok: true rather than an error.

core/src/host/project-board.js is a FlyBot-only file (upstream ds-bot/ has no copy), so generate stores it whole in patches/ds-bot/files/; the two mirrors in this diff are the same bytes, and no .patch hunk changes.

Red first

cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-board-meeting-date.test.js against main @ 45f52b3ad4f02cea4676895ad7f9e2802a307685, before any source change:

not ok 1 - #2587: a meetings record with a non-date decidedAt does not break the board read
  error: 'Invalid time value'
  name: 'RangeError'
    Date.toISOString (<anonymous>)
    meetingsOf (file:///.../core/src/host/project-board.js:403:78)
    Object.projectBoard (file:///.../core/src/host/project-board.js:565:81)

Reverting only the source change after the fix turns it red again with the same RangeError at :403; re-applying it is green.

Ran at the tip

TZ=UTC, at bb8d309672b987b043bfdb5ced861846322df8c7:

  • cd core && node --import ./test/harness-resolve.mjs --test test/project-board-meeting-date.test.js — 1 test, 1 pass.
  • cd core && node --import ./test/harness-resolve.mjs --test test/project-board.test.js — 58 tests, 58 pass.
  • cd core && node --import ./test/harness-resolve.mjs --test --test-concurrency=6 "test/*.test.js" — 3005 tests, 3002 pass, 3 fail. All three (test/check-arch-docs.test.js × 2 — it shells out to git, absent on this host — and test/usage-fields.test.js — sqlite fixture) fail identically on the untouched base tree at 45f52b3a, so none is this diff.
  • cd core && npm run lint — oxlint 0 errors, 84 warnings (the count on main); arch-docs cannot run here (no git), and the Docs-not-needed: trailer is in both the branch commit and this body.
  • npm --prefix plugins/{social,tree,timeline,workflow} test — 578, 137, 145, 453 tests, all pass.
  • node scripts/lint-plugins.mjs; node core/scripts/build-client.mjs --check; node scripts/ds-bot-patches.mjs check (10 patches apply; core/ equals ds-bot/ + patches/ds-bot/); node --test ".github/scripts/*.test.mjs" "policy/*.test.mjs"; node scripts/lint-interop.mjs; interop tests; node scripts/lint-live-e2e.mjs; live-e2e tests.

Not run: arch-docs (needs git, absent here), ds-bot-patches canonical (its generate needs git), and the smoke test (no DSH checkout/bench on this host).

Docs-not-needed: a reader-side guard on an existing timestamp; no subsystem behaviour changes

Scope

Only core/src/host/project-board.js, the new test, and their generated patches/ds-bot/files/ mirrors — no plugins/**, no CHANGELOG.md, no UNRELEASED, no changelog fragment: a reader-side guard on a stamp the current writer never produces.

Checkpoint

{
"pr": 2590,
"claim": "Dev",
"issue": 2587,
"summary": "A meetings record with an unparseable decidedAt or createdAt made the whole Project board read throw RangeError: Invalid time value. One helper now parses a stored stamp and formats only a finite instant, so the meeting lists with an empty date and the rows, queues and decisions still render. A readable timestamp formats exactly as before. The new test seeds decided and pending meetings with non-date strings and proves the endpoint answers ok: true.",
"files": [
"core/src/host/project-board.js",
"core/test/project-board-meeting-date.test.js",
"patches/ds-bot/files/src/host/project-board.js",
"patches/ds-bot/files/test/project-board-meeting-date.test.js"
],
"checks": {
"ran": "core: new test (1/1), project-board.test.js (58/58), full suite --test-concurrency=6 (3002 pass, 3 pre-existing env failures); core lint (oxlint 0 errors); plugins social/tree/timeline/workflow; lint-plugins; build-client --check; ds-bot-patches check; .github/scripts + policy tests; interop and live-e2e lint/tests",
"result": "green",
"at": "bb8d309672b987b043bfdb5ced861846322df8c7"
},
"next": "none — ready for review"
}

…t parse (#2587)

`meetingsOf()` guarded a stored meeting stamp with `text()` only and then fed
it to `new Date(...).toISOString()`. A `decidedAt` / `createdAt` left behind
by an older version or a hand edit made the whole `Object.projectBoard` read
throw `RangeError: Invalid time value`, so the board, its queues and its
decisions never rendered. A value that does not parse now reads as no date, the
way the sibling readers already treat an unreadable stamp, and the rest of the
board is unaffected.

Fixes #2587
Docs-not-needed: a reader-side guard on an existing timestamp; no subsystem behaviour changes
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 7, 2026
@Birfy Birfy self-assigned this Nov 7, 2026
@Birfy Birfy added agent-pr Pull request opened by an agent and removed agent-pr Pull request opened by an agent labels Nov 7, 2026
@Birfy

Birfy commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at bb8d309
Written-by: Reviewer
Attestation: bb8d309 role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-board-meeting-date.test.js
# tests 1  # pass 1  # fail 0
$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-board.test.js
# tests 58  # pass 58  # fail 0
$ cp ../base/core/src/host/project-board.js src/host/project-board.js   # only the changed source, back to the base blob (git sha f904a33119, = main @45f52b3a)
$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-board-meeting-date.test.js
# tests 1  # pass 0  # fail 1   RangeError: Invalid time value — meetingsOf :403 -> Object.projectBoard
$ cp /tmp/pb-tip.js src/host/project-board.js   # restored; md5 matches the tip blob a9bd4aa9, and the new test is green again
$ node /data/dsh/bot/workspace/p2590/tier.mjs   # base .github/scripts/risk-gate.mjs judge() over the PR file list
# risk-tier: T2; basis: core/src/host/project-board.js=T2, core/test/project-board-meeting-date.test.js=T1, patches/...=T1

Not run: arch-docs, ds-bot-patches canonical — both need a git binary this host does not have; the smoke test (no DSH bench here).

Review:

  • Tier T2. Recomputed from the diff, not read from a label: core/src/host/project-board.js is T2, the test and the two generated mirrors are T1.
  • Scope is the claim. 4 files: core/src/host/project-board.js (+9/−2), the new test (+92), and the same bytes in patches/ds-bot/files/ — verified byte-for-byte against the tip blobs, so core/ and the mirrors cannot disagree. No plugins/**, no CHANGELOG.md, no version, no fragment.
  • The fix is the class, not one site. dayOf() (:114) now Date.parse(text(value))s an unparseable stamp into '' instead of new Date(...).toISOString(), and both call sites — the decided list :411 (decidedAt) and the pending list :428 (createdAt) — go through it. A readable stamp keeps the identical day: same parse, same ISO slice.
  • Red first, as required. With only the changed source reverted to the base blob the new test fails at meetingsOf → Object.projectBoard with RangeError: Invalid time value; restored, 1/1 green. The base-blob round trip is the one the PR claims.
  • No new green path was bought by weakening an assertion. The test seeds three meetings — unparseable decidedAt, unparseable createdAt, one readable date — and checks the readable one still formats to 2026-11-01, so a guard that blanked every date would fail.
  • CI at the tip: tests-ok, ubuntu-latest · Node 24 · DSH 0.2.0-rc.2, merge-gate, package, all three plugins (*), policy-sign, mock · ubuntu are success; enqueue-guard is the only failure and its message is no Verifier report names the tip bb8d309672b9, which this comment answers. Nothing is still running.
  • Enqueue criteria: no hold, agent-pr present, body carries Fixes #2587 and Owner: Dev / Reviewer: Reviewer (different names), checkpoint block present and within limits.

T0/T1 no-PASS exemption does not apply, so this is the one conclusion at bb8d309672b987b043bfdb5ced861846322df8c7; a new commit voids it.

@github-actions github-actions Bot added the pass label Nov 7, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit 8126953 Nov 7, 2026
19 of 21 checks passed
@Birfy
Birfy deleted the fix/2587-meeting-date-guard branch November 7, 2026 08:27
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

project-board: a meetings record with a non-date decidedAt makes the whole board read throw

1 participant