Skip to content

Lua API Pandoc parity: conformance harnesses + first six fix strands (bd-grkrb9nj)#393

Merged
cscheid merged 26 commits into
mainfrom
feature/bd-grkrb9nj-lua-api-pandoc-parity
Jul 14, 2026
Merged

Lua API Pandoc parity: conformance harnesses + first six fix strands (bd-grkrb9nj)#393
cscheid merged 26 commits into
mainfrom
feature/bd-grkrb9nj-lua-api-pandoc-parity

Conversation

@cscheid

@cscheid cscheid commented Jul 13, 2026

Copy link
Copy Markdown
Member

Epic bd-grkrb9nj: make Q2's Lua filter API match Pandoc's marshaling behavior, with a test-harness-driven burn-down instead of whack-a-mole.

Infrastructure

  • Track 1 — vendored conformance suite: pandoc-lua-marshal's own test-{attr,inline,block}.lua (at upstream c2dc4e11) running inside pampa's production Lua filter environment, with an xfail ratchet (unexpected failures and unexpected passes both break CI). The xfail file is the scoreboard.
  • Track 2 — differential oracle suite: small (input.md, filter.lua) cases run through the real pampa binary and compared (normalized) against committed JSON snapshots from a pinned pandoc 3.9.0.2. CI never needs pandoc; snapshots are regenerated locally by script.

Fixes (one strand per commit)

Strand What
bd-0xghpvij pandoc.OrderedList honors its ListAttributes argument
bd-55mb0rjz structural __eq on elements/lists + Haskell-show tostring
bd-hitjclzp hslua-style property cache+readback: div.content:insert(x) persists; reads alias
bd-tzwcof0n full Pandoc Attr shapes (positional triple, HTML-like map, list-of-pairs), pandoc.AttributeList, attr.classes as a real List
bd-23yvjfmm filter return values through the fuzzy peekers: bare-string returns word-split, table entries coerce element-wise, number/boolean returns are loud actionable errors (pandoc errors too); shortcode table returns fixed the same way
bd-1fjtodu8 pandoc.List callable module (in-place, hslua-list semantics); instances no longer callable; deep Inlines:clone/Blocks:clone

Scoreboard

  • Track 1: 11 → 98 passing of 133 (remaining 35 xfails are catalogued by root cause, each with a strand)
  • Track 2: 2/8 → 17/17 passing, xfail list empty
  • Every fix was oracle-probed against pandoc 3.9.0.2 before implementation and verified e2e through the real binaries (byte-identical HTML for the covered cases)

Catalog: claude-notes/research/2026-07-13-lua-api-mismatch-catalog.md · Plan: claude-notes/plans/2026-07-13-lua-api-pandoc-parity.md

Remaining known clusters (filed, not in this PR): walk semantics (bd-2j048yfm, includes a C-stack overflow in truncating topdown), missing constructors (bd-sgfiiktn), setters (bd-0g2yp61w), __toinline/__toblock (bd-olz91r4v), error contract (bd-9p2686pc), doc-level filters (bd-a9g50za2).

🤖 Generated with Claude Code

cscheid and others added 12 commits July 13, 2026 12:01
…+ xfail ratchet

Phase 0.1-0.3 of the Lua API Pandoc-parity epic (bd-grkrb9nj, plan
claude-notes/plans/2026-07-13-lua-api-pandoc-parity.md):

- Vendor pandoc-lua-marshal's own Lua marshaling tests
  (test-{attr,inline,block}.lua @ c2dc4e11) plus hslua's pure-Lua
  tasty.lua runner (@ 82c983a9) under
  crates/pampa/tests/lua-conformance/, unmodified.
- prelude.lua replicates the upstream Haskell driver environment
  (constructors as bare globals, List global, enum constants as
  strings).
- Extract create_filter_environment() from apply_lua_filter (pure
  refactor, re-exported from pampa::lua) so the conformance runner
  measures the production filter environment, not a synthetic one.
- New integration test lua_conformance.rs runs the suite and enforces
  an xfail ratchet: unexpected failures AND unexpected passes both
  fail, so xfail.txt only shrinks. Both directions verified by fault
  injection.
- Baseline xfail.txt: 133 cases, 11 pass, 122 xfail — this is the
  empirical mismatch catalog baseline. Dominant failure classes
  visible already: missing element __eq, pandoc.List not callable,
  tostring shape mismatches, error-message contracts.

All 4048 pampa tests pass (cargo nextest run -p pampa
--features lua-filter).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0.4 of the Lua API Pandoc-parity epic (bd-grkrb9nj): each case
under tests/lua-conformance/differential/cases/ is an
(input.md, filter.lua) pair with a committed oracle.json snapshot of
pandoc 3.9.0.2's JSON AST for it. The runner drives the real pampa
binary (input.md -F filter.lua -t json), strips q2's source-tracking
JSON extensions, and requires AST identity, with the same
xfail ratchet as Track 1 (both directions fault-injection verified).
CI never needs pandoc; regen-oracles.sh regenerates snapshots locally
and refuses a non-pinned pandoc version.

Seeded with 8 cases; baseline 2 pass / 6 xfail. The 6 xfails
empirically confirm the seed mismatch catalog, one case per class:
A1 bare-string filter return silently ignored; A2 non-userdata
entries dropped from returned tables; B1 positional attr triple and
B2 HTML-like attr map both silently produce empty attrs; C1
OrderedList discards ListAttributes; D0 div.content:insert lost to a
detached property copy (the bd-grkrb9nj worked example). The 2
passing controls pin the behaviors that must not regress while
fixing: whole-property reassignment, and all five fuzzy Div
constructor forms.

All 4051 pampa tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- claude-notes/research/2026-07-13-lua-api-mismatch-catalog.md: the
  128 baseline conformance xfails grouped into 12 root-cause clusters
  with evidence, disposition, and strand. Headline: ~68 of 122
  Track-1 failures bottom out in missing element/list __eq.
- Ten child strands filed under epic bd-grkrb9nj (E: __eq/tostring,
  A: filter returns, B: attr shapes, D0: content mutation, D1:
  setters, E2: List module, C: missing constructors, C1: OrderedList,
  G: metamethods, H: error contract); bd-hitjclzp linked to bd-195t.
- Plan updated (Phase 0 and 1.1/1.2 checked off).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pandoc.OrderedList(items, listAttributes) bound its second argument as
_list_attr and hardcoded (1, Default, Default). Wire the existing
parse_list_attributes through, matching Pandoc (default only when the
argument is absent).

TDD trail: strengthened test_ordered_list_with_list_attributes (it
only checked .tag, which is how the bug survived) and confirmed it
failed with "Expected start 10, got 1" before the fix. The Track-2
differential case orderedlist-listattributes now matches the pandoc
3.9.0.2 oracle — first xfail removed by the new ratchet (differential
baseline 6 -> 5). The related Track-1 test (has list attribute
aliases) progresses past `start` and now fails on the `delimiter`
alias, which is bd-0g2yp61w territory; its xfail entry is unchanged.

All 4049 pampa tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
map_or_else/to_string_lossy cleanups in the conformance runner so
cargo xtask verify --skip-hub-build passes (-D warnings); check off
plan item 2.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track-1 conformance xfail 122 -> 64 (11 -> 69 passing), zero new
failures.

- New crates/pampa/src/lua/show.rs: Haskell-`show`-style rendering of
  AST values matching pandoc's tostring() exactly (formats probed
  against pandoc 3.9.0.2 and pinned by unit tests): elements, Inlines/
  Blocks lists, Attr tuple, Caption, Citation record syntax, Table
  parts, string escaping incl. decimal escapes + \& separator. q2
  extension nodes get stable pandoc-style spellings.
- __eq on LuaInline/LuaBlock: structural equality ignoring source
  info. Implemented by comparing the JSON writer's source-free
  serialization (inlines/blocks_to_source_free_json) rather than
  hand-maintaining a parallel ~60-variant equality; derived == is the
  fast path. LuaAttr __eq is component-wise and order-sensitive in
  the attribute list, matching Pandoc.
- Inlines/Blocks metatables get pandoc-style __tostring ([...]-shape);
  LuaAttr __tostring becomes the pandoc tuple shape.
- test_list_tostring updated from the legacy "Inlines {...}" shape to
  the Pandoc contract.

E2e verified through the real binaries: identical eq/tostring output
from `pampa eq.md -F eq.lua -t html` and `pandoc -L eq.lua`.

All 4059 pampa tests pass; cargo xtask verify --skip-hub-build green.

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

The bd-grkrb9nj worked example: div.content:insert(x) was silently
discarded because property reads returned detached copies. Adopt
hslua's semantics (the design decided with Carlos): a PropertyCache on
LuaInline/LuaBlock caches the Lua table handed out for cacheable
properties (content, citations, caption), so repeated reads alias the
same table, and every marshal-out path flushes the cached values back
through set_field before cloning the inner AST value.

- types.rs: PropertyCache (aliasing store + reentrancy-guarded flush);
  __index returns/stores cached tables; __newindex keeps assigned
  tables aliased; flush wired into peek_*_fuzzy, FromLua, clone, walk,
  __pairs, __eq, __tostring.
- filter.rs: all six return handlers + extract_lua_* helpers take Lua
  and extract flushed (shortcode.rs callers updated).
- New set_field arms required by the flush (and part of D1):
  BulletList/OrderedList/DefinitionList/LineBlock content,
  Figure/Table caption (via constructors' parse helpers, now
  pub(crate)).
- 9 new integration tests (test_lua_content_mutation.rs): insert /
  table.insert / indexed assignment / nested child mutation persist;
  reads alias (rawequal); tostring fresh after mutation; nil return
  still discards; reassignment control; citations flush.

Conformance: Track-1 xfail 64 -> 60, differential 5 -> 4
(content-insert-inplace now matches the pandoc 3.9.0.2 oracle), zero
new failures. E2e: the original t1.qmd/t1.lua repro is byte-identical
to pandoc through the real pampa binary. All 4068 pampa tests pass;
cargo xtask verify --skip-hub-build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (bd-tzwcof0n)

Track-1 conformance xfail 60 -> 43 (90/133 passing); differential
4 -> 2 — both attr oracle cases now match (silent-empty-attr class
eliminated). Zero new failures.

- parse_attr accepts every pandoc peekAttr shape: positional triple
  {id, {classes}, {kv}}, HTML-like map (id key, space-split class key,
  other keys -> attributes), Attr/AttributeList userdata, string
  identifier — plus the kept q2 named-key extension
  ({identifier=, classes=, attributes=}). Rejects error loudly with
  typed messages instead of silently producing empty attrs.
- pandoc.Attr dispatches on its first argument like pandoc's mkAttr;
  new pandoc.AttributeList constructor; shared parse_attribute_list
  accepts map / list-of-pairs / AttributeList userdata.
- LuaAttributesProxy: integer pair indexing (attrs[i] read, replace,
  delete-by-nil) and order-sensitive __eq.
- attr.classes and element .classes now return pandoc-List tables
  (getmetatable == pandoc.List) with hslua cache+readback persistence:
  LuaAttr refactored enum -> struct carrying its own PropertyCache;
  elements cache the attr userdata itself so cb.attr reads alias and
  nested attr mutations survive to the flush (recursive, with
  self-ref detection to avoid double-borrow). This also completes
  bd-195t: cb.attr.attributes[k]=v AND cb.attr.classes:insert(x) both
  persist, verified byte-identical to pandoc through the real binary.

All 4068 pampa tests pass; cargo xtask verify --skip-hub-build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All six filter.rs return handlers (element, list, and the four topdown
*_with_control variants) plus the two ad-hoc typewise list-splice sites
now coerce non-nil returns via peek_inlines_fuzzy/peek_blocks_fuzzy,
matching pandoc: bare strings word-split (Plain-wrapped in block
position), table entries coerce element-wise (strings become single
Strs), a single Inline userdata from a Block filter is Plain-wrapped.
Non-coercible returns (number/boolean) are now loud errors naming the
filter function and got-type instead of silent no-ops — pandoc errors
on these too (13 oracle probes against pinned pandoc 3.9.0.2).
Invariants kept: nil keeps the original, empty table deletes, second
return value false stops topdown traversal.

A3 audit: shortcode classify_table_result had the same
drop-non-userdata bug and also discarded inlines from mixed
inline/block tables; it now classifies via the peekers (Inlines first,
then Blocks). Dead extract_lua_{inlines,blocks}_from_table helpers
removed. Doc-level filter gap (Pandoc/Doc collected but never invoked,
no Meta support) filed as bd-a9g50za2.

Differential suite: 8 -> 15 cases, 15 pass, 0 xfail (7 new
oracle-pinned cases cover word-split, block-position strings,
Inlines/Blocks list filters, topdown, and mixed tables). Track-1
conformance unchanged at 90/133 (this cluster was Track-2-only).
E2e verified byte-identical to pandoc through the real pampa binary;
full cargo xtask verify green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bd-1fjtodu8)

pandoc.List now carries its own metatable with __call, matching
hslua-list semantics (oracle-probed against pandoc 3.9.0.2):
List(t) / List{...} attach the List metatable to the argument IN
PLACE and return the same table; List() creates an empty list; a
non-table argument raises "table expected, got X". Removed the stray
__call field from the instance metatable — list instances are not
callable in pandoc, and q2's were (calling an instance would have
reparented the argument's metatable to that instance).

Inlines:clone and Blocks:clone are now deep: each element userdata is
extracted (flushing cached property mutations) and re-wrapped fresh,
so mutating the clone never leaks into the original. Generic
List:clone stays shallow, also oracle-confirmed.

Scoreboard: Track-1 conformance 90 -> 98 passing / 35 xfail (flipped:
5 BulletList content tests, both deep-clone tests, and the
AttributeList delete-nonexistent test). Differential suite 15 -> 17
cases, all passing, xfail still empty (new cases list-module-callable
and list-clone-deep). 6 new integration tests in test_lua_list.rs.
E2e verified byte-identical to pandoc through the real pampa binary;
full cargo xtask verify green.

With the List noise gone, the walk-semantics residue is unmasked:
12 Track-1 xfails refiled as bd-2j048yfm (fresh failure messages
recorded in xfail.txt), including a C stack overflow in truncating
topdown traversal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Jul 13, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cscheid and others added 14 commits July 13, 2026 16:06
… (bd-2j048yfm)

New module crates/pampa/src/lua/walk.rs mirrors pandoc-lua-marshal's
Walk.hs/SpliceList.hs/Topdown.hs: the AST children map is written ONCE
(walk_{inline,block}_children, generic over a LuaWalker trait), with
TypewisePass (four sequential bottom-up passes; splicing visits
children before the element's own function) and TopdownWalk (single
pre-order traversal: list function, then element function, then
descent into the replaced elements' children; an element function's
second return value of false skips that element's children while
siblings continue, a list function's false halts the list) built on
it. Replaces ~900 lines of four hand-rolled per-pass recursions that
had drifted apart — the old passes silently skipped Table cell and
caption content, DefinitionList terms/definitions, Figure captions,
and Citation prefix/suffix.

elem:walk now follows pandoc's subtree rule: the element itself is
never offered to the filter and no synthetic singleton list exists.
This fixes the C stack overflow in the pandoc idiom
`return i:walk(filter), false` (self-inclusive walk recursed
infinitely) and makes inline-rooted walks run all four passes, so
block functions reach blocks nested inside Notes.

One pre-existing test updated per explicit decision (Carlos,
2026-07-13: pandoc compatibility wins over q2's early self-inclusive
walk design): test_elem_walk_typewise_traversal_order pinned a Blocks
visit for the synthetic [Div] wrapper list; it now expects the pandoc
contract (a single Blocks visit for Div.content).

Scoreboard: Track-1 conformance 98 -> 110 passing / 23 xfail (all 12
walk xfails flipped, zero new failures). Differential 17 -> 19 cases,
all passing (new: walk-elem-subtree, walk-topdown-truncate). 6 new
integration tests in test_lua_walk.rs. Full pampa suite 4100/4100;
cargo xtask verify green; e2e byte-identical to pandoc through the
real binaries including the topdown-truncation subtlety.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Grows Track-1 coverage per plan Decision 3's deferred scope: vendors
test-{citation,listattributes,metavalue,pandoc,simpletable,table,
cell}.lua from pandoc-lua-marshal (same pinned commit c2dc4e11 as the
original trio, unmodified). The conformance runner gains per-file
minimum-case floors (metavalue/simpletable have only 2 cases each).

Corpus: 133 -> 203 cases; baseline 122 pass / 81 xfail. The 58 new
xfails are the empirical spec for the rest of bd-sgfiiktn:
- Citation/ListAttributes are plain tables, not userdata (18): field
  reads return nil, __eq fails, no :clone
- Cell/TableBody helper userdata are not property-indexable, no attr
  aliases, no :walk (21)
- pandoc.Pandoc/pandoc.Meta* constructors missing (17)
- pandoc.SimpleTable missing (2)

Also includes the wasm-quarto-hub-client Cargo.lock update from
pampa's async-trait dependency (added with the walk engine in
930dce5).

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

pandoc.Citation now returns typed userdata (LuaCitation: PropertyCache
on prefix/suffix so reads alias and :insert persists, structural __eq
via source-free JSON, Haskell-show __tostring, deep :clone, eager
validated setters). pandoc.Cite's argument order flips to Pandoc's
(content, citations) — mkCite is `flip Cite`; q2's old (citations,
content) order broke Pandoc-correct filters (comment c-inqf5qlb) —
with the strict list-of-Citation-userdata peeker ("table expected,
got Citation" / "Citation expected, got X"). cite.citations reads as
an aliased pandoc-List of Citation userdata with cache+readback
persistence. CitationMode constants added to the conformance prelude
(upstream registerConstants parity); differential normalizer strips
q2's citationIdS source extension.

Scoreboard: Track-1 xfail 81 -> 72 (7 test-citation + 2 Cite flips,
zero new failures); differential 19 -> 20 cases, all pass (new case
cite-construct-userdata). E2e: normalized JSON byte-identical to
pandoc 3.9.0.2 through the real pampa binary; HTML matches.

4 q2 test sites updated to the flipped Cite arg order per the
pandoc-compat-wins decision. cargo xtask verify --skip-hub-build green.

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

pandoc.ListAttributes now returns typed userdata (start/style/delimiter
properties with eager validated setters, structural __eq, deep :clone).
The constructor and the triple peeker error loudly on garbage styles/
delimiters (the old code silently defaulted, masking typos) and on
partial triples like {3} (pandoc's peekTriple rejects those too); a
full {start, style, delimiter} triple still works everywhere.

OrderedList gained the listAttributes property (cached userdata:
aliased reads, ol.listAttributes.start = 42 persists via the flush)
and delimiter; start/style/delimiter are hslua-style aliases that
read/write THROUGH the cached listAttributes value, including the
oracle-probed pandoc quirk that a raw-triple assignment makes aliases
read nil until marshal-out re-peeks.

Scoreboard: Track-1 xfail 72 -> 59 (11 test-listattributes + 2
test-block flips, zero new failures); differential 20 -> 21 cases,
all pass (new case orderedlist-la-userdata-mutation pins in-place
style mutation + alias writes). E2e: normalized JSON byte-identical
to pandoc 3.9.0.2; HTML <ol start="7" type="I"> matches.

Six q2 test sites updated from positional table indexing (l[1]) to
the pandoc property names, per the pandoc-compat-wins decision.
cargo xtask verify --skip-hub-build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ody/Caption (bd-sgfiiktn S3)

All six table-part wrappers are now cache-backed typed userdata
(Rc<RefCell<T>> + PropertyCache via a shared table_part_userdata!
skeleton): typed properties with eager validated setters, attr plus
identifier/classes/attributes aliases routed through a cached LuaAttr
handle (nested `cell.attributes.k = v` persists to the flush),
structural __eq via synthetic-Table source-free JSON compare,
Haskell-show __tostring, deep :clone, and Cell:walk / Row:walk on the
walk.rs LuaWalker children map (typewise + topdown).

Fuzzy peekers match pandoc-lua-marshal: peekRowFuzzy ({attr, cells}
pair or bare cell list), peekCellFuzzy (named contents/content with
defaults, or bare blocks); non-table row/cell lists and garbage
alignments error loudly (previously silent empty/default).

Two more Pandoc arg-order fixes: pandoc.TableBody(body, head,
row_head_columns, attr) and pandoc.Caption(long, short). Caption is
typed userdata; Table gained head/foot/bodies/colspecs round-trips
with nested-mutation persistence (incl. raw colspec pair tables);
Table bodies accepts a single TableBody; caption accepts bare block
lists; Image gained the `caption` alias for its content.

Version skew recorded: pandoc 3.9.0.2 exports neither
pandoc.TableBody nor Cell:clone — the contract is pandoc-lua-marshal
@ c2dc4e11 (the vendored suite's commit); the differential case uses
only binary-supported behaviors.

Scoreboard: Track-1 xfail 59 -> 30 (29 flips: all 15 test-cell, all
12 test-table, Figure/Table caption + colspecs/head/foot/bodies in
test-block, Image caption in test-inline; zero new failures);
differential 21 -> 22 cases, all pass (new case
table-parts-nested-mutation; normalizer strips q2's `…S` source-info
companion fields on tagged nodes). E2e: normalized JSON
byte-identical to pandoc 3.9.0.2 through the real pampa binary; HTML
shows all four nested mutations (caption, colspec alignment, head
cell, body cell). cargo xtask verify --skip-hub-build green.

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

bd-sgfiiktn closed with S0-S3 complete; its remaining scope moved to
dedicated strands per Carlos: bd-2llqjsms (pandoc.Pandoc + Meta*,
blocked on the Meta<->ConfigValue design, to be designed together
with bd-a9g50za2) and bd-d4wd6r3i (SimpleTable deliberate divergence
per Decision 6, Q-code/registry mechanism coordinated with
bd-9p2686pc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (bd-olz91r4v)

bd-0g2yp61w — property assignment re-runs the constructor peekers:
element `attr` assignment now goes through parse_attr (bare string ->
identifier, positional triple, HTML-like map with class split,
Attr/AttributeList userdata flushed) — lua_value_to_attr delegates to
parse_attr instead of its weaker ad-hoc parser, fixing
`header.attr = 'id'`, `span.attr = {'', {}, {{'a','b'}}}`, and
`code.attr = {id=..., k=...}`. Added the missing quoted.quotetype and
math.mathtype setters with eager loud validation.

bd-olz91r4v — the four fuzzy peekers now consult the __toinline /
__toblock metamethod hooks (call_element_metamethod +
peek_{inline,block}_via_metamethod) with hslua's recoverable-failure
semantics: absent/non-function metafield, call error, or wrong return
type fall through to normal coercion. Hook ordering mirrors
pandoc-lua-marshal: tables try the hook before list interpretation
(singleton position) and before failing (element position); strict
element peeks come before hooks on userdata. Filter returns get the
hooks for free (they route through these peekers since bd-23yvjfmm).

Scoreboard: Track-1 xfail 30 -> 21 (5 setter + 4 metamethod flips,
zero new failures); differential 22 -> 24 cases, all pass (new cases
setter-repeek-attr-enums, toinline-toblock-hooks). E2e: both cases
byte-identical to pandoc 3.9.0.2 through the real pampa binary.
cargo xtask verify --skip-hub-build green. Remaining 21 xfails all
belong to design-gated strands (bd-2llqjsms 17, bd-d4wd6r3i 2,
bd-9p2686pc 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry (bd-d4wd6r3i)

Decision 6: q2 does not implement the legacy pre-pandoc-2.10
simple-table API. pandoc.SimpleTable and the pandoc.utils
to_simple_table/from_simple_table stubs (all previously absent) now
raise the shared Q-11-2 error pointing users at pandoc.Table.

- Q-11-2 allocated in quarto-error-catalog.
- Seeds the divergence registry at
  crates/pampa/tests/lua-conformance/divergences.md (first entry);
  bd-9p2686pc extends this file and formalizes the '# DIVERGENCE'
  xfail marker (today a comment convention).
- The 2 test-simpletable.lua xfails are now permanent '# DIVERGENCE'
  entries; Track-1 scoreboard unchanged (182 pass / 21 xfail).
- New integration tests cover all three entry points; e2e-verified
  through `q2 render` (Q-11-2 message + filter stack traceback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94 Error::runtime sites surveyed; oracle contract shape probed vs
pandoc 3.9.0.2. Phase 1 (ratchet '# DIVERGENCE' formalization) has no
policy dependency; Phase 2 blocked on two design questions (nil
permissiveness of Inlines/Blocks, Q-code granularity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (bd-9p2686pc)

Decisions (Carlos, 2026-07-14): granular actionable Q-codes over a
catch-all; pandoc.Inlines/Blocks now ERROR on nil/no-arg like pandoc
(the permissive empty-list reading was ambiguous — nil means "keep the
element" while {} means "remove it" in filter returns).

- Q-11-3 invalid argument: type_mismatch_error in types.rs emits
  hslua's "<expected> expected, got <type>" shape; adopted in the four
  fuzzy peekers' terminal branches and the Inlines/Blocks constructors.
- Q-11-4 invalid filter return: filter_return_error wraps the inner
  Q-11-3 detail without duplicating code or got-type.
- Q-11-5 invalid property assignment: read-only tag + unknown-field
  __newindex fallbacks on both element types.
- Ratchet: xfail parsing returns divergence flags ('# DIVERGENCE'
  trailing comment); a passing divergence entry is reported as a stale
  registry entry, and divergence_xfails_are_registered ties both xfail
  files to divergences.md.
- The 2 upstream error-contract tests FLIPPED: Track-1 xfail 21 -> 19
  (184 pass); differential stays 24/24. Two unit tests that pinned the
  old nil->empty convenience were replaced by error-contract tests
  (test_{inlines,blocks}_constructor_nil_errors), per the decision.
- Follow-up sweep of the remaining ~85 bare Error::runtime sites:
  bd-ixnp4uqj.

E2e: all three Q-codes observed through `q2 render` with real filters.

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

Sweep the remaining bare Error::runtime sites in types.rs,
constructors.rs, and list.rs onto the bd-9p2686pc contract:

- Q-11-3 (invalid argument) for value-conversion failures wherever
  they occur — peekers, FromLua impls, attr/caption/colspec/row/cell/
  list-attributes parsers, and enum-value validation (math type,
  quote type, citation mode, list number style/delim, alignment).
  hslua "<expected> expected, got <type>" shape where a value is at
  hand.
- Q-11-5 (invalid property assignment) for setter-structural
  refusals — unknown field ("cannot set unknown field 'x' on Cell"),
  read-only field, wrong-variant assignment, proxy __newindex
  key/shape errors.

New helpers in types.rs (type_mismatch_error_named,
invalid_value_error, read_only_field_error, unknown_field_error);
the already-shipped element __newindex fallbacks deduped onto them
with byte-identical messages. Table-part unknown-field messages gain
"on <Type>".

TDD: two table-driven pinning tests (Q-11-3 / Q-11-5 families)
written first and observed red. Note: attr.classes proxy __newindex
errors are unreachable from Lua (proxy is input-only since
bd-tzwcof0n) — tagged but not testable.

Conformance ratchet unchanged (Track-1 184 pass / 19 xfail; none of
the remaining xfails are message-dependent). Full cargo xtask verify
green; e2e-verified through q2 render (both codes observed with
filter file:line tracebacks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Lua filter defining Pandoc, Meta, or Doc gets its handler collected
but never invoked (bd-2llqjsms / bd-a9g50za2 track the
Meta<->ConfigValue design that invocation needs). Until that lands,
silently ignoring the handler is a trap; apply_lua_filter now emits a
Q-11-6 warning per doc-level handler, naming the filter file and the
handler, with the ignored/still-runs contract spelled out in the
problem/hint.

TDD: test_doc_level_handler_emits_unimplemented_warning (red first,
covers Pandoc/Meta/Doc) + negative test that element-only filters stay
warning-free. E2e-verified through q2 render: warning prints with
code, render succeeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
synthetic_io/os_available_in_filters put their assertions inside
'function Pandoc(doc)' -- a doc-level handler that q2 collects but
never invokes -- so the io/os checks had been passing vacuously since
they were written. The new Q-11-6 unimplemented-handler warning
(27646e0) exposed this by tripping the tests' empty-diagnostics
assertion in WASM CI. Move the checks into 'Para', which actually
runs against the test document; verified locally on wasm32 (6/6 pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cscheid
cscheid merged commit 8e05c03 into main Jul 14, 2026
8 checks passed
@cscheid
cscheid deleted the feature/bd-grkrb9nj-lua-api-pandoc-parity branch July 14, 2026 20:36
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