fix: draft() emits ciceromark Formula node without required value field (#146)#149
fix: draft() emits ciceromark Formula node without required value field (#146)#149mttrbrts wants to merge 3 commits into
Conversation
Coverage Report for CI Build 25955862016Coverage increased (+0.4%) to 64.606%Details
Uncovered Changes
Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
|
Follow-up push: swapped the regex-on-source detection for an AST-based check. What changed
Why Tests added (
Snapshot update Generated by Claude Code |
…ld (#146) An inline formula written as a bare expression — e.g. `{{% amount / 2.0 %}}` without `return` — was compiled to a TS function whose body was an expression statement, so the function returned undefined. JSON.stringify(undefined) is undefined, which left the `value` field unset on the ciceromark Formula node and caused the post-generation validator to reject the document with `ValidationException: missing the required field "value"`. Fix: `writeFunctionToString` now wraps user code with `return` when no explicit `return` keyword is present (strings/comments stripped to avoid false positives), restoring the implicit-expression semantics that Cicero/Ergo formulas have always had. As a defense in depth, the formula visitor in `generateAgreement` now throws a clear error if it ever still sees an undefined result, instead of producing an invalid document. Adds a `helloformula-implicit-return` good-template test that reproduces the bug and asserts the Formula node carries the evaluated `value`. Signed-off-by: Claude <noreply@anthropic.com>
CodeQL flagged the block-comment regex `/\/\*[\s\S]*?\*\//g` (and the nested-quantifier string-literal regex) as polynomial on uncontrolled input. The formula/condition source text comes from user-authored templates and is uncontrolled, so the alert is real. Replace the three regex-based replace calls with a single O(n) char-by-char scanner that drops line comments, block comments, and string literals. Functionally equivalent for the existing test cases and immune to ReDoS by construction. Signed-off-by: Claude <noreply@anthropic.com>
a0c6782 to
fc7364b
Compare
Replace the regex-on-source approach in wrapExpressionWithReturn with an
AST walk over top-level statements via ts.createSourceFile. The regex
matched any `return` keyword anywhere in the user code, which gave a
false positive when a nested function/arrow contained its own return
(e.g. `[1,2,3].map(x => { return x*2 })[0]`) — the outer expression was
then never wrapped and the Formula node ended up missing its `value`.
The new logic inspects only sourceFile.statements: if any top-level
statement is a ReturnStatement the code is kept verbatim; if the last
top-level statement is an ExpressionStatement it is rewritten as
`return <expr>;`; otherwise the code is left unchanged and the existing
"did not return a value" runtime error fires.
The stripStringsAndComments helper and RETURN_KEYWORD_RE constant are
removed. typescript is already a direct dependency so no package.json
changes were needed.
Adds focused unit tests for wrapExpressionWithReturn (bare expression,
explicit return, nested-return regression, multi-statement trailing
expression, statements-only, empty input). The formula-invalid snapshot
is regenerated: only the character offsets shift and a spurious
"';' expected." syntax error disappears because the new wrapping shape
is syntactically valid; the semantic "Cannot find name" errors are
unchanged.
Signed-off-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes implicit formula expressions so inline {{% expr %}} formulas produce a ciceromark Formula.value instead of undefined, while adding a clearer guard for formulas that still do not return a value.
Changes:
- Adds
wrapExpressionWithReturnand applies it when generating user-code functions. - Adds an undefined-result guard for formula evaluation.
- Adds a regression template/snapshot and unit coverage for expression wrapping.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/utils.ts |
Adds formula/condition wrapping logic before generated function output. |
src/TemplateMarkInterpreter.ts |
Adds explicit error handling for undefined formula results. |
test/utils.test.ts |
Adds unit tests for expression wrapping behavior. |
test/templates/good/helloformula-implicit-return/template.md |
Adds regression fixture for implicit formula return. |
test/templates/good/helloformula-implicit-return/model.cto |
Adds model for the regression fixture. |
test/templates/good/helloformula-implicit-return/data.json |
Adds data for the regression fixture. |
test/__snapshots__/TemplateMarkInterpreter.test.ts.snap |
Updates formula snapshots, including the new implicit-return fixture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| import { existsSync, mkdirSync, rmSync } from 'fs'; | ||
| import traverse from 'traverse'; | ||
| import * as ts from 'typescript'; |
| if (trimmed.length === 0) { | ||
| return trimmed; | ||
| } | ||
| const sourceFile = ts.createSourceFile('formula.ts', trimmed, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); |
| if (result === undefined) { | ||
| // JSON.stringify(undefined) is undefined, which would leave the | ||
| // required `value` field unset and fail downstream validation. | ||
| throw new Error(`Formula '${context.name}' did not return a value. Formulas must be an expression or use 'return' to produce a value.`); |
Fixes #146.
Root cause
A formula written as a bare expression — e.g.
{{% importerLOCAmount / 2.0 %}}without an explicitreturn— was compiled bywriteFunctionToStringinto a TS function whose body was an expression statement:That function returns
undefined. InevaluateUserCodethe result was then stored asJSON.stringify(undefined) === undefined. In the visitor that writesvalueonto theFormulanode, neither theresult === nullbranch nor thetypeof result === 'string'branch matched, so the fallbackcontext.value = JSON.stringify(result)also producedundefined.context.codewas then deleted, leaving theFormulanode withname+dependenciesbut novalue.validateCiceroMarkthen rejected the document with:This reproduces with the supply-agreement-loc template in cicero-template-library PR #484 (versions: template-engine 3.0.1, cicero-core 0.26.0, concerto-core 4.1.0).
Fix
src/utils.ts:writeFunctionToStringnow wraps the user's code withreturnwhen no explicitreturnkeyword is present at the source level. Strings and comments are stripped before the keyword scan to avoid false positives (e.g. a literal'return foo'inside the user's code). This restores the implicit-expression semantics that Cicero/Ergo formulas have always had —{{% amount / 2.0 %}}again produces a value. Existing formulas that already usereturnare unchanged.src/TemplateMarkInterpreter.ts: as defense in depth, the formula visitor ingenerateAgreementnow throws a clear, named error if the formula's evaluation result is stillundefined, instead of producing an invalid ciceromark document and surfacing the confusingmissing the required field "value"message from the validator.Test
Adds
test/templates/good/helloformula-implicit-return/— a minimal template containing the inline-formula repro{{% importerLOCAmount / 2.0 %}}. The matching snapshot asserts the producedorg.accordproject.ciceromark@0.6.0.Formulanode has"value": "500". Sandbox (child-process) evaluation is also covered by the existing harness, so both code paths are exercised.The
formula-invalidsnapshot was regenerated: the test still rejects garbage input — only the reported character offsets shift by the length of the prependedreturntoken. No behavior change.Scope
Limited to
accordproject/template-engine. No changes to cicero-core, ciceromark model, concerto, or other dependencies.Test plan
npx jest TemplateMarkInterpreter -t formula— all 6 formula tests pass (helloformula, helloformula-implicit-return, formula-now, formula-invalid, formula-no-method, formula-optional-noguard)npm run lintclean (no new warnings)npm run licchkcleannpm test— remaining 7 failures are pre-existing 403 errors fetching external models (network sandbox) and a pre-existing dynamic-import-callback issue inTemplateArchiveProcessor.test; all confirmed unchanged vs.mainbaseline.Generated by Claude Code