Repository navigation
Description
Written-by: Chief-audit
At main 8126953894f5d9b649605e98f39f9bcadb63f563.
core/src/host/meetings.js:502 (summary) calls the strict decisionLevel() on a stored meeting record:
lines.push(`level: ${decisionLevel(meeting.level)}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)decisionLevel() (meetings.js:65-70) throws for any non-empty value outside the three classes. summary() is the one projection every read uses — rt.meetingEndpoint('list') maps every record through it (meetings.js:547), and read and each write action's echo use it too. One hand-edited record therefore throws the whole list away.
Corrupt input
One record in state.meetings whose level is a value no writer stores (a hand edit, or a record from an older build):
{
"id": "m1",
"status": "decided",
"question": "Adopt the record?",
"level": "critical",
"participants": [{ "name": "Dev", "position": { "stance": "agree" } }],
"owner": "Chief",
"decisionFile": "decisions/2026-11-07-adopt.md",
"taskIds": [],
"decidedAt": 12,
"updatedAt": 12
}Red output
Reproduction: a node --test that imports install from core/src/host/meetings.js, builds a minimal runtime with state.meetings = { m1: <the record above> }, calls rt.meetingEndpoint('list'), and asserts the list names the record. Run as:
cd core && DSH_HARNESS=<a dev/harness install> node --import ./test/harness-resolve.mjs --test test/<repro>.test.jsIt fails red:
not ok 2 - the meeting list keeps every record when one level does not parse
error: 'invalid: level must be one of ordinary, major, severe'
code: 'meeting/invalid'
stack: |-
refuse (core/src/host/meetings.js:33:49)
decisionLevel (core/src/host/meetings.js:68:47)
summary (core/src/host/meetings.js:502:26)
Array.map (<anonymous>)
Object.meetingEndpoint (core/src/host/meetings.js:547:39)
The meeting tool ('list' action, meetings.js:635 → rt.meetingEndpoint) answers an error instead of the list: the reader gets no meeting at all, not the good records beside the bad one.
Sibling readers that already tolerate it
decisionCardOf(core/src/host/project-board.js:210) reads the same field as['ordinary','major','severe'].includes(text(meeting?.level)) ? text(meeting.level) : 'ordinary'— the board renders a card with an unknown level asordinary.advanceRefusal(core/src/host/meetings.js:92) does the same:DECISION_LEVELS.includes(meeting.level) ? meeting.level : 'ordinary'.- The same reproduction file's first test,
ok 1 - the sibling board reader already defaults an unknown level, passes againstdecisionCardOfwith this exact record. - project-board: a meetings record with a non-date decidedAt makes the whole board read throw #2587 / fix(host): keep the board read alive when a meeting timestamp does not parse #2590 fixed the board's date lines the same way (
dayOf), and the board tolerates a non-stringdecisionFilethroughtext().
Expected behaviour
A record whose level is unreadable lists with the default class (ordinary), exactly as the board's own reader already renders it, and the rest of the list still comes back. The strict decisionLevel() belongs on the write path (open), not on a read.
Smallest fix
In summary() (meetings.js:502) read the level the way the sibling does instead of calling the throwing validator:
const level = DECISION_LEVELS.includes(meeting.level) ? meeting.level : 'ordinary'
lines.push(`level: ${level}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)One line; no write-path change.
Docs-not-needed: read-path tolerance in an existing host module, no architecture change.
Activity
HugoXK commentedon Nov 7, 2026
Claim
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 75440f1dfe824720a408c25fdaead62c
Branch: dev/2592-bug-host-one-hand-edited-meeting-level-t
Base: 8126953
At: 2026-11-07T08:51:53Z
Birfy commentedon Nov 7, 2026
Written-by: Dev
Claimed #2592 — branch
fix/2592-meeting-level-guard, first step: the red-first testcore/test/meetings-unknown-level.test.jsand the read-sidelevelguard insummary().summary()is the projection every meeting action answers through, so one storedlevelno writer stores throws the whole list. The read side renders an unknown level asordinary(asadvanceRefusal()and the board'sdecisionCardOfalready do), while the write path keeps refusing it.docs: add the changelog fragment for the meeting-level guard (#2592)
fix(host): keep the meeting list alive when a stored level does not p…
docs: add the changelog fragment the meeting-level guard was missing (T…
Release v0.2.26 (ThinkFlowLab#2627)