Skip to content

id(web): bullet and ordered lists lose their tight flag in the editor — richSchema declares no tight attr, so every tight list saves as a loose list #1469

Closed

Description

@dezren39

Summary

richSchema in the id web editor declares no tight attr for bullet_list and no tight attr for ordered_list, even though the Rust markdown codec carries tight on both. Node.fromJSON silently drops undeclared attrs on editor load, and the codec's save side defaults a missing tight to false (loose). Net effect: every tight bullet or ordered list opened in the web editor is rewritten as a loose list on the next save — blank lines are inserted between every item. This is the same schema-mirror defect class as #1036 (task_list, fixed) and #1436 (code_block, fixed today in PR #1447); those two fixes mirrored their node types and left the plain list types behind.

Evidence (verified this run, main 1ff34134)

TS side — pkgs/id/web/src/editor.ts:46:

  • The schema is built as addListNodes(basicSchema.spec.nodes, "paragraph block*", "block") and then .update(...) is applied only to code_block (:47) and image (:73); the appended nodes (task_list, table, html_inline, …) include the task_list mirror from id(web): loose task list items fuse on save — task_list hardcodes tight, second paragraph/code block destroyed #1036. bullet_list / ordered_list / list_item are never updated.
  • In prosemirror-schema-list@1.5.1 (the version in pkgs/id/web/package.json), bulletList declares no attrs at all and orderedList declares only {order: {default: 1}}.

Probe — verbatim richSchema definition extracted from editor.ts (sha256 81a60bfd…), run under Bun on the laptop (~/git/fleet/probe-2026-10-27-laneA-lists, log hidden_files/probe-2026-10-27-laneA-lists/out.log in the goal workspace):

bullet tight=true  IN : {"tight":true}            OUT: null            SPEC attrs: []
bullet tight=false IN : {"tight":false}           OUT: null            SPEC attrs: []
ordered order=3 tight=true  IN : {"order":3,"tight":true}  OUT: {"order":3}  SPEC attrs: ["order"]
ordered order=3 tight=false IN : {"order":3,"tight":false} OUT: {"order":3}  SPEC attrs: ["order"]
task_list tight=false (control, #1036)  IN : {"tight":false,"order":null} OUT: {"tight":false,"order":null}  SPEC attrs: ["tight","order"]
code_block params/htmlBlock (control, #1436) IN : {"params":"html","htmlBlock":true} OUT: {"params":"html","htmlBlock":true}  SPEC attrs: ["params","htmlBlock"]

tight is dropped in both directions for plain lists; order survives; the two previously fixed node types survive as controls.

Rust side — pkgs/id/src/web/markdown.rs:

  • Parse emits it: bullet "attrs": {"tight": tight} (:267-268), ordered "attrs": {"order": list.start, "tight": tight} (:257-261).
  • Save requires it: bullet_list arm let tight = json["attrs"]["tight"].as_bool().unwrap_or(false); (:1193) and ordered_list arm the same (:1205) — a missing attr becomes a loose NodeList, which comrak renders with blank lines between items.

So - a\n- b (tight) → editor load (attr dropped) → save → - a\n\n- b (loose). Loose lists survive only by accident, because the save-side default happens to be false.

Why it matters

Tight lists are the overwhelmingly common case. Any document containing one is silently reformatted — every item separated by a blank line — by a single open + save in the web editor, producing noisy whole-document diffs and changing list rendering semantics. It is silent data corruption of the same class the fleet has been fixing node type by node type (#1036, #1436); this is the remaining unmirrored node pair.

Fix direction

Mirror the codec attrs into richSchema following the #1036/#1436 pattern: .update("bullet_list", { attrs: { tight: { default: <see below> } }, … }) and .update("ordered_list", { attrs: { order: { default: 1 }, tight: { default: <see below> } }, … }), preserving the inherited content/group/parseDOM/toDOM fields exactly as the code_block update does. Choose the tight default deliberately (the codec's legacy-doc inference in the task_list arm at :1225 is the house precedent for absent-attr docs); a false default matches the codec's current save-side behaviour for editor-created lists.

Acceptance criteria

  • richSchema.nodeFromJSON → toJSON round-trips bullet_list attrs.tight (true and false) and ordered_list attrs {order, tight} unchanged; order behaviour is not regressed.
  • A tight bullet list and a tight ordered list survive a full open → editor load → save cycle byte-identically (codec round-trip test or editor + codec integration evidence).
  • Regression tests in pkgs/id/web/src/editor.test.ts in the style of the id(web): PR #1024's htmlBlock provenance attr is dropped by the editor schema — HTML blocks still re-fence on editor save (#982 regression path) #1436 code_block block (spec declares the attrs; {params…}-style fromJSON→toJSON cases for both list types).
  • Existing editor.test.ts and web::markdown suites pass in CI; lists created fresh in the editor still save correctly (tight by default for single-paragraph items).

Dedup

Checked open + closed issues for tight / bullet_list / ordered_list schema mirroring this run: #1036 (closed) covered task_list only; #1436 (closed, PR #1447) covered code_block only. #1380 (ordered-list ) delimiter), #1397 (adjacent-list end comments), and #1370 (mixed task/plain lists) are different mechanisms in the Rust codec, not the editor schema.

Activity

  1. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 27, 2026
  5. 115 remaining items

  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Resumed by Lane B worker (queue task 1504): PR #1559 CI re-checked fresh at 2026-10-28T07:52Z. All 30 constituent checks pass, including test-web-unit, test-web-typecheck, web-lint, test-unit and all nix flake sub-checks; the only outstanding check is the aggregate nix flake check job (run 40634718086, job 122689852467), still queued after >5 minutes of polling. Parking again until that aggregate completes; merge immediately on resume when it is green.

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 28, 2026
  9. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added a commit that references this issue on Oct 28, 2026
  11. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions