Skip to content

fix: draft() throws when {{#ulist}}/{{#olist}} contains direct variable references (#145)#148

Merged
mttrbrts merged 1 commit into
mainfrom
fix/issue-145-ulist-direct-vars
May 16, 2026
Merged

fix: draft() throws when {{#ulist}}/{{#olist}} contains direct variable references (#145)#148
mttrbrts merged 1 commit into
mainfrom
fix/issue-145-ulist-direct-vars

Conversation

@mttrbrts
Copy link
Copy Markdown
Member

Fixes #145.

Root cause

When a {{#ulist}} / {{#olist}} body contains direct variable references
(e.g. {{volumeAbove}}) with no leading list marker (- or 1. ), the
markdown parser does not wrap the body in a CommonMark List > Item
structure — it produces a Paragraph directly under the
ListBlockDefinition:

ListBlockDefinition
  Paragraph         <-- no List/Item wrapper
    VariableDefinition  (volumeAbove)
    ...

generateRecursiveBlocks in src/TemplateMarkInterpreter.ts blindly
accessed context.nodes[0].nodes[0], which assumed the List > Item
wrapping was always present. With the wrapping absent, that path points
at a leaf inline node (a VariableDefinition or Text), and
generateAgreement was then called with that leaf as its root
templateMark.

Two failure modes followed:

  1. Throws Error: Paths must be supplied from getJsonPath when the
    first inline node is a VariableDefinition (matches the
    dist/index.js:703 stack reported in the issue): the traversal visits
    the root with an empty this.path, and getJsonPath rejects empty
    paths.
  2. Silently empty Items when the first inline node is Text: no
    error is thrown but each output Item ends up with nodes: [].

This is reproducible with the src/volumediscountulist and
src/volumediscountolist templates from
accordproject/cicero-template-library#484.

Fix

Detect whether the parser produced a CommonMark.List wrapper and pick
the per-iteration template accordingly:

  • Wrapped (existing behaviour preserved): descend into
    firstChild.nodes[0] (the Item template), recurse on it, and unwrap
    the processed children into each new output Item.
  • Unwrapped (new path): treat firstChild itself (a Paragraph) as
    the per-iteration template and place the processed block as the sole
    child of each new output Item, keeping a valid
    Item > Paragraph > inline... hierarchy.

Tests

  • test/templates/good/volumediscountulist and
    test/templates/good/volumediscountolist — snapshot fixtures that
    exercise the failure mode through the regular goodTemplates loop.
  • test/TemplateMarkInterpreter.test.ts — assertion-based regression
    block under describe('issue #145: ...') covering both failure modes
    (variable as the first inline node, and a leading-text inline node)
    for both ulist and olist. All three tests fail on main and pass
    with the fix.

Network-dependent tests in this repo (models.accordproject.org fetches,
child-process JS evaluation) were failing identically before and after
this change due to the sandboxed environment; they are unrelated to this
fix.


Generated by Claude Code

…le references (#145)

When a `{{#ulist}}` / `{{#olist}}` body contains direct variable references
(e.g. `{{volumeAbove}}`) with no leading list marker (`- ` or `1. `), the
markdown parser does not wrap the body in a CommonMark `List > Item`
structure — it produces a `Paragraph` directly under the
`ListBlockDefinition`. The existing recursion in `generateRecursiveBlocks`
blindly accessed `context.nodes[0].nodes[0]`, which assumed the
`List > Item` wrapping. With the wrapping absent, that path pointed at a
leaf node (a `VariableDefinition` or `Text`), and `generateAgreement` was
then called with a `VariableDefinition` as its root `templateMark`. The
traversal visited the root with an empty `this.path`, so `getJsonPath`
threw `Error: Paths must be supplied`. When the first inline node was a
`Text` rather than a `VariableDefinition`, no error was thrown but each
output `Item` was silently empty.

Detect whether the parser produced a `CommonMark.List` wrapper and pick
the per-iteration template accordingly: when wrapped, descend into
`firstChild.nodes[0]` (the `Item` template) and unwrap its children into
each new output `Item` (preserving existing behaviour). Otherwise treat
`firstChild` itself as the per-iteration template and wrap the processed
block as the sole child of each new output `Item`, which keeps a valid
`Item > Paragraph > inline...` hierarchy.

Adds two snapshot-based fixtures (`volumediscountulist`,
`volumediscountolist`) and three assertion-based regression tests that
cover both failure modes (variable as the first inline node, and a
leading text inline node).

Signed-off-by: Claude <noreply@anthropic.com>
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 25947592408

Coverage increased (+0.3%) to 64.167%

Details

  • Coverage increased (+0.3%) from the base build.
  • Patch coverage: 5 of 5 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1132
Covered Lines: 723
Line Coverage: 63.87%
Relevant Branches: 548
Covered Branches: 355
Branch Coverage: 64.78%
Branches in Coverage %: Yes
Coverage Strength: 5833.29 hits per line

💛 - Coveralls

@mttrbrts mttrbrts marked this pull request as ready for review May 16, 2026 06:46
@mttrbrts mttrbrts requested review from a team and Copilot May 16, 2026 06:46
@mttrbrts mttrbrts merged commit 7286faa into main May 16, 2026
11 checks passed
@mttrbrts mttrbrts deleted the fix/issue-145-ulist-direct-vars branch May 16, 2026 06:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes list-block drafting when {{#ulist}} / {{#olist}} bodies contain direct variable references without markdown list markers, preventing throws or empty generated items.

Changes:

  • Updates recursive list generation to handle both wrapped List > Item bodies and unwrapped paragraph bodies.
  • Adds regression tests for direct variables in unordered/ordered list blocks.
  • Adds snapshot fixture templates for volume discount ordered/unordered lists.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/TemplateMarkInterpreter.ts Detects wrapped vs unwrapped list-block body shape and drafts items accordingly.
test/TemplateMarkInterpreter.test.ts Adds issue #145 regression coverage for variable-first and leading-text list bodies.
test/__snapshots__/TemplateMarkInterpreter.test.ts.snap Adds expected snapshots for new volume discount list fixtures.
test/templates/good/volumediscountulist/template.md Adds unordered-list regression fixture template.
test/templates/good/volumediscountulist/model.cto Adds model for unordered-list fixture.
test/templates/good/volumediscountulist/data.json Adds data for unordered-list fixture.
test/templates/good/volumediscountolist/template.md Adds ordered-list regression fixture template.
test/templates/good/volumediscountolist/model.cto Adds model for ordered-list fixture.
test/templates/good/volumediscountolist/data.json Adds data for ordered-list fixture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

draft() throws "Paths must be supplied" when {{#ulist}}/{{#olist}} contains direct variable references

4 participants