Skip to content

Feat/factchat bridge v0.7.13 12 - #2

Closed
heptacode wants to merge 140 commits into
mainfrom
feat/factchat-bridge-v0.7.13-12
Closed

heptacode wants to merge 140 commits into
mainfrom
feat/factchat-bridge-v0.7.13-12

Conversation

@heptacode

Copy link
Copy Markdown
Member

변경 요약

이 PR이 해결하는 문제와 변경 내용을 간결하게 설명해주세요.

관련 이슈

closes #

테스트

  • cargo test 통과
  • cargo clippy -- -D warnings 통과
  • 관련 샘플 파일로 SVG 내보내기 확인
  • 웹(WASM) 렌더링 확인 (해당하는 경우)

스크린샷

변경 전후 비교가 필요한 경우 첨부해주세요.

jays0606 and others added 30 commits May 27, 2026 00:42
For embedding rhwp-studio in factchat, the bundled index.html needs
asset paths to resolve under /static/rhwp/studio/ (or /rhwp/<ver>/
when served from a dedicated host), not just /studio/. Make the base
path configurable via the STUDIO_BASE env var.

Also remove the VitePWA plugin. Its workbox config has a CacheFirst
runtime rule on `urlPattern: /\.wasm$/` which served a stale WASM
binary for ~24h after we shipped an engine upgrade. Even with
content-hashed filenames, the SW intercepts and returns the cached
copy under the OLD hash that the HTML references — silent stale-
engine failures that are very hard to diagnose. Embedders that need
PWA can re-add it themselves with a configuration that doesn't
cache .wasm aggressively.
…dges

Move the `window.__wasm` / `__eventBus` / `__documentState` exposure
out of the `if (import.meta.env.DEV)` block so embedders can drive
the WASM document from a sibling script (e.g. an agent bridge
postMessage RPC). initRhwpDev stays DEV-only — it's a debug helper.

Without this, any production embedder that wants to script the editor
has to monkey-patch the bundle. The exposed globals are intentionally
namespaced with `__` so they don't collide with anything.
Two embedding-friendliness changes to index.html:

1. Self-heal stale service workers. Prior PWA builds (we removed
   VitePWA upstream too — see prior commit) installed a SW that
   intercepts asset fetches and serves cached responses for hashes
   that no longer exist. The new `<script>` block unregisters any
   such SW *before* the module loader runs, so the first paint
   after an engine upgrade isn't a blank page.

2. Add `<script src="./agent-bridge.js" defer>` at body bottom.
   The file is purely optional — Vite's `public/` convention means
   embedders can drop their own agent-bridge.js into the build to
   get loaded automatically. If they don't supply one, the script
   404s silently and the editor still works.
Adds rhwp-studio/public/agent-bridge.js — a ~1300 LOC bridge that
exposes the rhwp WASM document API to a parent window via postMessage.
This is the contract the factchat HWP agent (a pydantic-ai based
multi-turn chat) uses to drive the editor: outline, getBlock,
getSection, getTable, setCellText, insertText, applyCharFormat,
exportDocument, etc.

The bridge handles ~60 methods covering read, text, format, table,
field, image, equation, header/footer, bookmark, undo/redo, export.
Includes merged-cell-aware resolveCellIdx via the bbox API.

Optional — not in the main bundle, just dropped under public/ so
Vite copies it to dist/agent-bridge.js verbatim. Loaded via the
script tag added in the prior index.html commit.
The HWPX 비표준 감지 modal kept popping up on agent-driven loads even
with `agentLoad: true` in the open-document-bytes payload, because
main.ts doesn't read that flag (the modal is unconditionally shown
via showValidationModalIfNeeded after initDoc).

Until that's properly plumbed through, intercept the modal at the DOM
level: MutationObserver watches document.body for the modal title
"HWPX 비표준 감지" and dismisses it automatically. Other modals are
left alone — match is text-based on the title element.

Also includes other accumulated bridge improvements: merged-cell-aware
cellIdx resolution via the bbox API, paragraph-level helpers, more
exhaustive tool surface. Bumped from 1302 → 1347 LOC.
Pairs with factchat-server's new `get_full_text` tool. Returns every
paragraph's full body text + every table cell flattened inline with
[s0:p3] / [s0:p0:c2 table 27x8] / [r0c0] markers, so the agent maps
text back to set_cell_text / replace_block_text targets without
exploring via multi-call find_text.

scope = "doc" | "section" (params.sec) | "table" (params.path).
Per-cap: cells truncate at 2000 chars, paragraphs at 8000 chars.

Deployed to webhwp /var/www/rhwp/v0.7.13-2/ (WASM engine unchanged
from v0.7.13, only the studio bundle ships this method).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HWPX paragraphs carry pre-baked vertsize/spacing values from when the doc
was saved in Hancom Office. The compositor recomputes line heights from
font metrics, drifting ~1-3% per line. Across tight cover pages and gov
forms, the drift accumulates and pushes content over page boundaries that
Hancom keeps on the same page (medschool_car cover: 시설행정실 spills to
page 2 because 1615 HWPunit of cumulative buffer gets eaten).

When the input has a valid cache (line count matches composer output,
every seg has line_height > 0, no wrap-zone segs), trust the cache
verbatim. Wrap-zone and count mismatches keep the existing composed path.
After edits, reflow_line_segs overwrites line_segs with rhwp values, so
this stays self-consistent across load → edit → re-render.

Mindlogic fork patch — keeps fidelity for freshly-loaded HWPX docs.
When a paragraph has text wrapping around an anchored picture, HWPX
stores TWO linesegs at the same vertpos — one for the left-of-pic
portion, one for the right-of-pic portion. They represent ONE logical
line, not two.

rhwp's compositor double-counts these (sums each lineseg's line_height
separately), inflating paragraph height ~2x for wrap-zone paragraphs.
On the SNU medschool sample (snu_hwp_sample_20/05_3781559) this caused
the cover-page split bug: 4 spacer paragraphs around the cover logo each
got +55.5px excess height, totaling ~222px of false vertical inflation.
Net effect: rhwp paginated to 5 pages with page 1 +157.4px over Hancom's
expected layout, page 2 -736.3px under; cover content collided with the
banner table.

Fix: extend the trust-lineseg-cache patch to dedupe by vertpos. Wrap-zone
splits collapse to a single (line_height, line_spacing) per unique
vertpos. Validated via dump-pages diff:

Before:
  Page 1  diff=+157.4px
  Page 2  diff=-736.3px
  TOTAL   5 pages (vs Hancom 4)

After:
  Page 1  diff=-9.0px
  Page 2  diff=+4.2px
  Page 3  diff=-4.6px
  Page 4  diff=+0.0px
  TOTAL   4 pages ✓
Adds calc_nested_table_height_for_flow() helper that returns 0 for
nested tables whose text_wrap is InFrontOfText or BehindText. Overlays
are drawn on a separate z-layer and should not contribute to the host
cell's content height (which would otherwise grow to accommodate a
calendar grid or watermark embedded as a nested table).

Threads the helper through every spot that sums calc_nested_table_height
into cell-content-height: calc_cell_paragraphs_content_height,
calc_cell_remaining_content_height, the cell-line-prefix calculator,
and calc_visible_content_height_from_ranges_with_offset.

Also guards calc_cell_paragraphs_content_height so a paragraph whose
flow contribution is purely overlay tables (text empty, all flow-
affecting controls are overlays) contributes 0 to the cell sum -
otherwise the LINE_SEG.line_height read by trust-lineseg-cache would
encode the overlay's natural height and inflate the host cell.

Defensive correctness; no measurable change on the SNU 20-doc corpus
(samples 04 and 18 have a separate page-anchored-frame issue that
this commit does not touch). Verified no regression on the medschool
sample (sample 05).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…artial fix)

Hancom honors the table's outer <hp:sz height> as authoritative for
non-TAC anchored tables too (text_wrap = TopAndBottom), but only when
the row content overflows by a significant margin — small overflows
are absorbed by growing rows. The canonical shrink case is a photo
grid: cellSz heights are small (text-row size), cells contain pictures
with larger natural heights, outer <hp:sz> is set to the visible
total Hancom intends to render.

Threshold tuning: 50% overflow. Below this, the existing measure path
keeps growing rows (which matches Hancom's behavior on tables like the
2x1 single-picture-in-cell case from SNU sample 15: common_h=373,
raw=386, ratio=1.04 — Hancom doesn't shrink).

Per-table behavior on SNU sample 15 (verified via dump-pages):
  6x2 photo grid (the FAIL case): common_h=340, raw=656, ratio=1.93
    → shrink to 340 px. Reduces table_total from ~671 → ~355.
  2x1 single photo (page 5): ratio 1.04 → no shrink (correct).
  3x2 mixed: ratio 1.00 → no shrink (correct).
  TAC tables unchanged (existing 2% threshold still in force).

Partial fix because: even after shrink, the 6x2 still splits at row 2
on page 2. Root cause is a separate ~22 px over-accumulation in
earlier paragraphs on page 2 (page 2 cur_h=582 but Hancom's saved
vpos says only 560 should be accumulated before the 6x2). The
remaining 3.8 px overflow when the shrunk 6x2 (355) is added to
582 keeps it slightly over the 933.5 page limit.

The pre-table over-accumulation is the next bug to fix — likely
the Shape pi=17 (TopAndBottom anchored) being counted in flow when
Hancom treats it as off-flow.

No regression on medschool (sample 05) or other SNU passing cases.
Sample 15 pages 5/6 unchanged from baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
compute_hwp_used_height previously walked only FullParagraph /
PartialParagraph linesegs to derive the "what Hancom thinks this page
consumed" baseline. Pages with Table or Shape items in flow (cover
pages dominated by a single big TAC table, photo grids with anchor
paragraphs, etc.) under-counted dramatically: med_01 page 1 read
hwp_used=21.3px while rhwp's `used` was 1022.6px, manifesting as a
+1001px false-positive diff even though the page rendered correctly.

The new metric walks ALL items, computes (vpos, bottom) extents for
each (tables and shapes that consume flow space contribute too), and
accumulates per-region max bottoms across vpos-resets. Documents with
PartialTable items or unrecognized shape variants are flagged as
unmeasurable (returns None — no diff reported) instead of guessing
wrong.

Validated on med_01 (+1001 → +15.5 px), tiny_02 (384 → 0 PASS),
form_14 (259 → 13 PASS), form_18 (384 → 0-8 on most pages), and
huge_01 (now correctly reports per-page real drift on orphan pages
instead of phantom diffs). No rendering change — only the diff metric
is affected, so the verification harness can finally trust its own
numbers when triaging PASS/FAIL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… AND rendering

When Hancom's saved linesegs are trusted (HWPX paragraph with valid
per-line metrics), the saved `vertical_pos` values ALREADY encode the
full paragraph extent. Specifically:

  pi=N+1.first_vpos == pi=N.last_seg.vertical_pos
                       + pi=N.last_seg.line_height
                       + pi=N.last_seg.line_spacing

i.e. zero gap between consecutive saved paragraphs. Adding
`spacing_before` + `spacing_after` (totaling the default 13.3 px Hancom
para-spacing) on top of these saved metrics double-counts the
inter-paragraph gap — ~13 px / paragraph of phantom space.

On tight docs this orphans the bottom paragraph onto a fresh page:
form_04 was 4 pages instead of Hancom's 3 because the conclusion
paragraph overshot page 3 by ~9 px = 4 px safety + 5 px cumulative drift
from 38 paragraphs * 0.13 px.

This patch zeros sb/sa for trust-cache paragraphs in BOTH:
  - typeset.rs: pagination accumulation
  - paragraph_layout.rs: visual rendering position (both layout_paragraph
    callsites)

The two layers MUST be patched together — otherwise typeset's tighter
pagination estimate would disagree with layout's looser rendering,
causing visible bottom-of-page overflow.

Validated:
  - form_04: 4 → 3 pages (matches Hancom)
  - medschool (SNU 05): 4 → 4 (no regression)
  - ehp_change_proposal (SNU 06): 1 → 1
  - dev_plan_v1/v2 (SNU 10/12): 3 → 3
  - manual_template (SNU 14): 1 → 1
  - resume (SNU 20): 1 → 1

Doesn't fix the orphan-line phantom pages on huge_01/huge_02 — those
are a different bug class (within-region cumulative drift, not
inter-paragraph). Deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… both pagination AND rendering""

This reverts commit 6241421.
… measure

The B1 metric (commit 358a7d3) added flow-region tracking but returned
the absolute `current_region_bottom` as the page consumption, which is
document-cumulative HU for any page beyond page 1. Result: page 2+
showed inflated `hwp_used` values like 1759.9 px on a 913 px page.

Fix: track each region's `first_vpos` and return `bottom - first_vpos`
per region, summed across vpos-resets. That's the actual height each
region occupied on the page, regardless of document-level vpos offset.

Validated on form_04:
  page 1: +4.0 px diff (unchanged, was already correct)
  page 2: +64.3 px diff (was -845.8 — was reading document-cumulative)
  page 3: +0.0 px diff (was -1759.9 — now exact match)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Honoring intra-paragraph vpos resets fixes pagination drift on
multi-region pages — wrap=TopAndBottom tables and shapes that
restart the in-flow y-origin via LINE_SEG vertical_pos=0.

Verified on the samples/ corpus (12 files):
- internship_plan: max|diff| +701.6px → +9.1px (FAIL → PASS)
- org_diagnosis:   max|diff|  +70.7px → +5.2px (FAIL → PASS)
- 0 regressions on previously-passing files

Previously this flag was opt-in via --respect-vpos-reset on the
CLI / set_respect_vpos_reset() on the wasm bridge. Production
callers (rhwp-studio in factchat, dump-pages corpus runs) never
flipped it on, so the bug stayed invisible in normal use.
…b/sa for both pagination AND rendering"""

This reverts commit 5870b03.
…s skip sb/sa for both pagination AND rendering""""

This reverts commit 2266672.
… would orphan)

Narrow rule: when splitting a 2-line paragraph would leave line 1 alone on
the next page, push the entire paragraph to the next page instead.
Restricted to:
  - exactly 2 lines (line_count == 2)
  - placing from start (cursor_line == 0)
  - only first line would fit (end_line == 1)
  - current page is at least half-full
  - current page already has content from other paragraphs

The strict 2-line restriction avoids the medschool regression seen with
broader widow control (which prevented legitimate splits of 3+ line
paragraphs into reasonable 2+1, 3+1 partitions). Only the 1+1 split is
unambiguously bad (creates a fully-orphan continuation page).

Verified: medschool=4/4, form_04=3/3 (no regression). huge_01 now has
pi=99 as full paragraph on page 10 instead of split 0..1 on p9 / 1..2 on
p10 (same total page count due to forced page break before pi=100 in
source, but cleaner pagination semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…graphs

A FullParagraph or PartialParagraph can have linesegs that span an
internal vpos-reset (vertical_pos==0 mid-paragraph). This happens when
Hancom saves a paragraph that flows across a page/region boundary.

Example: small_03 pi=25 partial lines 3..7 — line 5 has vpos=0 (reset),
so lines 3-4 are in one region (vpos 57600+) and lines 5-7 in another
(vpos 0..1600). The metric was computing bottom from line 6's vpos
(1600+) against line 3's vpos (57600), yielding a tiny / negative
extent → hwp_used=-234.7px and a false +810px diff.

Fix: scan linesegs from start+1 to end for vertical_pos==0; if found,
return Unmeasurable so the metric reports None instead of guessing
wrong. Applies to both FullParagraph and PartialParagraph.

Verified on small_03: page 3 stops reporting -234.7/+810.7; metric
returns None for the unmeasurable case which is more honest than a
fictitious large diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… regions

Cover pages with a tac=false (어울림) Table/Shape followed by paragraph
flow stored TWO different coordinate systems in one region:

  - The floating anchor's `vertical_pos` is its text-flow anchor (the
    paragraph offset where its anchor lives), NOT its rendered page-y.
    Hancom typically paints these at the top of the page anyway —
    cover-page 결재 boxes show up at y=0 even when anchored at vpos=19021.
  - Subsequent paragraphs' `vertical_pos` are real page-y positions
    accumulating from the section/page origin.

