diff --git a/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md b/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md new file mode 100644 index 000000000..bbd281357 --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-partial-markdown-streaming-body-rows.md @@ -0,0 +1,367 @@ +# Streaming Table Body Rows Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** An in-progress table body row renders as a streaming row inside the table's `` (growing cell-by-cell) instead of flickering as a raw-pipe paragraph or a spurious second table. + +**Architecture:** One `optimisticBlock`-gated branch in `handleBlockLine` (`block.ts`), placed before the `mode === 'table'` close: a partial open-line row (leading pipe, no trailing pipe yet) appends via the existing `appendTableRow` builder on the throwaway projection state. Committed parse untouched. Ships as `@cacheplane/partial-markdown` 0.5.3, consumed by `libs/chat`; a Chrome MCP live-stream capture is a required verification gate. + +**Tech Stack:** TypeScript, Vitest, tsup (cacheplane); Angular/Vitest (ngaf `libs/chat`); Chrome MCP for the live gate. + +**Repo note:** Tasks 1–3 run in **`~/repos/cacheplane`** (branch off main). Tasks 4–6 run in **`~/repos/angular-agent-framework`**. cacheplane main is PROTECTED (3 required checks) — release goes PR → squash-merge → annotated tag on the merged commit. + +**Spec:** [docs/superpowers/specs/2026-07-05-partial-markdown-streaming-body-rows-design.md](../specs/2026-07-05-partial-markdown-streaming-body-rows-design.md) + +--- + +## File Structure + +**`~/repos/cacheplane/packages/partial-markdown`:** +- `src/handlers/block.ts` — `OPEN_TABLE_ROW_RE` (next to `TABLE_HEADER_INPROGRESS_RE`, ~line 682) + the new branch in `handleBlockLine` (before the table close at ~line 97). +- `src/__tests__/streaming-table.test.ts` — new `describe` block for body rows (appended; reuses the existing `blocks()` helper). +- `CHANGELOG.md`, `package.json` — 0.5.3. + +**`~/repos/angular-agent-framework`:** +- `libs/chat/package.json`, `package-lock.json` — `^0.5.2 → ^0.5.3` (surgical lockfile edit; never regenerate). +- `libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts` — body-row anti-flicker test appended. + +--- + +## Task 1: Body-row projection branch (TDD) + +**Repo:** `~/repos/cacheplane` — first create the branch: `git checkout main && git pull --ff-only && git checkout -b feat/streaming-table-body-rows` + +**Files:** +- Modify: `packages/partial-markdown/src/handlers/block.ts` (~line 97 branch; ~line 682 regex) +- Test: `packages/partial-markdown/src/__tests__/streaming-table.test.ts` (append) + +- [ ] **Step 1: Write the failing tests** + +Append to `packages/partial-markdown/src/__tests__/streaming-table.test.ts` (the file already imports `createPartialMarkdownParser`/`materialize` and defines `blocks()`): + +```ts +describe('streaming table body rows — open-line projection', () => { + // Committed header + delimiter, so the table is active (mode 'table'). + const HEADER = '| A | B |\n| - | - |\n'; + + it('projects a bare "|" as an empty in-progress row (no paragraph)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('|'); + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + expect(doc.children[0].children).toHaveLength(2); // header + empty in-progress row + }); + + it('projects a partial row into the SAME table (no spurious second table)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1 | y'); // two pipes, no trailing pipe — used to spawn a 2nd optimistic table + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + const rows = doc.children[0].children; + expect(rows).toHaveLength(2); // header + in-progress body row + const lastRow = JSON.stringify(rows[1]); + expect(lastRow).toContain('x1'); + expect(lastRow).toContain('y'); + }); + + it('grows the in-progress row cell-by-cell, padded to header width', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1'); + let doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].children[1].children).toHaveLength(2); // padded to 2 cols + p.push(' | y1'); + doc = materialize(p.root) as any; + expect(JSON.stringify(doc.children[0].children[1])).toContain('y1'); + }); + + it('still appends a COMPLETE open-line row (regression guard)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + p.push('| x1 | y1 |'); // trailing pipe — pre-existing mid-table branch + const doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].children).toHaveLength(2); + }); + + it('leaves the committed parse unchanged (newline-terminated + finish)', () => { + const p = createPartialMarkdownParser(); + p.push('| A | B |\n| - | - |\n| x1 | y1 |\n| x2 | y2 |\n'); + p.finish(); + const doc = materialize(p.root) as any; + expect(doc.children).toHaveLength(1); + expect(doc.children[0].type).toBe('table'); + expect(doc.children[0].status).toBe('complete'); + expect(doc.children[0].children).toHaveLength(3); // header + 2 body rows + }); + + it('a blank open line still closes nothing prematurely (table stays, no extra row)', () => { + const p = createPartialMarkdownParser(); + p.push(HEADER); + // No open line at all: committed table only. + expect(blocks(p)).toEqual([{ type: 'table', status: 'streaming' }]); + const doc = materialize(p.root) as any; + expect(doc.children[0].children).toHaveLength(1); // header only + }); +}); +``` + +- [ ] **Step 2: Run to verify the new tests fail** + +Run: `pnpm -C packages/partial-markdown test -- streaming-table` +Expected: FAIL — bare `|` yields `[table, paragraph]`, `| x1 | y` yields `[table, table]` (the 0.5.2 artifacts). The complete-row, committed-unchanged, and header-only tests already pass. + +- [ ] **Step 3: Add the regex + branch** + +In `packages/partial-markdown/src/handlers/block.ts`: + +(a) Next to the existing header regex (~line 682, immediately above `const TABLE_HEADER_INPROGRESS_RE`): + +```ts +// A line that has STARTED a table row but not necessarily finished it: up to +// three spaces of indent then a leading pipe. Prefix-consistent with +// TABLE_ROW_RE (which additionally requires the trailing pipe). +const OPEN_TABLE_ROW_RE = /^\s{0,3}\|/; +``` + +(b) In `handleBlockLine`, find (~line 96): + +```ts + // Close open table if current line is not a table row. + if (s.mode === 'table' && !TABLE_ROW_RE.test(line)) { + s = closeOpenTable(s); + } +``` + +Insert **immediately before** it: + +```ts + // Optimistic projection only: while a table is active, an open line that + // begins a new row (leading pipe, row not yet complete) projects as an + // in-progress body row appended to the active table — instead of closing the + // table and rendering the partial row as a paragraph or a spurious second + // optimistic header table. A bare "|" projects as an empty in-progress row. + if (s.optimisticBlock && s.mode === 'table' && OPEN_TABLE_ROW_RE.test(line) && !TABLE_ROW_RE.test(line)) { + return appendTableRow(s, line); + } + +``` + +Note: the local variable at this point is `s` (declared just above as `{ ...state, line: state.line + 1, lineBuffer: '' }`) — verify by reading the surrounding code. + +- [ ] **Step 4: Run the streaming-table suite to verify green** + +Run: `pnpm -C packages/partial-markdown test -- streaming-table` +Expected: PASS (all, including the 12 pre-existing 0.5.2 tests). + +- [ ] **Step 5: Run the full package suite (incl. chunk-fuzz)** + +Run: `pnpm -C packages/partial-markdown test` +Expected: PASS — the chunk-invariance property (table corpus entry) re-verifies the new branch never changes committed output under any chunk split. + +- [ ] **Step 6: Commit** + +```bash +cd ~/repos/cacheplane +git add packages/partial-markdown/src/handlers/block.ts packages/partial-markdown/src/__tests__/streaming-table.test.ts +git commit -m "feat(partial-markdown): project in-progress table body rows on the open line" +``` + +--- + +## Task 2: Lint, typecheck, CHANGELOG, version 0.5.3 + +**Repo:** `~/repos/cacheplane` + +**Files:** +- Modify: `packages/partial-markdown/CHANGELOG.md`, `packages/partial-markdown/package.json` + +- [ ] **Step 1: Lint + typecheck** + +Run: `pnpm -C packages/partial-markdown lint && pnpm -C packages/partial-markdown typecheck` +Expected: PASS, no errors. Fix any issues before continuing. + +- [ ] **Step 2: Prepend the CHANGELOG entry** (above `## 0.5.2 — 2026-06-27`): + +```markdown +## 0.5.3 — 2026-07-05 + +### Added + +- **Streaming table body rows.** While a table is active, an in-progress body + row on the open line now renders as a streaming row inside the table — + growing cell-by-cell, padded to header width — instead of momentarily + closing the table and flashing as a raw-pipe paragraph (bare `|` / one pipe) + or a spurious second header-only table (two+ pipes). A bare `|` projects as + an empty in-progress row. Projection-only: the committed parse is unchanged. + Completes the streaming-table work begun in 0.5.2. +``` + +- [ ] **Step 3: Bump version** — `packages/partial-markdown/package.json`: `"version": "0.5.2"` → `"version": "0.5.3"`. + +- [ ] **Step 4: Build** — Run: `pnpm -C packages/partial-markdown build` — Expected: tsup regenerates `dist/` cleanly. + +- [ ] **Step 5: Commit** + +```bash +cd ~/repos/cacheplane +git add packages/partial-markdown/CHANGELOG.md packages/partial-markdown/package.json +git commit -m "chore(partial-markdown): 0.5.3 — streaming table body rows" +``` + +--- + +## Task 3: Release 0.5.3 (PR → tag → npm) + +**Repo:** `~/repos/cacheplane`. Main is protected (3 required checks: Workspace install+lint+typecheck, Package partial-json, Package partial-markdown) — direct pushes are rejected. + +- [ ] **Step 1: Dry-run the publish** + +```bash +cd ~/repos/cacheplane/packages/partial-markdown && npm publish --dry-run; cd - +``` +Expected: would publish `@cacheplane/partial-markdown@0.5.3`, `dist/` files included. + +- [ ] **Step 2: Push branch + open PR** + +```bash +cd ~/repos/cacheplane +git push -u origin feat/streaming-table-body-rows +gh pr create --fill --title "feat(partial-markdown): streaming table body rows (0.5.3)" +``` + +- [ ] **Step 3: Merge on green (3 checks), then tag the merged main commit** + +```bash +gh pr checks --watch # all 3 required checks pass +gh pr merge --squash --delete-branch +git checkout main && git pull --ff-only +git tag -a partial-markdown-v0.5.3 -m "partial-markdown 0.5.3 — streaming table body rows" +git push origin partial-markdown-v0.5.3 +``` +Expected: tag push triggers `.github/workflows/publish.yml` (OIDC). + +- [ ] **Step 4: Verify npm** + +```bash +gh run list --workflow publish.yml --limit 1 # completed success +npm view @cacheplane/partial-markdown@0.5.3 version dist.integrity dist.tarball +``` +Expected: `0.5.3`. NOTE: npm CDN can lag ~1 min after workflow success — a brief 404 is propagation, not failure. Record `dist.integrity` and `dist.tarball` for Task 4. + +--- + +## Task 4: Consume 0.5.3 in `libs/chat` + body-row anti-flicker test + +**Repo:** `~/repos/angular-agent-framework` + +**Files:** +- Modify: `libs/chat/package.json` (`^0.5.2` → `^0.5.3`) +- Modify: `package-lock.json` — surgical hand-edit ONLY (never run bare `npm install`; regeneration on macOS drops Linux `@next/swc-*` bindings): the two range occurrences of `"@cacheplane/partial-markdown": "^0.5.2"` (root deps block + libs/chat block; find with `grep -n "partial-markdown" package-lock.json`) → `"^0.5.3"`, and the `node_modules/@cacheplane/partial-markdown` install block → version `0.5.3` + the `resolved`/`integrity` values recorded in Task 3. +- Modify: `libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts` (append) + +- [ ] **Step 1: Branch off main** + +```bash +cd ~/repos/angular-agent-framework +git checkout main && git pull --ff-only +git checkout -b feat/chat-consume-pm-0.5.3 +``` + +- [ ] **Step 2: Write the failing consumer test** + +Append inside the existing `describe('ChatStreamingMdComponent — streaming table rendering', …)` block in `libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts`: + +```ts + it('streams body rows inside the table — no paragraph, no second table (0.5.3)', () => { + host.streaming.set(true); + grow('| A | B |\n| - | - |\n'); + for (const c of ['|', '| x1', '| x1 | y', '| x1 | y1 |', '| x1 | y1 |\n']) { + grow('| A | B |\n| - | - |\n' + c); + expect(el.querySelectorAll('table').length, `one table at ${JSON.stringify(c)}`).toBe(1); + expect( + [...el.querySelectorAll('p')].some((p) => (p.textContent || '').includes('|')), + `no raw-pipe paragraph at ${JSON.stringify(c)}`, + ).toBe(false); + } + }); +``` + +- [ ] **Step 3: Verify it fails on 0.5.2** + +Run: `npx nx test chat --skip-nx-cache -- streaming-markdown.table-stream` +Expected: FAIL — on 0.5.2 the `'|'` step renders a raw-pipe `

` and the `'| x1 | y'` step renders 2 tables. + +- [ ] **Step 4: Bump dep + surgical lockfile edit** (values from Task 3). Refresh the on-disk copy WITHOUT touching the lockfile: `cd /tmp && npm pack @cacheplane/partial-markdown@0.5.3 && tar -xzf cacheplane-partial-markdown-0.5.3.tgz && rm -rf ~/repos/angular-agent-framework/node_modules/@cacheplane/partial-markdown && cp -r package ~/repos/angular-agent-framework/node_modules/@cacheplane/partial-markdown`. Verify: `cd ~/repos/angular-agent-framework && npm ls @cacheplane/partial-markdown` → `0.5.3`, no invalid/missing. + +- [ ] **Step 5: Verify the test passes + gates** + +Run: `npx nx test chat --skip-nx-cache -- streaming-markdown.table-stream` → PASS. +Run: `npx nx lint chat && npx nx test chat && npx nx build chat` → all PASS. + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/package.json package-lock.json libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts +git commit -m "build(chat): consume partial-markdown 0.5.3 — streaming table body rows + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 5: Chrome MCP live verification (REQUIRED GATE) + +**Repo:** `~/repos/angular-agent-framework`, on the Task 4 branch. This is the gate the user explicitly required. It is performed by the CONTROLLER session (needs Chrome MCP + the user's OPENAI key via `examples/chat/python/.env`), not a subagent. + +- [ ] **Step 1: Start the stack** — `npx nx run examples-chat:serve` in the background; poll until BOTH `http://localhost:2024/ok` returns 200 AND the log shows `Application bundle generation complete` (a port-200 alone can race the first build). Kill any stale servers on :4200/:2024 first. + +- [ ] **Step 2: Drive + capture via Chrome MCP** — navigate to `http://localhost:4200/embed`; install a 16ms DOM sampler on the last `chat-streaming-md` recording `{tables, pipeParagraphs}` per frame into `window.__frames`; send: `Output ONLY a markdown table of 12 chemical elements with columns Symbol, Name, Atomic Number, Group. No prose whatsoever.` (set textarea value via the native setter + input event, then keydown/keypress/keyup Enter). + +- [ ] **Step 3: Assert the capture** — read `window.__frames` after the stream: + - `tables === 1` in every frame after the first table frame (0 frames with 2 tables), + - `pipeParagraphs === 0` in every frame (0 raw-pipe `

