tina4-nodejs 3.13.86 → 3.13.87

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/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.86)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.87)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.86 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.87 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1244
1244
  ## v3 Features Summary
1245
1245
 
1246
1246
  - **98 built-in features**, zero third-party dependencies
1247
- - **5,379 tests** passing across 154 files (build + typecheck green; 9 PostgreSQL/Valkey service-gated failures)
1247
+ - **5,879 tests** passing across 189 files (build + typecheck green)
1248
1248
  - **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
1249
1249
  - **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
1250
1250
  - **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.86",
3
+ "version": "3.13.87",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -1675,18 +1675,21 @@ var init_postgres = __esm({
1675
1675
  }
1676
1676
  async startTransactionAsync() {
1677
1677
  await this.executeAsync("BEGIN");
1678
+ this._inTransaction = true;
1678
1679
  }
1679
1680
  commit() {
1680
1681
  throw new Error("Use commitAsync() for PostgreSQL.");
1681
1682
  }
1682
1683
  async commitAsync() {
1683
1684
  await this.executeAsync("COMMIT");
1685
+ this._inTransaction = false;
1684
1686
  }
1685
1687
  rollback() {
1686
1688
  throw new Error("Use rollbackAsync() for PostgreSQL.");
1687
1689
  }
1688
1690
  async rollbackAsync() {
1689
1691
  await this.executeAsync("ROLLBACK");
1692
+ this._inTransaction = false;
1690
1693
  }
1691
1694
  tables() {
1692
1695
  throw new Error("Use tablesAsync() for PostgreSQL.");
@@ -2705,10 +2708,35 @@ var init_firebird = __esm({
2705
2708
  translated = SQLTranslator.ilikeToLike(translated);
2706
2709
  return translated;
2707
2710
  }
2711
+ /**
2712
+ * The handle every statement runs on. While an explicit transaction is open
2713
+ * (startTransactionAsync set `this.transaction`), statements MUST run on that
2714
+ * transaction object so they are undone by rollbackAsync() / persisted by
2715
+ * commitAsync() — node-firebird's transaction exposes the same
2716
+ * query()/execute() as the connection. With no transaction open we run on
2717
+ * `this.db`, whose per-statement work auto-commits on the connection.
2718
+ *
2719
+ * This matches the Python master's contract (tina4_python/database/firebird.py):
2720
+ * there, ALL statements run on the single connection and start_transaction()
2721
+ * merely suppresses the per-statement autocommit in execute() so the batch
2722
+ * stays open until commit()/rollback(). node-firebird has no such suppression
2723
+ * hook — its `db.query/execute` always auto-commit — so the equivalent is to
2724
+ * route statements through the transaction object instead. Same observable
2725
+ * behaviour: an open transaction is atomic and rolls back cleanly.
2726
+ *
2727
+ * Previously every statement ran on `this.db` unconditionally, so the
2728
+ * transaction created by startTransactionAsync() never saw a single statement
2729
+ * — rollbackAsync() rolled back an EMPTY transaction and the already
2730
+ * auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
2731
+ * bug fixed in 3.13.86.
2732
+ */
2733
+ statementHandle() {
2734
+ return this.transaction ?? this.db;
2735
+ }
2708
2736
  queryPromise(sql, params) {
2709
2737
  return new Promise((resolve31, reject) => {
2710
2738
  const translated = this.translateSql(sql);
2711
- this.db.query(translated, params ?? [], (err, result) => {
2739
+ this.statementHandle().query(translated, params ?? [], (err, result) => {
2712
2740
  if (err) reject(err);
2713
2741
  else resolve31(result ?? []);
2714
2742
  });
@@ -2717,7 +2745,7 @@ var init_firebird = __esm({
2717
2745
  executePromise(sql, params) {
2718
2746
  return new Promise((resolve31, reject) => {
2719
2747
  const translated = this.translateSql(sql);
2720
- this.db.execute(translated, params ?? [], (err) => {
2748
+ this.statementHandle().execute(translated, params ?? [], (err) => {
2721
2749
  if (err) reject(err);
2722
2750
  else resolve31();
2723
2751
  });
@@ -4691,14 +4719,15 @@ var init_database = __esm({
4691
4719
  async executeMany(sql, paramSets = []) {
4692
4720
  const adapter = this.getNextAdapter();
4693
4721
  const results = [];
4694
- await adapterStartTransaction(adapter);
4722
+ const owns = !this.inExplicitTransaction();
4723
+ if (owns) await adapterStartTransaction(adapter);
4695
4724
  try {
4696
4725
  for (const params of paramSets) {
4697
4726
  results.push(await adapterExecute(adapter, sql, params));
4698
4727
  }
4699
- await adapterCommit(adapter);
4728
+ if (owns) await adapterCommit(adapter);
4700
4729
  } catch (e) {
4701
- await adapterRollback(adapter);
4730
+ if (owns) await adapterRollback(adapter);
4702
4731
  throw e;
4703
4732
  }
4704
4733
  return results;
@@ -12751,6 +12780,7 @@ var init_request = __esm({
12751
12780
  var engine_exports = {};
12752
12781
  __export(engine_exports, {
12753
12782
  Frond: () => Frond,
12783
+ TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
12754
12784
  setFormTokenSessionId: () => setFormTokenSessionId
12755
12785
  });
12756
12786
  import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
@@ -12826,6 +12856,14 @@ function renderDump(value) {
12826
12856
  function liveAttr(value) {
12827
12857
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12828
12858
  }
12859
+ function capCache(cache, maxEntries) {
12860
+ if (cache.size < maxEntries) return;
12861
+ let drop = Math.floor(maxEntries / 2);
12862
+ for (const key of cache.keys()) {
12863
+ cache.delete(key);
12864
+ if (--drop <= 0) break;
12865
+ }
12866
+ }
12829
12867
  function tokenize(source) {
12830
12868
  const rawBlocks = [];
12831
12869
  source = source.replace(RAW_BLOCK_RE, (_match, content) => {
@@ -13177,6 +13215,9 @@ function evalExpr(expr, context) {
13177
13215
  }).join("");
13178
13216
  }
13179
13217
  }
13218
+ if (expr.startsWith("not ")) {
13219
+ return evalComparison(expr, context);
13220
+ }
13180
13221
  for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
13181
13222
  if (findOutsideQuotes(expr, op) >= 0) {
13182
13223
  return evalComparison(expr, context);
@@ -13732,7 +13773,7 @@ function _generateFormToken(descriptor = "") {
13732
13773
  function _generateFormTokenValue(descriptor = "") {
13733
13774
  return new SafeString(_buildFormTokenJwt(descriptor));
13734
13775
  }
13735
- var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13776
+ var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13736
13777
  var init_engine = __esm({
13737
13778
  "../frond/src/engine.ts"() {
13738
13779
  "use strict";
@@ -13765,6 +13806,7 @@ var init_engine = __esm({
13765
13806
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
13766
13807
  filterChainCache = /* @__PURE__ */ new Map();
13767
13808
  pathParseCache = /* @__PURE__ */ new Map();
13809
+ TEMPLATE_CACHE_MAX = 256;
13768
13810
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
13769
13811
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
13770
13812
  VarRef = class {
@@ -14170,6 +14212,7 @@ var init_engine = __esm({
14170
14212
  const source = readFileSync6(filePath, "utf-8");
14171
14213
  const mtime = statSync6(filePath).mtimeMs;
14172
14214
  const tokens = tokenize(source);
14215
+ capCache(this.compiled, TEMPLATE_CACHE_MAX);
14173
14216
  this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
14174
14217
  return this.executeWithSource(source, tokens, context);
14175
14218
  }
@@ -14184,6 +14227,7 @@ var init_engine = __esm({
14184
14227
  }
14185
14228
  }
14186
14229
  const tokens = tokenize(source);
14230
+ capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
14187
14231
  this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
14188
14232
  return this.executeCached(tokens, context);
14189
14233
  }
@@ -14399,6 +14443,9 @@ var init_engine = __esm({
14399
14443
  } else if (tag === "macro") {
14400
14444
  const skip = this.handleMacro(tokens, i, context);
14401
14445
  i = skip;
14446
+ } else if (tag === "import") {
14447
+ this.handleImportAs(content, context);
14448
+ i++;
14402
14449
  } else if (tag === "from") {
14403
14450
  this.handleFromImport(content, context);
14404
14451
  i++;
@@ -14965,7 +15012,7 @@ var init_engine = __esm({
14965
15012
  return i2;
14966
15013
  }
14967
15014
  const macroName = m[1];
14968
- const paramNames = m[2].split(",").map((p) => p.trim()).filter(Boolean);
15015
+ const params = _Frond.parseMacroParams(m[2]);
14969
15016
  const bodyTokens = [];
14970
15017
  let i = start2 + 1;
14971
15018
  while (i < tokens.length) {
@@ -14980,13 +15027,94 @@ var init_engine = __esm({
14980
15027
  const capturedContext = { ...context };
14981
15028
  context[macroName] = (...args) => {
14982
15029
  const macroCtx = { ...capturedContext };
14983
- for (let pi = 0; pi < paramNames.length; pi++) {
14984
- macroCtx[paramNames[pi]] = pi < args.length ? args[pi] : null;
15030
+ for (let pi = 0; pi < params.length; pi++) {
15031
+ const [pname, pdefault] = params[pi];
15032
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
14985
15033
  }
14986
15034
  return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
14987
15035
  };
14988
15036
  return i;
14989
15037
  }
15038
+ /**
15039
+ * Parse a macro parameter list into [name, default] pairs.
15040
+ *
15041
+ * Handles: name, name="default", name='default'. Splitting on "," alone left a
15042
+ * defaulted parameter literally NAMED `greeting='Hello'`, so the body's
15043
+ * {{ greeting }} matched nothing (rendered empty) AND the caller's positional
15044
+ * argument was stored under that junk key and lost. Mirrors the Python master's
15045
+ * _parse_macro_params. The default is null when none is declared.
15046
+ */
15047
+ static parseMacroParams(rawParams) {
15048
+ return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
15049
+ const eq = p.indexOf("=");
15050
+ if (eq === -1) return [p, null];
15051
+ const name = p.slice(0, eq).trim();
15052
+ let dflt = p.slice(eq + 1).trim();
15053
+ if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
15054
+ dflt = dflt.slice(1, -1);
15055
+ }
15056
+ return [name, dflt];
15057
+ });
15058
+ }
15059
+ /**
15060
+ * {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
15061
+ *
15062
+ * The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
15063
+ * resolves through the engine's existing dotted-call path and each macro keeps the
15064
+ * same argument binding, default handling and SafeString output as any other macro.
15065
+ * A namespace object (not a class) is deliberate: a function stored as a class
15066
+ * attribute binds as a method and would inject the namespace as the first argument,
15067
+ * which is exactly the argument-shift bug the Python master carried (fixed there
15068
+ * with types.SimpleNamespace). Both import forms must render identically.
15069
+ */
15070
+ handleImportAs(content, context) {
15071
+ const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
15072
+ if (!m) return;
15073
+ const filename = m[1];
15074
+ const alias = m[2];
15075
+ const namespace = {};
15076
+ const source = this.load(filename);
15077
+ const tokens = tokenize(source);
15078
+ let i = 0;
15079
+ while (i < tokens.length) {
15080
+ const [ttype, raw] = tokens[i];
15081
+ if (ttype === "BLOCK") {
15082
+ const [tagContent] = stripTag(raw);
15083
+ if ((tagContent.split(/\s+/)[0] || "") === "macro") {
15084
+ const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15085
+ if (macroM) {
15086
+ const macroName = macroM[1];
15087
+ const params = _Frond.parseMacroParams(macroM[2]);
15088
+ const bodyTokens = [];
15089
+ i++;
15090
+ while (i < tokens.length) {
15091
+ if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
15092
+ i++;
15093
+ break;
15094
+ }
15095
+ bodyTokens.push(tokens[i]);
15096
+ i++;
15097
+ }
15098
+ const capturedBody = [...bodyTokens];
15099
+ const capturedParams = [...params];
15100
+ const capturedCtx = { ...context };
15101
+ const engine = this;
15102
+ namespace[macroName] = (...args) => {
15103
+ const macroCtx = { ...capturedCtx };
15104
+ for (let pi = 0; pi < capturedParams.length; pi++) {
15105
+ const [pname, pdefault] = capturedParams[pi];
15106
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15107
+ }
15108
+ return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15109
+ };
15110
+ continue;
15111
+ }
15112
+ }
15113
+ }
15114
+ i++;
15115
+ }
15116
+ context[alias] = namespace;
15117
+ }
14990
15118
  handleFromImport(content, context) {
14991
15119
  const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
14992
15120
  if (!m) return;
@@ -15004,7 +15132,7 @@ var init_engine = __esm({
15004
15132
  const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15005
15133
  if (macroM && names.includes(macroM[1])) {
15006
15134
  const macroName = macroM[1];
15007
- const paramNames = macroM[2].split(",").map((p) => p.trim()).filter(Boolean);
15135
+ const paramNames = _Frond.parseMacroParams(macroM[2]);
15008
15136
  const bodyTokens = [];
15009
15137
  i++;
15010
15138
  while (i < tokens.length) {
@@ -15022,7 +15150,8 @@ var init_engine = __esm({
15022
15150
  context[macroName] = (...args) => {
15023
15151
  const macroCtx = { ...capturedCtx };
15024
15152
  for (let pi = 0; pi < capturedParams.length; pi++) {
15025
- macroCtx[capturedParams[pi]] = pi < args.length ? args[pi] : null;
15153
+ const [pname, pdefault] = capturedParams[pi];
15154
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15026
15155
  }
15027
15156
  return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15028
15157
  };
@@ -18129,7 +18258,14 @@ function fullAnalysis(root = "src") {
18129
18258
  total_functions: allFunctions.length,
18130
18259
  avg_complexity: Math.round(avgCC * 100) / 100,
18131
18260
  avg_maintainability: Math.round(avgMI * 10) / 10,
18261
+ // Display-only: the top-15 for the "most complex functions" report.
18262
+ // Do NOT source offenders / --fail-on from this — capping here silently
18263
+ // hides the 16th+ over-threshold function from the gate. offenders()
18264
+ // reads "all_functions" (below) instead.
18132
18265
  most_complex_functions: allFunctions.slice(0, 15),
18266
+ // Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
18267
+ // so no function over the complexity threshold ever escapes the gate.
18268
+ all_functions: allFunctions,
18133
18269
  file_metrics: fileMetrics,
18134
18270
  violations,
18135
18271
  dependency_graph: importGraph,
@@ -18145,7 +18281,7 @@ function offenders(root = "src", top = 20) {
18145
18281
  return { offenders: [], summary: { error: analysis.error } };
18146
18282
  }
18147
18283
  const items = [];
18148
- for (const fn of analysis.most_complex_functions || []) {
18284
+ for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
18149
18285
  const cc = fn.complexity;
18150
18286
  if (cc > 10) {
18151
18287
  items.push({
@@ -1674,18 +1674,21 @@ var init_postgres = __esm({
1674
1674
  }
1675
1675
  async startTransactionAsync() {
1676
1676
  await this.executeAsync("BEGIN");
1677
+ this._inTransaction = true;
1677
1678
  }
1678
1679
  commit() {
1679
1680
  throw new Error("Use commitAsync() for PostgreSQL.");
1680
1681
  }
1681
1682
  async commitAsync() {
1682
1683
  await this.executeAsync("COMMIT");
1684
+ this._inTransaction = false;
1683
1685
  }
1684
1686
  rollback() {
1685
1687
  throw new Error("Use rollbackAsync() for PostgreSQL.");
1686
1688
  }
1687
1689
  async rollbackAsync() {
1688
1690
  await this.executeAsync("ROLLBACK");
1691
+ this._inTransaction = false;
1689
1692
  }
1690
1693
  tables() {
1691
1694
  throw new Error("Use tablesAsync() for PostgreSQL.");
@@ -2704,10 +2707,35 @@ var init_firebird = __esm({
2704
2707
  translated = SQLTranslator.ilikeToLike(translated);
2705
2708
  return translated;
2706
2709
  }
2710
+ /**
2711
+ * The handle every statement runs on. While an explicit transaction is open
2712
+ * (startTransactionAsync set `this.transaction`), statements MUST run on that
2713
+ * transaction object so they are undone by rollbackAsync() / persisted by
2714
+ * commitAsync() — node-firebird's transaction exposes the same
2715
+ * query()/execute() as the connection. With no transaction open we run on
2716
+ * `this.db`, whose per-statement work auto-commits on the connection.
2717
+ *
2718
+ * This matches the Python master's contract (tina4_python/database/firebird.py):
2719
+ * there, ALL statements run on the single connection and start_transaction()
2720
+ * merely suppresses the per-statement autocommit in execute() so the batch
2721
+ * stays open until commit()/rollback(). node-firebird has no such suppression
2722
+ * hook — its `db.query/execute` always auto-commit — so the equivalent is to
2723
+ * route statements through the transaction object instead. Same observable
2724
+ * behaviour: an open transaction is atomic and rolls back cleanly.
2725
+ *
2726
+ * Previously every statement ran on `this.db` unconditionally, so the
2727
+ * transaction created by startTransactionAsync() never saw a single statement
2728
+ * — rollbackAsync() rolled back an EMPTY transaction and the already
2729
+ * auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
2730
+ * bug fixed in 3.13.86.
2731
+ */
2732
+ statementHandle() {
2733
+ return this.transaction ?? this.db;
2734
+ }
2707
2735
  queryPromise(sql, params) {
2708
2736
  return new Promise((resolve21, reject) => {
2709
2737
  const translated = this.translateSql(sql);
2710
- this.db.query(translated, params ?? [], (err, result) => {
2738
+ this.statementHandle().query(translated, params ?? [], (err, result) => {
2711
2739
  if (err) reject(err);
2712
2740
  else resolve21(result ?? []);
2713
2741
  });
@@ -2716,7 +2744,7 @@ var init_firebird = __esm({
2716
2744
  executePromise(sql, params) {
2717
2745
  return new Promise((resolve21, reject) => {
2718
2746
  const translated = this.translateSql(sql);
2719
- this.db.execute(translated, params ?? [], (err) => {
2747
+ this.statementHandle().execute(translated, params ?? [], (err) => {
2720
2748
  if (err) reject(err);
2721
2749
  else resolve21();
2722
2750
  });
@@ -4690,14 +4718,15 @@ var init_database = __esm({
4690
4718
  async executeMany(sql, paramSets = []) {
4691
4719
  const adapter = this.getNextAdapter();
4692
4720
  const results = [];
4693
- await adapterStartTransaction(adapter);
4721
+ const owns = !this.inExplicitTransaction();
4722
+ if (owns) await adapterStartTransaction(adapter);
4694
4723
  try {
4695
4724
  for (const params of paramSets) {
4696
4725
  results.push(await adapterExecute(adapter, sql, params));
4697
4726
  }
4698
- await adapterCommit(adapter);
4727
+ if (owns) await adapterCommit(adapter);
4699
4728
  } catch (e) {
4700
- await adapterRollback(adapter);
4729
+ if (owns) await adapterRollback(adapter);
4701
4730
  throw e;
4702
4731
  }
4703
4732
  return results;
@@ -12750,6 +12779,7 @@ var init_request = __esm({
12750
12779
  var engine_exports = {};
12751
12780
  __export(engine_exports, {
12752
12781
  Frond: () => Frond,
12782
+ TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
12753
12783
  setFormTokenSessionId: () => setFormTokenSessionId
12754
12784
  });
12755
12785
  import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
@@ -12825,6 +12855,14 @@ function renderDump(value) {
12825
12855
  function liveAttr(value) {
12826
12856
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12827
12857
  }
12858
+ function capCache(cache, maxEntries) {
12859
+ if (cache.size < maxEntries) return;
12860
+ let drop = Math.floor(maxEntries / 2);
12861
+ for (const key of cache.keys()) {
12862
+ cache.delete(key);
12863
+ if (--drop <= 0) break;
12864
+ }
12865
+ }
12828
12866
  function tokenize(source) {
12829
12867
  const rawBlocks = [];
12830
12868
  source = source.replace(RAW_BLOCK_RE, (_match, content) => {
@@ -13176,6 +13214,9 @@ function evalExpr(expr, context) {
13176
13214
  }).join("");
13177
13215
  }
13178
13216
  }
13217
+ if (expr.startsWith("not ")) {
13218
+ return evalComparison(expr, context);
13219
+ }
13179
13220
  for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
13180
13221
  if (findOutsideQuotes(expr, op) >= 0) {
13181
13222
  return evalComparison(expr, context);
@@ -13731,7 +13772,7 @@ function _generateFormToken(descriptor = "") {
13731
13772
  function _generateFormTokenValue(descriptor = "") {
13732
13773
  return new SafeString(_buildFormTokenJwt(descriptor));
13733
13774
  }
13734
- var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13775
+ var SafeString, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13735
13776
  var init_engine = __esm({
13736
13777
  "../frond/src/engine.ts"() {
13737
13778
  "use strict";
@@ -13764,6 +13805,7 @@ var init_engine = __esm({
13764
13805
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
13765
13806
  filterChainCache = /* @__PURE__ */ new Map();
13766
13807
  pathParseCache = /* @__PURE__ */ new Map();
13808
+ TEMPLATE_CACHE_MAX = 256;
13767
13809
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
13768
13810
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
13769
13811
  VarRef = class {
@@ -14169,6 +14211,7 @@ var init_engine = __esm({
14169
14211
  const source = readFileSync6(filePath, "utf-8");
14170
14212
  const mtime = statSync6(filePath).mtimeMs;
14171
14213
  const tokens = tokenize(source);
14214
+ capCache(this.compiled, TEMPLATE_CACHE_MAX);
14172
14215
  this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
14173
14216
  return this.executeWithSource(source, tokens, context);
14174
14217
  }
@@ -14183,6 +14226,7 @@ var init_engine = __esm({
14183
14226
  }
14184
14227
  }
14185
14228
  const tokens = tokenize(source);
14229
+ capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
14186
14230
  this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
14187
14231
  return this.executeCached(tokens, context);
14188
14232
  }
@@ -14398,6 +14442,9 @@ var init_engine = __esm({
14398
14442
  } else if (tag === "macro") {
14399
14443
  const skip = this.handleMacro(tokens, i, context);
14400
14444
  i = skip;
14445
+ } else if (tag === "import") {
14446
+ this.handleImportAs(content, context);
14447
+ i++;
14401
14448
  } else if (tag === "from") {
14402
14449
  this.handleFromImport(content, context);
14403
14450
  i++;
@@ -14964,7 +15011,7 @@ var init_engine = __esm({
14964
15011
  return i2;
14965
15012
  }
14966
15013
  const macroName = m[1];
14967
- const paramNames = m[2].split(",").map((p) => p.trim()).filter(Boolean);
15014
+ const params = _Frond.parseMacroParams(m[2]);
14968
15015
  const bodyTokens = [];
14969
15016
  let i = start2 + 1;
14970
15017
  while (i < tokens.length) {
@@ -14979,13 +15026,94 @@ var init_engine = __esm({
14979
15026
  const capturedContext = { ...context };
14980
15027
  context[macroName] = (...args) => {
14981
15028
  const macroCtx = { ...capturedContext };
14982
- for (let pi = 0; pi < paramNames.length; pi++) {
14983
- macroCtx[paramNames[pi]] = pi < args.length ? args[pi] : null;
15029
+ for (let pi = 0; pi < params.length; pi++) {
15030
+ const [pname, pdefault] = params[pi];
15031
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
14984
15032
  }
14985
15033
  return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
14986
15034
  };
14987
15035
  return i;
14988
15036
  }
15037
+ /**
15038
+ * Parse a macro parameter list into [name, default] pairs.
15039
+ *
15040
+ * Handles: name, name="default", name='default'. Splitting on "," alone left a
15041
+ * defaulted parameter literally NAMED `greeting='Hello'`, so the body's
15042
+ * {{ greeting }} matched nothing (rendered empty) AND the caller's positional
15043
+ * argument was stored under that junk key and lost. Mirrors the Python master's
15044
+ * _parse_macro_params. The default is null when none is declared.
15045
+ */
15046
+ static parseMacroParams(rawParams) {
15047
+ return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
15048
+ const eq = p.indexOf("=");
15049
+ if (eq === -1) return [p, null];
15050
+ const name = p.slice(0, eq).trim();
15051
+ let dflt = p.slice(eq + 1).trim();
15052
+ if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
15053
+ dflt = dflt.slice(1, -1);
15054
+ }
15055
+ return [name, dflt];
15056
+ });
15057
+ }
15058
+ /**
15059
+ * {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
15060
+ *
15061
+ * The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
15062
+ * resolves through the engine's existing dotted-call path and each macro keeps the
15063
+ * same argument binding, default handling and SafeString output as any other macro.
15064
+ * A namespace object (not a class) is deliberate: a function stored as a class
15065
+ * attribute binds as a method and would inject the namespace as the first argument,
15066
+ * which is exactly the argument-shift bug the Python master carried (fixed there
15067
+ * with types.SimpleNamespace). Both import forms must render identically.
15068
+ */
15069
+ handleImportAs(content, context) {
15070
+ const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
15071
+ if (!m) return;
15072
+ const filename = m[1];
15073
+ const alias = m[2];
15074
+ const namespace = {};
15075
+ const source = this.load(filename);
15076
+ const tokens = tokenize(source);
15077
+ let i = 0;
15078
+ while (i < tokens.length) {
15079
+ const [ttype, raw] = tokens[i];
15080
+ if (ttype === "BLOCK") {
15081
+ const [tagContent] = stripTag(raw);
15082
+ if ((tagContent.split(/\s+/)[0] || "") === "macro") {
15083
+ const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15084
+ if (macroM) {
15085
+ const macroName = macroM[1];
15086
+ const params = _Frond.parseMacroParams(macroM[2]);
15087
+ const bodyTokens = [];
15088
+ i++;
15089
+ while (i < tokens.length) {
15090
+ if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
15091
+ i++;
15092
+ break;
15093
+ }
15094
+ bodyTokens.push(tokens[i]);
15095
+ i++;
15096
+ }
15097
+ const capturedBody = [...bodyTokens];
15098
+ const capturedParams = [...params];
15099
+ const capturedCtx = { ...context };
15100
+ const engine = this;
15101
+ namespace[macroName] = (...args) => {
15102
+ const macroCtx = { ...capturedCtx };
15103
+ for (let pi = 0; pi < capturedParams.length; pi++) {
15104
+ const [pname, pdefault] = capturedParams[pi];
15105
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15106
+ }
15107
+ return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15108
+ };
15109
+ continue;
15110
+ }
15111
+ }
15112
+ }
15113
+ i++;
15114
+ }
15115
+ context[alias] = namespace;
15116
+ }
14989
15117
  handleFromImport(content, context) {
14990
15118
  const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
14991
15119
  if (!m) return;
@@ -15003,7 +15131,7 @@ var init_engine = __esm({
15003
15131
  const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15004
15132
  if (macroM && names.includes(macroM[1])) {
15005
15133
  const macroName = macroM[1];
15006
- const paramNames = macroM[2].split(",").map((p) => p.trim()).filter(Boolean);
15134
+ const paramNames = _Frond.parseMacroParams(macroM[2]);
15007
15135
  const bodyTokens = [];
15008
15136
  i++;
15009
15137
  while (i < tokens.length) {
@@ -15021,7 +15149,8 @@ var init_engine = __esm({
15021
15149
  context[macroName] = (...args) => {
15022
15150
  const macroCtx = { ...capturedCtx };
15023
15151
  for (let pi = 0; pi < capturedParams.length; pi++) {
15024
- macroCtx[capturedParams[pi]] = pi < args.length ? args[pi] : null;
15152
+ const [pname, pdefault] = capturedParams[pi];
15153
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15025
15154
  }
15026
15155
  return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15027
15156
  };
@@ -18128,7 +18257,14 @@ function fullAnalysis(root = "src") {
18128
18257
  total_functions: allFunctions.length,
18129
18258
  avg_complexity: Math.round(avgCC * 100) / 100,
18130
18259
  avg_maintainability: Math.round(avgMI * 10) / 10,
18260
+ // Display-only: the top-15 for the "most complex functions" report.
18261
+ // Do NOT source offenders / --fail-on from this — capping here silently
18262
+ // hides the 16th+ over-threshold function from the gate. offenders()
18263
+ // reads "all_functions" (below) instead.
18131
18264
  most_complex_functions: allFunctions.slice(0, 15),
18265
+ // Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
18266
+ // so no function over the complexity threshold ever escapes the gate.
18267
+ all_functions: allFunctions,
18132
18268
  file_metrics: fileMetrics,
18133
18269
  violations,
18134
18270
  dependency_graph: importGraph,
@@ -1183,7 +1183,14 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
1183
1183
  total_functions: allFunctions.length,
1184
1184
  avg_complexity: Math.round(avgCC * 100) / 100,
1185
1185
  avg_maintainability: Math.round(avgMI * 10) / 10,
1186
+ // Display-only: the top-15 for the "most complex functions" report.
1187
+ // Do NOT source offenders / --fail-on from this — capping here silently
1188
+ // hides the 16th+ over-threshold function from the gate. offenders()
1189
+ // reads "all_functions" (below) instead.
1186
1190
  most_complex_functions: allFunctions.slice(0, 15),
1191
+ // Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
1192
+ // so no function over the complexity threshold ever escapes the gate.
1193
+ all_functions: allFunctions,
1187
1194
  file_metrics: fileMetrics,
1188
1195
  violations,
1189
1196
  dependency_graph: importGraph,
@@ -1243,8 +1250,10 @@ export function offenders(root: string = "src", top: number = 20): OffendersResu
1243
1250
 
1244
1251
  const items: Offender[] = [];
1245
1252
 
1246
- // Function-level: cyclomatic complexity.
1247
- for (const fn of analysis.most_complex_functions || []) {
1253
+ // Function-level: cyclomatic complexity. Use the FULL function list (not the
1254
+ // display-capped most_complex_functions[:15]) so a 16th+ over-threshold
1255
+ // function is never silently dropped from the offenders list or --fail-on.
1256
+ for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
1248
1257
  const cc: number = fn.complexity;
1249
1258
  if (cc > 10) {
1250
1259
  items.push({