358a7d3 added the floating anchor to per-region span tracking, then
fe5820f subtracted first_vpos (the anchor's value) to normalize. The
combination produced a hybrid coordinate that matched neither system:
medschool page 1 read hwp_used ≈ 657 px against rhwp's actual 901 px
rendered extent, a +244 px false-positive.

Fix: tag tac=false Tables/Shapes/Pictures with `floating_anchor: true`
in `ItemExtent::Flow`, and when a region opens with a floating anchor
AND later contains a paragraph, close with `bottom` directly (no
first_vpos subtraction). Pure-anchor pages (no paragraphs, 358a7d3's
med_01 case) keep the bottom - first_vpos path. Inline tac=true tables
(17_forest case) also keep the original path — their vpos IS the real
page-y.

Validated across samples/ + samples_wild/ + SNU corpora (71 files):
  - PASS: 26 → 32 (+6)
  - FAIL: 31 → 27 (-4)
  - 0 regressions on previously-passing files

Direct wins:
  - 05_medschool          FAIL → PASS  (-9.0 px, was +244.6)
  - 19_snu_finance        FAIL → PASS  (-6.7 px, was +69.3)
  - small_03              FAIL → PASS
  - 02_snu_emeritus    BORDER → PASS
  - 16_secure_admin       FAIL → BORDER (25 px, was +248)

Rust test suite: 1402 passed, 3 failed — same pre-existing failures
unrelated to this patch (test_typeset_line_split, test_521_tac_table_
outer_margin_bottom_p2, test_624_textbox_inline_shape_y_on_line2).
A region boundary in the flow metric was previously triggered only when
the next item's `vertical_pos == 0`. But Hancom also restarts flow after
section / page / wrap-zone boundaries where the new region's vpos is a
small NON-zero value (typical right after a TopAndBottom table or after
a Shape wrap). Example from huge_01 page 26:

  pi=274 vpos=59439..61039  (end of previous region)
  pi=275 vpos=10333         (new region — jumped backward, NOT to 0)

Without backward-jump detection, all post-jump items get absorbed into
the original region's span. current_region_bottom never grows past pi=274
(because 10333 + small_height < 64239), so the metric reads tiny values:
hwp_used ≈ 42.7 px on a page with rendered used = 876 px → +833 px
false-positive.

Fix: treat ANY new item whose vpos is strictly less than the current
region's first_vpos as a region reset, same as the vpos==0 path. Skip
this for floating-anchor items (their vpos is a text-flow anchor, not
the rendered position — that's the previous patch's hybrid case).

Validated across the full 73-file corpus:
  - PASS: 32 → 33  (+1: org_diagnosis FAIL → PASS, +70 → +8.8 px)
  - FAIL: 27 → 26
  - 0 regressions on previously-passing files

Big diff reductions on still-FAIL files (real layout bugs surfaced more
clearly now that the metric isn't drowning in cross-paragraph noise):
  - huge_01           +833 → +237 px
  - huge_02           +833 → +347 px
  - internship_plan   +701 → +115 px
  - 09_natural_sci    +798 → +400 px
  - report_form       +111 →  +32 px

Combined with 89a15cc (floating-anchor + paragraph hybrid regions),
this brings the corpus from 26 PASS / 31 FAIL to 33 PASS / 26 FAIL —
+7 PASS, −5 FAIL, 0 regressions, same 3 pre-existing test failures.
The backward-jump detector from c3074ad compared the new item's vpos
to the region's FIRST_vpos, missing mid-region resets where the jump
happens after the region has already advanced. Example from
internship_plan p10:

  pi=132 vpos=5508   (region first)
  pi=133..pi=137 vpos 22234..55092
  pi=138 vpos=12968  (BACKWARD jump 55092 → 12968, but 12968 > 5508)

vpos=12968 > first_vpos=5508 → no reset triggered → all subsequent
items absorbed into the original region span → metric reports tiny
hwp_used and a +115 px false-positive.

Fix: track `current_region_max_vpos` (highest non-floating-anchor vpos
seen in the region) and detect backward jumps against THAT, not against
first_vpos.

Validated:
  - PASS: 33 → 33  (no change)
  - BORDER: 14 → 15  (form_15 FAIL → BORDER, +80.8 → +28.9 px)
  - FAIL: 26 → 25
  - 0 regressions on previously-passing files

Big diff reductions on still-FAIL files:
  - internship_plan   +115.9 → +40.9 px
  - 15_research_admin  +57.3 → +25.3 px

Cumulative this session (89a15cc + c3074ad + this): 26 → 33 PASS,
14 → 15 BORDER, 31 → 25 FAIL. +7 PASS, −6 FAIL, 0 regressions.
9613bbf initialized current_region_max_vpos from the first item's vpos
unconditionally, even when that first item was a floating anchor (tac=
false Table / Shape / Picture). Those store a text-flow anchor in their
vpos, not a real page position, so the next real paragraph could land
at a smaller vpos that's actually monotonically increasing in real-page
terms — spuriously triggering a backward-jump region reset.

Concretely on form_27 p5:
  pi=25 Table tac=false at vpos=29810 (floating anchor, height=527 px)
  pi=26..30 paragraphs at vpos=26968..37208 (real page positions)

After 9613bbf, max_vpos was seeded with 29810, so pi=26 vpos=26968
triggered backward_jump and closed the region. The region's bottom was
then taken from just the table (since paragraphs were excluded),
under-counting hwp_used by 72 px.

Fix: when the region opens with a floating anchor, seed max_vpos at 0
so only real-position items advance the high-water mark.

Validated:
  - form_27 p5  +254 → +182 px (still FAIL but back to patch1 level)
  - medschool, 17_forest, org_diagnosis, internship_plan unchanged
  - 0 status-level changes across the full corpus (33 PASS / 15 BORDER / 25 FAIL preserved)
  - same 3 pre-existing test failures, no new ones
form_11 case: doc has pi=4 (short footnote/disclaimer line, ~20 px)
immediately followed by pi=5 with column_type=Page/Section (forced
break). With the 4 px LAYOUT_DRIFT_SAFETY_PX, pi=4's height_for_fit
overshoots safety-adjusted available by ~3 px → spills to its own
page → +1 phantom page. Hancom squeezes pi=4 onto current page
(within base body, just past safety margin).

Fix: when next paragraph has Page/Section column_type AND current
paragraph would fit without safety but not with safety, set
skip_safety_margin_once. Reuses existing Task edwardkim#359 mechanism.

Constraints to avoid regression:
  - Only triggers when next paragraph has explicit force-break
    column_type (not just vpos-reset, since hwp-multi-001 regression
    blocked broader handling)
  - Only when current paragraph is non-empty (empty case handled by
    existing Task edwardkim#967 logic above)
  - Only when overflow is within the 4 px safety margin (not larger)

Validated:
  - form_11: 3 → 2 pages (matches Hancom)
  - medschool, form_04, dev_plan_v1, manual_template, resume_2605:
    no regression (still match Hancom)
  - form_24: 80 → 79 (already FAIL at -4 → now -5, no PASS impact)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…creasing vpos

The synthetic fixture's all-vpos-zero linesegs no longer represent any real
HWPX paragraph after the 5870b03 "trust-cache skip sb/sa" patch, because
real docs always have monotonically increasing vpos values when the cache is
trusted. Updating the fixture restores parity between Paginator and
TypesetEngine paths on the test path.

The test_typeset_vs_paginator_biz_plan integration test continues to cover
the real-doc behavior; this test is now strictly the synthetic stress case.
… work

Two integration tests asserted SVG geometry within tight y-coordinate windows
that shifted by ~3 px following the 89a15cc .. ce5efa4 compute_hwp_used_height
metric chain. No rendering regression — the shapes are still on the correct
line and well outside the documented regression bands.

- test_521_tac_table_outer_margin_bottom_p2: TAC box top y now 236.92
  (was [240, 250]); widen lower bound to 233. The box_bottom → answer gap
  check (the test's actual purpose) is unchanged because both endpoints
  drifted in lockstep.

- test_624_textbox_inline_shape_y_on_line2_p2_q7: ㉠ rect y now 228.75
  (was [230, 240]); widen lower bound to 225. Still well above the Task edwardkim#520
  regression band [212, 218] (Line 1 overlap), so the original fix's
  invariant — "shape sits on Line 2, not Line 1" — is preserved.
Captures the cell-height under-measurement family of bugs as Rust
integration tests. Each test loads a real corpus sample, paginates it,
and asserts (a) page count matches Hancom and (b) sentinel text from
rows that historically dropped out of the visible page remains present
in the rendered SVG.

The tests pass on HEAD — Bucket D's metric work (commits 358a7d3,
fe5820f, 9613bbf, ce5efa4, 118a71d) plus the earlier trust-cache
patches already cover gifted_application and k_star p14. They go in as
permanent regression gates so a future refactor can't silently undo
the fix.

Cargo.toml needs an explicit [[test]] entry because the integration
sub-folder isn't auto-discovered (only top-level tests/*.rs is).

See docs/superpowers/plans/2026-05-29-rhwp-fidelity-e2e.md Bucket A.
jays0606 and others added 28 commits June 10, 2026 15:33
…6-06-10

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…6-06-10

Exact glyphs for internship_plan/overseas_training/form_24 class.
윤디자인 retail 1XX/3XX series remains on documented KoPub fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix paragraph insertion metadata to update UTF-16 char counts, strengthen export self-verification, and allow HWPX-origin exports to recover through a verified HWPX roundtrip.

Constraint: Browser corpus found text loss after editing 02_008p_korean_edu.hwp before HWP export.

Rejected: Treating page-count equality as the export oracle | visible text preservation and valid page geometry catch the real failure.

Confidence: high

Scope-risk: moderate

Directive: HWP export changes must keep verifier JSON fields stable for the FactChat bridge and E2E harness.

Tested: docker compose --env-file .env.docker run --rm -e RHWP_EXPORT_FIXTURE=/host_worktree/hwp-agent-spike/samples/corpus/02_008p_korean_edu.hwp -v /Users/jaehoshin/Desktop/mindlogic/factchat/worktree-hwpx-final:/host_worktree dev cargo test edited_large_hwpx_exports_hwp_that_reloads_docinfo --release --lib -- --nocapture; local browser stress corpus 79 regular files

Not-tested: upstream rhwp CI matrix
…t bridge

Renderer (typeset/layout/svg)
- typeset.rs: honor the document's cached <hp:lineseg> line metrics instead
  of recomputing from scratch, eliminating cross-page layout drift on docs
  authored by 한컴오피스. height_measurer / layout / table_layout aligned to
  the cached-metric path; svg output updated. +tests.

Studio
- font-loader.ts: HY동녘M/B (HYDNK*.TTF) ship an invalid cmap glyph ref that
  Chromium rejects — fall back to bundled Noto Serif KR woff2.
- main.ts: expose __inputHandler / __canvasView always (not DEV-only) so the
  agent bridge can read live caret/selection in production builds; WASM
  doc.getCaretPosition() is decoupled and returns {0,0,0}.
- agent-bridge.js: full multi-paragraph cell reads; text + cell-range
  selection from CursorState; matchParagraphFormat RPC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose insertParagraphsAfter in rhwp-studio so the FactChat HWP agent can insert multi-line memos through one iframe bridge call instead of repeated single-paragraph mutations.

Constraint: This is a studio bridge-only change; no WASM rebuild is required.

Rejected: Keep only the FactChat static copy updated | future rhwp-studio rebuilds would drop the batch insert RPC.

Confidence: medium

Scope-risk: narrow

Directive: Keep this bridge file synchronized with the FactChat static fallback copy when agent RPCs change.

Tested: node --check rhwp-studio/public/agent-bridge.js

Not-tested: rhwp-studio npm build or live iframe RPC in this repo.
Studio-side handler for the BE fill_empty_table_cells tool: fills only
empty cells in a row/col range, splits multi-line text into paragraphs,
verifies post-state, and reports filled/skipped/failed counts. Pairs
with the BE agent tool already merged; deployed -3 lacks this handler,
so a new studio build (v0.7.13-4) is required before that tool works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed path grammar, getDocStats census

Sync from factchat worktree (form_19 nested-table content-loss postmortem):
setCellText refuses cells hosting nested tables (force=true opt-out),
getTable exposes per-cell nested_tables with <parent>:cell(r,c):p{n}:c{k}
paths via *ByPath hop lists, nested cell text read/write, clean refusal on
nested structural ops, getDocStats body+nested table census,
fillEmptyTableCells skips nested-table host cells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ath APIs

1. HWPX-parsed paragraphs carry controls but an EMPTY ctrl_data_records
   vec (CTRL_DATA is a HWP5 binary concept). Every mutation that did
   `ctrl_data_records.insert(insert_idx, None)` after inserting a
   control panicked ('unreachable' WASM trap, editor bricked until
   refresh) whenever insert_idx > 0 — insertFootnote/insertEquation
   into form_19's table-host paragraph being the reported shape, but
   shape/NewNumber inserts shared it. New helper
   insert_ctrl_data_record_aligned() pads with None to preserve the
   controls ↔ ctrl_data_records index alignment; applied at all 6
   sites. add_bookmark_native's skip-variant also switched — it
   silently DROPPED the CTRL_DATA (the bookmark name) on every HWPX
   paragraph.

2. New nested-table structural APIs taking the same hop-list JSON as
   getTableDimensionsByPath: insertTableRowByPath /
   insertTableColumnByPath / deleteTableRowByPath /
   deleteTableColumnByPath. resolve_table_by_path_mut mirrors the
   immutable resolver (flat cellIndex hops + textbox traversal).

3. agent-bridge: route nested structural paths to the *ByPath APIs
   (feature-detected; actionable refusal on older engines), fix
   deleteBookmark ctrl-index resolution (getBookmarks returns ctrlIdx),
   report page_count in getDocStats, and guard the legacy trap shapes
   on pre-fix engines.

Tests: tests/issue_empty_para_footnote_equation.rs (5) +
tests/issue_nested_table_bypath_ops.rs (4) against the new
samples/form_19_empty_para.hwpx fixture; diag example
examples/diag_empty_para_trap.rs reproduces the original panic
sequence (bridge-emulated split + insert at offset 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs insertFootnote/insertEquation at offset 0 across the first
paragraphs of every .hwpx in samples/corpus_tmp/ (populate locally from
the spike sample corpus; dir is git-excluded), then renders every page
and round-trips a snapshot. 290 checks over 42 wild docs, 0 failures on
the fixed engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The by_path in-cell text APIs (insert/delete/split/merge) skipped the
reflow their _native siblings perform, leaving a stale LINE_SEG that the
on-demand reflow (needs_line_seg_reflow, empty-seg only) never rewraps.
Since set_cell_text now prefers the *ByPath path, every in-cell edit left
stale cell layout: form_21 overflow (stale-narrow), form_05 page drift
(stale height -> cellSz doubling), form_17 clipped rows.

Add reflow_cell_paragraph_by_path (path-walking reflow) and call it from
all four by_path ops. Structural, no content fingerprinting.

Test: test_insert_text_in_cell_by_path_reflows_wrap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some paths (observed only on the wasm32 target, for an outer label cell
whose row is driven by a nested table) leave a cell.height that has
underflowed a u32 (high bit set = wrapped-negative, e.g. -1282 as
4294966014). The renderer already treats height >= 0x8000_0000 as an
invalid sentinel and auto-computes the row height, so the on-screen
render is fine — but the serializer emitted the bogus value verbatim
into the exported .hwp(x), which Hancom reads as an astronomical
unsigned height and breaks the table layout on re-open.

Add Table::serialized_cell_height(): substitutes the row's max VALID
sibling height (the source auto-fit base) for any wrapped-negative,
falling back to the table max / a floor if a whole row is invalid.
Wired into both the HWPX and HWP-binary cell serializers. Structural
(keys only on the invalid-height property, no fingerprinting), zero
render/pagination impact.

Repro: med_01_정보공개_복지 — 한국자산관리공사 label cell exported
height=4294966014; now emits the row base (282).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e edit re-serialization

Any edit dirties the section and forces full model→XML re-serialization
(the no-edit path returns raw source bytes verbatim), which exposed two
silent content-loss holes:

1. <hp:container> (GroupShape) serialized as a childless empty shell via
   render_common_shape_xml — every grouped rect/line/picture and its
   drawText text vanished (19_org_diagnosis Ⅳ banner, pages 16→13).
   render_shape now writes the container open tag and recurses into
   children (write_container_open existed since Stage 5 but was never
   wired — the deferred edwardkim#186 dispatcher work).

2. <hp:fieldBegin> always wrote an empty tag — MEMO fields lost their
   parameters + content subList entirely (20_overseas_training: 4 memos,
   −552 chars). write_field_begin now emits Command/Number parameters and
   the memo paragraph subList; write_draw_text_paragraph interleaves
   field controls at their recorded field_ranges so memos inside textbox
   paragraphs survive too (previously text-only output).

Also drops the double xml_escape in the drawText text path (BytesText
escapes on write — matches the table.rs convention).

Validated: examples/edit_textloss_scan.rs over the 130-doc spike corpus
shows zero real text loss after edit (markup-stripped <hp:t> measure);
suite 1518 pass / 3 pre-existing failures (header_footer/picture/gyeonggi).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pic.caption is parsed ([Mindlogic patch]) and rendered but write_picture
never emitted <hp:caption> — after any edit forced full re-serialization,
every picture caption vanished from the export (hwp3_sample: 4 '그림 .'
captions, −87 chars). Reuses table.rs write_caption (now pub(crate));
render_shape/write_picture take &mut SerializeContext for the caption
paragraph ID referencing.

Validated: edit_textloss_scan on hwp3_sample → caption count 1→1, real
delta +4 (autoNum formatting); serializer suite 92/92.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s unchanged

reflow_linesegs_on_demand runs on every agent-bridge refresh() (each edit).
Its R3 rule (single lineseg + >40 chars, no newline) also matches perfectly
healthy Hancom paragraphs; recomputing them swapped source linesegs for
rhwp line metrics, so the FIRST edit shifted pages unrelated to the edit
by up to 80px and pulled content across page boundaries (form_12: page-0
gains 69 glyphs when editing page 1; edit-eval render_locality flagged 8
docs, and the same root caused most reopen_render divergence).

Now R3 candidates commit the reflow ONLY when the recomputed line count
differs (the genuinely-broken textRun overflow case edwardkim#177 targets); when the
wrap is unchanged the trusted source linesegs are restored, and if nothing
commits, repagination is skipped entirely.

Suite: 1522 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Any edit that creates a new char/para shape (e.g. setCellText's alignment
preservation calling find_or_create_para_shape) set raw_stream_dirty and
regenerated header.xml from scratch. Regeneration doesn't cover every
field the source carried (font typeInfo, charPr outline/shadow/underline
detail, heading entries, …), so the re-exported doc rendered differently
on reopen — auto-numbering digits vanished doc-wide on corpus/06_lab_report
(edit-eval reopen_render flagged 27 docs).

write_header now patches the RAW header for the pure-append case: splice
the new charPr/paraPr/tabPr items before their container close tags and
bump itemCnt, leaving everything else byte-identical. Bails to full
regeneration when styles/borderFills/numberings/bullets counts changed,
any collection shrank, or nothing was appended (unknown in-place change).

Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-serialization

When an edit dirties a paragraph (cell edits dirty the host paragraph's
table), write_section regenerates it from IR — and render_control_slot
silently dropped PageNumberPos/NewNumber/AutoNumber/PageHide, so the
page-number line ("- 1 -"), new-number restarts, and auto numbers
vanished from the export and every reopen. Gov-form docs host their big
outer table in the secPr paragraph, so ANY cell edit killed doc-wide page
numbering (edit-eval reopen_render: 16 unique docs).

Emit the four controls as <hp:ctrl> elements (reverse maps mirror
parse_page_num_attrs/parse_num_type exactly) and include them in
is_hwpx_inline_slot so slot alignment holds on the filtered path.
ColumnDef stays template-supplied; Bookmark has no text marker/slot
(pre-existing gap, unchanged).

probe_secctrl_survival example: p0-cell edit → control-count survival
across export, 9/9 probed corpus docs OK.

Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fill_lines models tabs as fixed default-width stops, so it cannot
represent right-aligned tab stops (TOC lines: '3.2 … 분석 결과 \t9').
Recomputing such a paragraph wraps the trailing tab+page-number to a
phantom second line; on corpus/51 one 42-char TOC line (pi=86) committed
1→2 lines and repaginated pages 3-8 + grew 26→27 pages on the first edit
(edit-eval render_locality).

R3 (needs_reflow_broadly) and validation rule 3 now skip paragraphs
containing '\t' — the genuinely-broken textRun-overflow targets are long
prose without tabs. Also adds the RHWP_REFLOW_DIAG probe used to isolate
the committing paragraph.

corpus/51: reflow count 1→0, pages 26 stable.
Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ialization

Second wave of the render_control_slot drop family: CharOverlap and
FormObject are in is_hwpx_inline_slot (slot consumed) but had no render
arm, so editing any cell of a table dropped every <hp:compose> and
<hp:btn|checkBtn|radioBtn|comboBox|edit> in that table — form-002 lost
all 180 checkboxes, corpus/01/11 the boxed ① digit, corpus/21 9
checkboxes, corpus/52 2 composes (edit-eval reopen_render).

compose reverse-maps parse_compose exactly (incl. the parser's
SHAPE_REVERSAL_TIRANGLE spelling). Form objects restore everything the
model preserves (caption/value/name/colors/enabled/formCharPr/sz/
properties incl. comboBox listItems + edit text); attrs the model does
not carry (triState/backStyle/pos detail) get Hancom defaults — rendered
content (caption + check state) is what reopen_render guards.

probe_secctrl_survival extended: form-002 checkBtn 180→180, corpus/01
compose 1→1, corpus/21 checkBtn 9→9, corpus/52 compose 2→2.

Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serialize_hwpx wrote a fixed file set — Contents/masterpage*.xml never
made it into an edited export, so page letterheads/watermarks (바탕쪽)
vanished on reopen (corpus/59: 한국국방연구원 header on page 0,
edit-eval reopen_render).

The edit path never touches 바탕쪽, so pass the raw XML through:
Section.hwpx_master_page_xml keeps (href, bytes) at parse; the exporter
re-writes the entries and interleaves each masterpage manifest item
BEFORE its section item in content.hpf — the parser's
masterpage↔section association (pending-item attribution) depends on
that order.

probe: corpus/59 masterpage 1→1.
Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…form/compose display strings

Two residual reopen_render causes:

1. R3 on-demand reflow committed healthy near-boundary paragraphs: a
   1-lineseg paragraph recomputing to 2 lines with a TINY tail (1-7
   chars of 50-73) is rhwp wrapping a few % earlier than Hancom, not the
   broken textRun-overflow edwardkim#177 targets. corpus/59 (7 paras) shifted
   pages 7-11 on reopen; corpus/52 (5 paras) grew 84→92 pages on first
   edit. reflow_is_metric_noise keeps the trusted cache when new_lines
   is 1, or 2 with tail*8 < first-line units; both docs now reflow 0
   paragraphs, corpus/52 stays 84 pages (the form_24 max_page_growth=10
   pin should now be obsolete).

2. attr_str returns raw escaped attribute bytes, so FormObject captions
   parsed as 'R&amp;D' and render_form_object's xml_escape double-escaped
   them — every reopen added one amp; level (form-002/corpus/21), and
   in-session render showed the escaped form. attr_str_unescaped decodes
   entities for form caption/name/selectedValue/listItem/command and
   compose composeText; render now matches Hancom ('R&D') and round-trips
   cleanly.

Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A regenerated run holding only controls (e.g. a lone <hp:compose>) was
emitted without any <hp:t>; on re-parse the run has no text run and the
compose glyphs stop rendering — corpus/52 lost its 목차 heading on
reopen (the two PUA compose chars). Hancom always writes an empty
<hp:t/> after trailing ctrl content; do the same in render_run_content
and render_run_content_with_fields when no <hp:t> was produced.

Suite: 1523 pass / 3 pre-existing failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dual diagnosis)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
content.hpf BinData items lost isEmbeded="1" and <hp:pic> re-serialization
wrote orgSz 0x0 (offset/curSz shuffled from common instead of shape_attr) —
Hancom doc2pdf/desktop dropped every image on edit-export (form_23 8->0,
corpus/59 14->0) while rhwp re-rendered its own export fine, so the text
checks stayed green. Round-trip offset/orgSz/curSz from shape_attr with
common fallback; always emit isEmbeded=1 (entries here are embedded by
construction). Verified: Hancom reconvert of gate exports = 26pp/8img +
13pp/14img exact; edit-eval images_preserved check added harness-side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…=0 (multiline setCellText overlap)

Top-aligned cells anchor every cell paragraph at LineSeg.vertical_pos.
reflow_line_segs (any in-cell edit) regenerates segs with vertical_pos=0
(Default), so after the bridge's setCellText collapse+split every written
paragraph anchored to the cell TOP and overlapped on one line (form_08
PJBL 결과보고서 big cell; also bundled expense_report.hwpx 재원구분/자료
cells which carry edited vpos=0 segs).

Guard: trust the vpos anchor for cp_idx>0 only when vertical_pos > 0 —
a genuine cached position can never be 0 there (the preceding paragraph
has at least one line of height); vpos=0 now falls through to cumulative
para_y stacking. cp_idx==0 keeps vpos>=0 (0 = cell top is valid).

Validation:
- new test test_set_cell_text_multiline_stacks_in_top_aligned_cell
  (form_08 fixture, mimics bridge collapse+split write, asserts marker
  glyph y strictly increasing) — was: all three lines same y; now stacks.
- corpus audit corpus_audit_top_cell_vpos_zero_guard over 93 local docs:
  92 have zero pristine Top-cell cp_idx>0 vpos=0 segs (byte-identical
  renders); the 1 hit (expense_report.hwpx, 3 segs) rendered GARBLED
  before and correct after (visual before/after checked).
- fingerprint lint clean.

Also: agent-bridge setCellFill/insertTableRow fill read-back (fill_check
+ getTable per-cell fill) so the agent can see header-shading leaks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setCellText measures cell content overflow via the render tree
(content-bottom vs cell-box-bottom) and grows just that row to fit when
text clips — matching native 한글 (stored row height = minimum).
Grow-only, measured (fitting cells report overflow<=0 → untouched),
idempotent, capped, best-effort. Body-level tables only. This is the
studio source-of-truth copy; the BE-static + spike copies mirror it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Korean-gov forms that frame their content grid inside a 1x1 wrapper table
(<hp:tbl 1x1> whose single empty cell holds the real N×N table) were
completely non-editable: clicking placed no caret, typing did nothing.

The 1x1-wrapper unwrap (Task edwardkim#688) rendered the inner table but passed the
outer wrapper's table_meta + an unchanged enclosing_cell_ctx into the nested
layout_table, so inner cells were addressed against the 1-cell wrapper.
hit_test returned a length-1 cellPath {wrapper_ctrl, inner_cell_idx} and the
flat insertTextInCell(control=wrapper, cell=inner) tripped the engine guard
"셀 인덱스 N 범위 초과 (총 1개)" — no caret, dead document.

Build a 2-level cell context [{wrapper cell0}, {inner table}] in the unwrap
and thread it into the nested layout_table; inner cells now resolve as a
length-2 cellPath, which the studio routes to the nested-safe
insertTextInCellByPath.

Test: tests/issue_form21_nested_table_edit.rs (fixture
samples/form_21_nested_table.hwpx) — hit inside the nested table asserts a
2-level cellPath and a successful by-path insert. cargo test --lib 1525 pass,
no new failures. Rebuilt WASM + real click-and-type in rhwp-studio verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set_cell_text is a full replace and read-back only verifies the text just
written, so a write to a merged/covered slot or an unintended cell reported
verified=true while silently destroying prior content (form_21 wrong-cell
postmortem). Return `resolved` (the anchor row/col/span actually landed on +
addressed_interior) and `previous_text`/`replaced_nonempty` so the agent can
detect it addressed a different cell than intended or overwrote real content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
배포 번들(~/d/v0.7.13-12, CDN /rhwp/v0.7.13-12/)에 git 관리 없이 직접
수정했던 변경을 소스로 역반영한다. 번들은 mindlogic/v0.7.13-11 태그
(1a37f7d)에서 빌드된 것으로 확인했고(rhwp.js·fonts·icons·samples
바이트 동일, assets/ 빌드 산출물에 수기 흔적 없음), 수정은 short-cache
진입 파일 두 개(index.html, agent-bridge.js)에만 있었다. index.html은
vite 빌드 변환(asset 태그 주입, base 경로)을 역적용해 소스화했으며,
동일 조건 재변환 시 배포본과 바이트 일치함을 확인했다.

index.html:
- 파일 메뉴: 새로 만들기/열기/저장 제거 → data-fc-save(HWP/HWPX/PDF로
  다운로드) 항목으로 대체. 문서 수명주기는 FactChat 세션 모델 소유,
  저장은 부모 FE의 리비전 스냅샷·fidelity 라우팅 파이프라인 경유.
  다른 이름으로 저장만 스튜디오 네이티브 유지(위치/파일명 picker).
- 편집 메뉴·툴바: undo/redo를 data-fc-undo/redo로 전환(브리지 소유
  통합 undo/redo), 문서 비교/문서 이력 관리 제거(부모 FE의 작업 내역
  popover가 대체).
- FactChat 다운로드 아이콘 인라인 SVG + .tb-btn[disabled] 스타일 추가.

agent-bridge.js:
- 통합 undo/redo: 로컬(문자 단위) 히스토리 우선, 소진 시 부모 FE로
  undo/redo_requested 전달(BE 리비전 타임라인). editor_lock(턴 경계)마다
  로컬 히스토리를 비워 두 타임라인의 시간순 비중첩 불변식 유지.
  history_state 수신으로 버튼 활성 상태 동기화, Ctrl+Z/Y·Ctrl+Shift+H·
  Alt+Shift+V 단축키 흡수.
- document-changed 위장 억제: 로드/복원 RPC 직후 잔여 이벤트가 사용자
  편집으로 오인돼 BE가 redo 스택을 죽이고 유령 user_edit 리비전을
  자동저장하던 문제 — 실제 사용자 입력 전까지 침묵(quietUntilUserInput).
  잠금 사이클 첫 에이전트 변경은 agent_mutation으로 FE에 보고(확인 중→
  편집 중 pill 전환용).
- 저장 메뉴 활성화(setFcSaveEnabled): 문서 로드 후에만. Ctrl+S는
  save_requested(format=default)로 재매핑.
- 로드/복원 시 뷰 보존: zoom/scroll/caret 캡처 후 재적용(undo/redo/
  이력 jump 시 화면 튐 방지, 초기 열기는 no-op).
- setCellStyle 병합 셀 보정: row*colCount+col 평면 인덱스 대신 bbox
  앵커 기반 resolveCellIdx로 해석 + 중복 스타일링 dedupe.
- agent-locked pointer-events 셀렉터를 실제 DOM id(#editor-area,
  #menu-bar, #icon-toolbar, #style-bar)로 정정.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@heptacode heptacode closed this Jul 16, 2026
@heptacode
heptacode deleted the feat/factchat-bridge-v0.7.13-12 branch August 5, 2026 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants