tina4-nodejs 3.13.86 → 3.13.88

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.88)
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.88 - 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,899 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.88",
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,11 +12780,32 @@ 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";
12757
12787
  import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync6 } from "node:fs";
12758
12788
  import { join as join11, resolve as resolve7 } from "node:path";
12789
+ function jsonText(value) {
12790
+ try {
12791
+ const text = JSON.stringify(value);
12792
+ return text === void 0 ? "null" : text;
12793
+ } catch {
12794
+ const seen = /* @__PURE__ */ new WeakSet();
12795
+ const text = JSON.stringify(value, (_key, v) => {
12796
+ if (typeof v === "bigint") return v.toString();
12797
+ if (typeof v === "object" && v !== null) {
12798
+ if (seen.has(v)) return null;
12799
+ seen.add(v);
12800
+ }
12801
+ return v;
12802
+ });
12803
+ return text === void 0 ? "null" : text;
12804
+ }
12805
+ }
12806
+ function jsonSafe(value) {
12807
+ return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
12808
+ }
12759
12809
  function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
12760
12810
  if (value === null) return "null";
12761
12811
  if (value === void 0) return "undefined";
@@ -12826,6 +12876,14 @@ function renderDump(value) {
12826
12876
  function liveAttr(value) {
12827
12877
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12828
12878
  }
12879
+ function capCache(cache, maxEntries) {
12880
+ if (cache.size < maxEntries) return;
12881
+ let drop = Math.floor(maxEntries / 2);
12882
+ for (const key of cache.keys()) {
12883
+ cache.delete(key);
12884
+ if (--drop <= 0) break;
12885
+ }
12886
+ }
12829
12887
  function tokenize(source) {
12830
12888
  const rawBlocks = [];
12831
12889
  source = source.replace(RAW_BLOCK_RE, (_match, content) => {
@@ -13177,6 +13235,9 @@ function evalExpr(expr, context) {
13177
13235
  }).join("");
13178
13236
  }
13179
13237
  }
13238
+ if (expr.startsWith("not ")) {
13239
+ return evalComparison(expr, context);
13240
+ }
13180
13241
  for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
13181
13242
  if (findOutsideQuotes(expr, op) >= 0) {
13182
13243
  return evalComparison(expr, context);
@@ -13732,7 +13793,7 @@ function _generateFormToken(descriptor = "") {
13732
13793
  function _generateFormTokenValue(descriptor = "") {
13733
13794
  return new SafeString(_buildFormTokenJwt(descriptor));
13734
13795
  }
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;
13796
+ var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, 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
13797
  var init_engine = __esm({
13737
13798
  "../frond/src/engine.ts"() {
13738
13799
  "use strict";
@@ -13744,6 +13805,15 @@ var init_engine = __esm({
13744
13805
  return this.value;
13745
13806
  }
13746
13807
  };
13808
+ JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
13809
+ JSON_UNSAFE_MAP = {
13810
+ "<": "\\u003c",
13811
+ ">": "\\u003e",
13812
+ "&": "\\u0026",
13813
+ "'": "\\u0027",
13814
+ "\u2028": "\\u2028",
13815
+ "\u2029": "\\u2029"
13816
+ };
13747
13817
  NUMERIC_RE = /^-?\d+(\.\d+)?$/;
13748
13818
  METHOD_CALL_RE = /^(\w+)\s*\(([\s\S]*)?\)$/;
13749
13819
  FN_CALL_RE = /^([\w.]+)\s*\(([\s\S]*)?\)$/;
@@ -13765,6 +13835,7 @@ var init_engine = __esm({
13765
13835
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
13766
13836
  filterChainCache = /* @__PURE__ */ new Map();
13767
13837
  pathParseCache = /* @__PURE__ */ new Map();
13838
+ TEMPLATE_CACHE_MAX = 256;
13768
13839
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
13769
13840
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
13770
13841
  VarRef = class {
@@ -13833,7 +13904,7 @@ var init_engine = __esm({
13833
13904
  int: (v) => v ? parseInt(String(v), 10) || 0 : 0,
13834
13905
  float: (v) => v ? parseFloat(String(v)) || 0 : 0,
13835
13906
  string: (v) => String(v),
13836
- json_encode: (v) => JSON.stringify(v),
13907
+ json_encode: (v) => jsonSafe(v),
13837
13908
  json_decode: (v) => typeof v === "string" ? JSON.parse(v) : v,
13838
13909
  keys: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.keys(v) : [],
13839
13910
  values: (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.values(v) : [],
@@ -13978,8 +14049,12 @@ var init_engine = __esm({
13978
14049
  form_token: (v) => _generateFormToken(v != null ? String(v) : ""),
13979
14050
  formTokenValue: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
13980
14051
  form_token_value: (v) => _generateFormTokenValue(v != null ? String(v) : ""),
13981
- tojson: (v, indent) => new SafeString(indent !== void 0 ? JSON.stringify(v, null, parseInt(String(indent), 10)) : JSON.stringify(v)),
13982
- to_json: (v, indent) => new SafeString(indent !== void 0 ? JSON.stringify(v, null, parseInt(String(indent), 10)) : JSON.stringify(v)),
14052
+ // Same serializer as json_encode -- the three names are one behaviour. The
14053
+ // old indent argument is gone: PHP cannot honour an arbitrary indent
14054
+ // (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
14055
+ // broke byte-parity for the one filter whose whole job is a wire format.
14056
+ tojson: (v) => jsonSafe(v),
14057
+ to_json: (v) => jsonSafe(v),
13983
14058
  js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"))
13984
14059
  };
13985
14060
  _formTokenSessionId = "";
@@ -14170,6 +14245,7 @@ var init_engine = __esm({
14170
14245
  const source = readFileSync6(filePath, "utf-8");
14171
14246
  const mtime = statSync6(filePath).mtimeMs;
14172
14247
  const tokens = tokenize(source);
14248
+ capCache(this.compiled, TEMPLATE_CACHE_MAX);
14173
14249
  this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
14174
14250
  return this.executeWithSource(source, tokens, context);
14175
14251
  }
@@ -14184,6 +14260,7 @@ var init_engine = __esm({
14184
14260
  }
14185
14261
  }
14186
14262
  const tokens = tokenize(source);
14263
+ capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
14187
14264
  this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
14188
14265
  return this.executeCached(tokens, context);
14189
14266
  }
@@ -14399,6 +14476,9 @@ var init_engine = __esm({
14399
14476
  } else if (tag === "macro") {
14400
14477
  const skip = this.handleMacro(tokens, i, context);
14401
14478
  i = skip;
14479
+ } else if (tag === "import") {
14480
+ this.handleImportAs(content, context);
14481
+ i++;
14402
14482
  } else if (tag === "from") {
14403
14483
  this.handleFromImport(content, context);
14404
14484
  i++;
@@ -14714,7 +14794,7 @@ var init_engine = __esm({
14714
14794
  value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
14715
14795
  continue;
14716
14796
  case "json_encode":
14717
- value = JSON.stringify(value);
14797
+ value = jsonSafe(value);
14718
14798
  continue;
14719
14799
  case "dump":
14720
14800
  value = renderDump(value);
@@ -14965,7 +15045,7 @@ var init_engine = __esm({
14965
15045
  return i2;
14966
15046
  }
14967
15047
  const macroName = m[1];
14968
- const paramNames = m[2].split(",").map((p) => p.trim()).filter(Boolean);
15048
+ const params = _Frond.parseMacroParams(m[2]);
14969
15049
  const bodyTokens = [];
14970
15050
  let i = start2 + 1;
14971
15051
  while (i < tokens.length) {
@@ -14980,13 +15060,94 @@ var init_engine = __esm({
14980
15060
  const capturedContext = { ...context };
14981
15061
  context[macroName] = (...args) => {
14982
15062
  const macroCtx = { ...capturedContext };
14983
- for (let pi = 0; pi < paramNames.length; pi++) {
14984
- macroCtx[paramNames[pi]] = pi < args.length ? args[pi] : null;
15063
+ for (let pi = 0; pi < params.length; pi++) {
15064
+ const [pname, pdefault] = params[pi];
15065
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
14985
15066
  }
14986
15067
  return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
14987
15068
  };
14988
15069
  return i;
14989
15070
  }
15071
+ /**
15072
+ * Parse a macro parameter list into [name, default] pairs.
15073
+ *
15074
+ * Handles: name, name="default", name='default'. Splitting on "," alone left a
15075
+ * defaulted parameter literally NAMED `greeting='Hello'`, so the body's
15076
+ * {{ greeting }} matched nothing (rendered empty) AND the caller's positional
15077
+ * argument was stored under that junk key and lost. Mirrors the Python master's
15078
+ * _parse_macro_params. The default is null when none is declared.
15079
+ */
15080
+ static parseMacroParams(rawParams) {
15081
+ return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
15082
+ const eq = p.indexOf("=");
15083
+ if (eq === -1) return [p, null];
15084
+ const name = p.slice(0, eq).trim();
15085
+ let dflt = p.slice(eq + 1).trim();
15086
+ if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
15087
+ dflt = dflt.slice(1, -1);
15088
+ }
15089
+ return [name, dflt];
15090
+ });
15091
+ }
15092
+ /**
15093
+ * {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
15094
+ *
15095
+ * The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
15096
+ * resolves through the engine's existing dotted-call path and each macro keeps the
15097
+ * same argument binding, default handling and SafeString output as any other macro.
15098
+ * A namespace object (not a class) is deliberate: a function stored as a class
15099
+ * attribute binds as a method and would inject the namespace as the first argument,
15100
+ * which is exactly the argument-shift bug the Python master carried (fixed there
15101
+ * with types.SimpleNamespace). Both import forms must render identically.
15102
+ */
15103
+ handleImportAs(content, context) {
15104
+ const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
15105
+ if (!m) return;
15106
+ const filename = m[1];
15107
+ const alias = m[2];
15108
+ const namespace = {};
15109
+ const source = this.load(filename);
15110
+ const tokens = tokenize(source);
15111
+ let i = 0;
15112
+ while (i < tokens.length) {
15113
+ const [ttype, raw] = tokens[i];
15114
+ if (ttype === "BLOCK") {
15115
+ const [tagContent] = stripTag(raw);
15116
+ if ((tagContent.split(/\s+/)[0] || "") === "macro") {
15117
+ const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15118
+ if (macroM) {
15119
+ const macroName = macroM[1];
15120
+ const params = _Frond.parseMacroParams(macroM[2]);
15121
+ const bodyTokens = [];
15122
+ i++;
15123
+ while (i < tokens.length) {
15124
+ if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
15125
+ i++;
15126
+ break;
15127
+ }
15128
+ bodyTokens.push(tokens[i]);
15129
+ i++;
15130
+ }
15131
+ const capturedBody = [...bodyTokens];
15132
+ const capturedParams = [...params];
15133
+ const capturedCtx = { ...context };
15134
+ const engine = this;
15135
+ namespace[macroName] = (...args) => {
15136
+ const macroCtx = { ...capturedCtx };
15137
+ for (let pi = 0; pi < capturedParams.length; pi++) {
15138
+ const [pname, pdefault] = capturedParams[pi];
15139
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15140
+ }
15141
+ return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15142
+ };
15143
+ continue;
15144
+ }
15145
+ }
15146
+ }
15147
+ i++;
15148
+ }
15149
+ context[alias] = namespace;
15150
+ }
14990
15151
  handleFromImport(content, context) {
14991
15152
  const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
14992
15153
  if (!m) return;
@@ -15004,7 +15165,7 @@ var init_engine = __esm({
15004
15165
  const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
15005
15166
  if (macroM && names.includes(macroM[1])) {
15006
15167
  const macroName = macroM[1];
15007
- const paramNames = macroM[2].split(",").map((p) => p.trim()).filter(Boolean);
15168
+ const paramNames = _Frond.parseMacroParams(macroM[2]);
15008
15169
  const bodyTokens = [];
15009
15170
  i++;
15010
15171
  while (i < tokens.length) {
@@ -15022,7 +15183,8 @@ var init_engine = __esm({
15022
15183
  context[macroName] = (...args) => {
15023
15184
  const macroCtx = { ...capturedCtx };
15024
15185
  for (let pi = 0; pi < capturedParams.length; pi++) {
15025
- macroCtx[capturedParams[pi]] = pi < args.length ? args[pi] : null;
15186
+ const [pname, pdefault] = capturedParams[pi];
15187
+ macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
15026
15188
  }
15027
15189
  return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
15028
15190
  };
@@ -18129,7 +18291,14 @@ function fullAnalysis(root = "src") {
18129
18291
  total_functions: allFunctions.length,
18130
18292
  avg_complexity: Math.round(avgCC * 100) / 100,
18131
18293
  avg_maintainability: Math.round(avgMI * 10) / 10,
18294
+ // Display-only: the top-15 for the "most complex functions" report.
18295
+ // Do NOT source offenders / --fail-on from this — capping here silently
18296
+ // hides the 16th+ over-threshold function from the gate. offenders()
18297
+ // reads "all_functions" (below) instead.
18132
18298
  most_complex_functions: allFunctions.slice(0, 15),
18299
+ // Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
18300
+ // so no function over the complexity threshold ever escapes the gate.
18301
+ all_functions: allFunctions,
18133
18302
  file_metrics: fileMetrics,
18134
18303
  violations,
18135
18304
  dependency_graph: importGraph,
@@ -18145,7 +18314,7 @@ function offenders(root = "src", top = 20) {
18145
18314
  return { offenders: [], summary: { error: analysis.error } };
18146
18315
  }
18147
18316
  const items = [];
18148
- for (const fn of analysis.most_complex_functions || []) {
18317
+ for (const fn of analysis.all_functions || analysis.most_complex_functions || []) {
18149
18318
  const cc = fn.complexity;
18150
18319
  if (cc > 10) {
18151
18320
  items.push({