agent-coord-mcp 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/work.ts CHANGED
@@ -63,6 +63,33 @@ export type BoardRow = {
63
63
  raw?: string;
64
64
  };
65
65
 
66
+ // The one arity BoardRow can hold. boardCells() and the parser's arity guard
67
+ // both reference this constant so the two sides cannot drift; a test pins
68
+ // boardCells().length === BOARD_ARITY.
69
+ export const BOARD_ARITY = 5;
70
+
71
+ // A table row whose column count does not match BoardRow. It is REFUSED a
72
+ // record (boardRowsOf never returns it) but PRESERVED byte-exactly — the
73
+ // alternative, destructuring whatever arity into a fixed 5-tuple, silently
74
+ // narrowed 6-column rows and widened 4-column ones on export (hit live
75
+ // 2026-07-29: #38's extra `Pane` column was dropped on write-back). Refusing
76
+ // the row rather than throwing keeps one bad row from taking down the whole
77
+ // document's import. The schema decision stays with the board owner: widening
78
+ // BoardRow is a deliberate act (change BOARD_ARITY, boardCells, and this
79
+ // guard together), never something a docs commit does by accident.
80
+ export type MalformedRow = {
81
+ malformed: true;
82
+ // Replayed byte-exactly on render — never narrowed, never widened.
83
+ verbatim: string;
84
+ // Names the line, quotes the row, states expected/actual — loud enough to
85
+ // fix from the message alone.
86
+ issue: string;
87
+ };
88
+
89
+ export function isMalformedRow(r: BoardRow | MalformedRow): r is MalformedRow {
90
+ return (r as MalformedRow).malformed === true;
91
+ }
92
+
66
93
  // A parsed document: an ordered block list. `text` blocks are verbatim lines
67
94
  // (never interpreted); the others carry records rendered back in place.
68
95
  export type Block =
@@ -71,7 +98,7 @@ export type Block =
71
98
  | { kind: "done"; entries: DoneEntry[] }
72
99
  // `header` and `align` are kept as raw lines for the same reason as
73
100
  // BoardRow.raw — the alignment row (`|---|---|`) has no canonical form.
74
- | { kind: "board"; header: string; align: string; rows: BoardRow[] };
101
+ | { kind: "board"; header: string; align: string; rows: (BoardRow | MalformedRow)[] };
75
102
 
76
103
  export type WorkDoc = {
77
104
  // Kept so an export can be written back with the exact byte tail it had.
@@ -182,11 +209,13 @@ function boardCells(r: BoardRow): string[] {
182
209
  }
183
210
 
184
211
  // Replay the original line when the fields still say what it said; re-render
185
- // once anything actually changed.
186
- export function renderBoardRow(r: BoardRow): string {
212
+ // once anything actually changed. A malformed row has no fields to have
213
+ // changed — it replays its bytes, always.
214
+ export function renderBoardRow(r: BoardRow | MalformedRow): string {
215
+ if (isMalformedRow(r)) return r.verbatim;
187
216
  if (r.raw !== undefined) {
188
217
  const cells = splitRow(r.raw);
189
- if (cells.length === 5 && cells.every((c, i) => c === boardCells(r)[i])) return r.raw;
218
+ if (cells.length === BOARD_ARITY && cells.every((c, i) => c === boardCells(r)[i])) return r.raw;
190
219
  }
191
220
  return renderRow(boardCells(r));
192
221
  }
@@ -280,11 +309,28 @@ export function parseWorkDoc(source: string): WorkDoc {
280
309
 
281
310
  // Lanes table: a header row followed by an alignment row.
282
311
  if (TABLE_ROW.test(line) && TABLE_ALIGN.test(lines[i + 1] ?? "")) {
283
- const rows: BoardRow[] = [];
312
+ const rows: (BoardRow | MalformedRow)[] = [];
284
313
  let j = i + 2;
285
314
  for (; j < lines.length && TABLE_ROW.test(lines[j] ?? ""); j++) {
286
315
  const raw = lines[j] ?? "";
287
- const [lane = "", owner = "", state = "", currentSlice = "", nextGo = ""] = splitRow(raw);
316
+ const cells = splitRow(raw);
317
+ // Arity guard: a row BoardRow cannot hold is refused a record, kept
318
+ // verbatim, and named loudly — destructuring it into the 5-tuple is
319
+ // exactly the silent narrow/widen this exists to prevent. An empty
320
+ // trailing cell (`| a | b | c | d | |`) is still five columns and
321
+ // still a legitimate row.
322
+ if (cells.length !== BOARD_ARITY) {
323
+ rows.push({
324
+ malformed: true,
325
+ verbatim: raw,
326
+ issue:
327
+ `board row at line ${j + 1} has ${cells.length} column(s), expected ${BOARD_ARITY} — ` +
328
+ `refused (kept verbatim, excluded from board records). Fix the row, or widen BoardRow ` +
329
+ `deliberately (BOARD_ARITY + boardCells + this guard together). Row: ${raw}`,
330
+ });
331
+ continue;
332
+ }
333
+ const [lane = "", owner = "", state = "", currentSlice = "", nextGo = ""] = cells;
288
334
  rows.push({ id: idFor("b", lane), lane, owner, state, currentSlice, nextGo, raw });
289
335
  }
290
336
  flushText();
@@ -325,5 +371,16 @@ export function doneEntriesOf(doc: WorkDoc): DoneEntry[] {
325
371
  }
326
372
 
327
373
  export function boardRowsOf(doc: WorkDoc): BoardRow[] {
328
- return doc.blocks.flatMap((b) => (b.kind === "board" ? b.rows : []));
374
+ return doc.blocks.flatMap((b) =>
375
+ b.kind === "board" ? b.rows.filter((r): r is BoardRow => !isMalformedRow(r)) : [],
376
+ );
377
+ }
378
+
379
+ // Every refused row's issue, in document order — what makes the parse-time
380
+ // rejection LOUD at the tool layer (import_work/list_work/export_work all
381
+ // surface it) instead of a filtered-out record nobody notices.
382
+ export function workDocIssues(doc: WorkDoc): string[] {
383
+ return doc.blocks.flatMap((b) =>
384
+ b.kind === "board" ? b.rows.filter(isMalformedRow).map((r) => r.issue) : [],
385
+ );
329
386
  }