` frames; the #743 baseline measured 2 such frames — must now be 0), + - final DOM: one table, 4 `thead th`, 12 `tbody tr`. + If any assertion fails: STOP, root-cause (systematic-debugging), do not proceed to Task 6. + +- [ ] **Step 4: Shut down the stack** — kill the serve processes by PID (`lsof -ti :4200 :2024`); confirm both ports free. (The nx process tree respawns children — kill by PID, verify, repeat if needed.) + +--- + +## Task 6: PR + merge on green + +**Repo:** `~/repos/angular-agent-framework` + +- [ ] **Step 1: Push + PR** + +```bash +git push -u origin feat/chat-consume-pm-0.5.3 +gh pr create --title "build(chat): consume partial-markdown 0.5.3 — streaming table body rows" --fill +``` +Include in the body: root cause recap (0.5.2 deferred body rows; partial open-line row closed the table → paragraph / 2nd-table flicker), the fix (projection-only body-row branch), test evidence (cacheplane suite, consumer red→green), and the Chrome MCP capture numbers from Task 5. + +- [ ] **Step 2: Merge on green** + +```bash +gh pr checks --watch # required: Vercel – threadplane +gh pr merge --squash --delete-branch +``` +If GitHub reports "head branch is not up to date" with green head checks and no conflicting files (stale-cache quirk / non-conflicting main advance), use `gh pr merge --squash --delete-branch --admin`. + +- [ ] **Step 3: Verify main** + +```bash +git checkout main && git pull --ff-only +gh run list --branch main --limit 3 +``` +Expected: merge commit present; CI green (ignore the pre-existing non-required "PostHog telemetry quality" red — stale secret, unrelated). + +--- + +## Out of scope (do not implement) + +- Setext headings; non-pipe-fenced tables; live alignment during the delimiter stream. +- Any chat component/view changes — none are needed. diff --git a/docs/superpowers/specs/2026-07-05-partial-markdown-streaming-body-rows-design.md b/docs/superpowers/specs/2026-07-05-partial-markdown-streaming-body-rows-design.md new file mode 100644 index 000000000..8efebcd50 --- /dev/null +++ b/docs/superpowers/specs/2026-07-05-partial-markdown-streaming-body-rows-design.md @@ -0,0 +1,105 @@ +# Streaming table body rows (partial-markdown) — Design + +**Date:** 2026-07-05 +**Status:** Approved, ready for implementation plan +**Repos:** `~/repos/cacheplane` (`@cacheplane/partial-markdown` — the fix, released as 0.5.3) and `~/repos/angular-agent-framework` (`libs/chat` — consume the release) +**Continues:** [2026-06-25-partial-markdown-streaming-table-header-design.md](2026-06-25-partial-markdown-streaming-table-header-design.md) (0.5.2 header projection) and the chat finalize-debounce fix (#743). This is the open-body-row case explicitly deferred from 0.5.2. + +## Problem + +With 0.5.2 + #743, a streaming table renders its header immediately and stays a table — but each **body row in progress** produces a transient artifact below the table. Probed against the live 0.5.2 dist, streaming `| A | B |\n| - | - |\n| x1 | y1 |\n…` char-by-char: + +``` +open line "|" -> [ table:streaming | paragraph:streaming ] (bare pipe → paragraph) +open line "| x1 | y" -> [ table:streaming | table:streaming ] (partial row → spurious 2nd table) +open line "| x1 | y1 |"-> [ table:streaming ] (complete row → appended correctly) +``` + +The user sees a raw-pipe paragraph or a phantom one-row table flicker under the real table as each row streams, settling only at each row's newline. + +### Root cause + +In `handleBlockLine` (`src/handlers/block.ts`), when a table is active (`s.mode === 'table'`) and the current line does **not** match `TABLE_ROW_RE` (`/^\s*\|.*\|\s*$/` — requires a trailing pipe), the table is closed (~line 83, `closeOpenTable`). On the open-line projection this fires for every partial row: the not-yet-complete row lacks its trailing pipe, so the projection closes the table and the partial row falls through to: + +- a **paragraph** (bare `|` or one pipe: `| x1`), or +- the 0.5.2 **optimistic header branch** (two+ pipes: `| x1 | y`) → a spurious second header-only table. + +A complete row on the open line (`| x1 | y1 |`) already appends correctly via the mid-table body-row branch (~line 161). + +## Goal + +While a table is streaming, an in-progress body row renders as a **streaming row inside the table's ``**, growing cell-by-cell — never as a paragraph and never as a second table. Committed parse unchanged. + +## Decisions (locked) + +1. **Projection-only** — same `optimisticBlock` architecture as 0.5.2. No committed-state changes; revert/`finish()` correctness automatic. +2. **Bare `|` open line projects as an empty in-progress row** (smoothest; no pop-in when the first cell char arrives). +3. **Fix in the library, not chat** — the projection is the right layer. +4. **Live verification via Chrome MCP is a required gate** — capture the streaming DOM frame-by-frame in the running examples/chat app and assert zero paragraph/second-table frames during body-row streaming, before release. + +## Change + +One new branch in `handleBlockLine` (`packages/partial-markdown/src/handlers/block.ts`), placed **immediately before** the `mode === 'table'` close at ~line 83: + +```ts + // Optimistic projection only: while a table is active, an open line that + // begins a new row (leading pipe, row not yet complete) projects as an + // in-progress body row appended to the active table — instead of closing the + // table and rendering the partial row as a paragraph or a spurious second + // optimistic header table. A bare "|" projects as an empty in-progress row. + if (s.optimisticBlock && s.mode === 'table' && OPEN_TABLE_ROW_RE.test(line) && !TABLE_ROW_RE.test(line)) { + return appendTableRow(s, line); + } +``` + +with, alongside the existing table regexes' local declarations in `block.ts`: + +```ts +// A line that has started a table row but not necessarily finished it: up to +// three spaces of indent then a leading pipe. Prefix-consistent with +// TABLE_ROW_RE (which additionally requires the trailing pipe). +const OPEN_TABLE_ROW_RE = /^\s{0,3}\|/; +``` + +Notes: +- `appendTableRow` + `splitTableCells` already handle a partial row: cells split on unescaped/non-code pipes, body rows pad/truncate to the header's `alignments.length`, so the row grows cell-by-cell and always has the full column count (trailing cells empty until their text streams). +- Placement before the ~line 83 `closeOpenTable` means the projection never closes the active table for a partial row. The committed path (no `optimisticBlock`) is untouched: it still closes the table when a non-row line follows, and appends real rows on their newline. +- The 0.5.2 optimistic **header** branch (~line 170) is unreachable for this case afterward (mode is `'table'`, that branch requires `mode === 'block'`) — the spurious-2nd-table artifact disappears because the partial row no longer falls through to it. +- No parser.ts / types.ts changes — `optimisticBlock` and the preview-state plumbing already exist. + +## Error handling / correctness invariants + +- **Committed parse unchanged.** Newline-terminated and `finish()` output identical to 0.5.2 (branch is `optimisticBlock`-gated). +- **Revert.** If the "partial row" turns out not to be a row (e.g. the open line continues into `| just prose…` and ends without a trailing pipe at newline), the committed parser closes the table and emits a paragraph at that newline; the projection, rebuilt each push, follows. Same accepted optimism tradeoff as the header. +- **Escaped/code pipes.** Cell tokenization is `splitTableCells` — identical to the committed path, so `\|` and backtick-fenced pipes don't split cells. +- **Width clamp.** Body rows pad/truncate to header width (existing `appendTableRow` behavior); the `table_overflow` warning may fire transiently on the projection for a pathological partial row — harmless (projection is throwaway) but verified in tests not to corrupt committed warnings. + +## Testing + +**partial-markdown — extend `src/__tests__/streaming-table.test.ts`:** +- Open line `|` after a committed header+delimiter → ONE `table`, body row count includes an empty streaming row; no `paragraph` sibling. +- Open line `| x1` and `| x1 | y` → ONE `table` (no second table), last row contains `x1`/`y`, padded to header width. +- Cell growth: pushing `| x1`, ` | y`, `1 |` grows the last row's populated cells. +- Complete-row open line (`| x1 | y1 |`) still appends (existing behavior, regression guard). +- Committed output unchanged: full table document parses identically to 0.5.2 (newline-terminated + finish()). +- Existing chunk-fuzz table corpus entry re-verifies chunk invariance over the new branch. + +**chat (`libs/chat`) — extend `streaming-markdown.table-stream.spec.ts`:** +- While streaming body rows token-by-token, assert exactly one `table` element and zero raw-pipe `

` siblings at every step. + +**Live verification (required gate, Chrome MCP):** +- Run examples/chat locally (`nx run examples-chat:serve`), drive it via Chrome MCP: send a 10+ row table prompt, sample the last `chat-streaming-md` DOM every ~16ms during the stream, and assert: + - `tables === 1` in every frame after the header appears (no second table), + - `pipeParagraphs === 0` in every frame (no raw-pipe paragraph), + - final table has the full row count. +- This is the same harness used to verify #743; the pre-fix baseline there measured `framesTableWithPipe = 2` — post-fix it must be 0. + +## Release & consume + +1. cacheplane: bump `0.5.2 → 0.5.3`, CHANGELOG, PR to protected main (3 required checks), squash-merge, then annotated tag `partial-markdown-v0.5.3` on the merged commit → OIDC publish; verify on npm (CDN may lag ~1 min). +2. ngaf: bump `libs/chat` dep `^0.5.2 → ^0.5.3`, surgical lockfile edit (never regenerate on macOS), chat lint/test/build, the Chrome MCP live gate above, PR, merge on green Vercel. + +## Out of scope + +- Setext headings; non-pipe-fenced tables; live alignment during the delimiter stream (unchanged from 0.5.2's out-of-scope list). +- Any chat-side rendering changes — none needed. diff --git a/libs/chat/package.json b/libs/chat/package.json index 428e579cc..c8c6ea73e 100644 --- a/libs/chat/package.json +++ b/libs/chat/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@cacheplane/partial-json": ">=0.1.1 <0.3.0", - "@cacheplane/partial-markdown": "^0.5.2" + "@cacheplane/partial-markdown": "^0.5.3" }, "peerDependencies": { "zod": "^3.25.0", diff --git a/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts b/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts index e8e0b4f09..cb1282a5b 100644 --- a/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts +++ b/libs/chat/src/lib/streaming/streaming-markdown.table-stream.spec.ts @@ -97,4 +97,17 @@ describe('ChatStreamingMdComponent — streaming table rendering', () => { vi.useRealTimers(); } }); + + it('streams body rows inside the table — no paragraph, no second table (0.5.3)', () => { + host.streaming.set(true); + grow('| A | B |\n| - | - |\n'); + for (const c of ['|', '| x1', '| x1 | y', '| x1 | y1 |', '| x1 | y1 |\n']) { + grow('| A | B |\n| - | - |\n' + c); + expect(el.querySelectorAll('table').length, `one table at ${JSON.stringify(c)}`).toBe(1); + expect( + [...el.querySelectorAll('p')].some((p) => (p.textContent || '').includes('|')), + `no raw-pipe paragraph at ${JSON.stringify(c)}`, + ).toBe(false); + } + }); }); diff --git a/package-lock.json b/package-lock.json index 1b0d13b37..dc93f494a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@angular/platform-browser": "~21.1.0", "@angular/router": "~21.1.0", "@cacheplane/partial-json": "^0.1.1", - "@cacheplane/partial-markdown": "^0.5.2", + "@cacheplane/partial-markdown": "^0.5.3", "@langchain/core": "^1.1.33", "@langchain/langgraph-sdk": "^1.7.4", "@neondatabase/serverless": "^0.10.0", @@ -207,7 +207,7 @@ "license": "PolyForm-Noncommercial-1.0.0 OR LicenseRef-Threadplane-Commercial", "dependencies": { "@cacheplane/partial-json": ">=0.1.1 <0.3.0", - "@cacheplane/partial-markdown": "^0.5.2" + "@cacheplane/partial-markdown": "^0.5.3" }, "peerDependencies": { "@angular/common": "^20.0.0 || ^21.0.0", @@ -7272,9 +7272,9 @@ "license": "MIT" }, "node_modules/@cacheplane/partial-markdown": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@cacheplane/partial-markdown/-/partial-markdown-0.5.2.tgz", - "integrity": "sha512-8ItJlOjihgvIBEZ269XfZHoU+yZ7TDhqNyxopYeJiyaa3ROISwAY2Q/2CAgUHFLDXZtk+9xn1qElntIkcOJjlw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@cacheplane/partial-markdown/-/partial-markdown-0.5.3.tgz", + "integrity": "sha512-XlvjGVsEY7tA3Q4eSY0EiP3FmKkzv5VwFWCmmqvnvf1/dYy+YjQXmDg1mqtFWk4iNyjMMd7aBtGiY+lEuZPmLQ==", "license": "MIT", "engines": { "node": ">=20"