@spintax/core 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
3
3
  All notable changes to `@spintax/core` are documented here. This project adheres to
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.5.3 — 2026-08-18
7
+
8
+ The expansion budget added in 0.5.2 was created per rendered AST, and an `#include` renders one
9
+ — so every include handed the bomb a fresh allowance. Fifty `#include` lines over a single
10
+ 62-character body turned **690 bytes into 57 MB**, growing linearly with the include count: the
11
+ bound held for each subtree and bounded nothing overall.
12
+
13
+ ### Fixed
14
+
15
+ The budget now belongs to the `render()` call and travels on the render context, includes and
16
+ all. Measured after: 200 includes over the same body produce 1.14 MB, flat, in about 210 ms —
17
+ the same figure one include produces.
18
+
19
+ A host may cap the number of include resolutions (the reference Worker does), but the engine
20
+ must not need it to.
21
+
22
+ ### Notes
23
+
24
+ Worth stating plainly, since it decides who is exposed: **this needs an untrusted template
25
+ author, not untrusted data.** The bomb is built from `#set` definitions, and definitions live in
26
+ the template — T1, author-trusted in the §6 model. A context value is T2, and `neutralize()`
27
+ strips `%` from it (`%b% %b%` becomes `b b`), so shielded data cannot carry a reference at all.
28
+ A host that renders its own templates against user data is not reachable; a host that renders
29
+ templates its users write is.
30
+
6
31
  ## 0.5.2 — 2026-08-18
7
32
 
8
33
  **`render()` no longer dies on a 62-character template** (issue #69). Not a regression — the
package/dist/index.cjs CHANGED
@@ -920,7 +920,7 @@ function renderAst(ast, ctx) {
920
920
  locale: ctx.locale,
921
921
  depth: 0,
922
922
  onPluralError: ctx.onPluralError,
923
- budget: { left: MAX_EXPANSION_CHARS }
923
+ budget: ctx.budget
924
924
  };
925
925
  const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
926
926
  const text = renderNodes(ast.nodes, { ...walkOpts, vars });
@@ -1360,7 +1360,10 @@ function renderWith(input, rng, opts = {}) {
1360
1360
  resolver: opts.includeResolver,
1361
1361
  maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
1362
1362
  includeStack: [],
1363
- onPluralError: opts.onPluralError
1363
+ onPluralError: opts.onPluralError,
1364
+ // One allowance for the whole call. Created here rather than in renderAst because
1365
+ // includes re-enter renderAst, and a budget made there is a budget per subtree.
1366
+ budget: { left: MAX_EXPANSION_CHARS }
1364
1367
  };
1365
1368
  const ast = resolveAst(input);
1366
1369
  let out = renderAst(ast, ctx);