@filipebraida/adonis-function-points 0.8.0 → 0.10.0

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
@@ -6,6 +6,105 @@ release moves the number for unchanged code, the rule set version moves with it
6
6
  otherwise the difference would measure the tool's change rather than the work, and
7
7
  that difference becomes an invoice.
8
8
 
9
+ ## 0.10.0
10
+
11
+ **Rule set `afp@1.9.0`.** One rule and two report fixes, from a team's third review of their
12
+ own count — the one they accepted as a baseline. The rule moves no point on the three
13
+ validated applications and no point on its fixture: it moves a gap from the wrong line to
14
+ the right one. A 0.9.0 baseline still has to be recounted, because the rule set says so.
15
+
16
+ ### Fixed
17
+
18
+ - **A warning names a transaction the way the table and the config keys do.** Four warnings
19
+ printed the route's own pattern (`GET /orders/:id`) where the table, `fp:explain` and the
20
+ keys of `overrides` / `boundary.ignoreEntryPoints` use the identity (`GET /orders/:param`,
21
+ §5). Copying from a warning into the configuration required a translation nobody was told
22
+ about. All of them print the identity now.
23
+ - **"leaves whole — page never reads X" contradicted itself.** The rule was right — a store
24
+ handed to a page that uses nothing of it the reader can see leaves whole, in the open, never
25
+ as a floor — and the sentence was not. It says now: "handed to page "x", and nothing the
26
+ reader can see uses X: every column counted, in the open".
27
+
28
+ ### Counting
29
+
30
+ - **The value of a transaction callback is what it returns.** `const { row } = await
31
+ db.transaction(async (trx) => { … return { row: created } })`, then `row.save()`: the write
32
+ was an unreadable receiver. The callback is a body — its own locals are bound first, in
33
+ source order — and every `return` names the store the value holds, whole or under one key
34
+ of a returned literal; `Model.transaction` and `trx.transaction` are the same shape. A
35
+ returned number binds nothing; a raw-query row nobody can type stays reported. On the three
36
+ applications the value of a transaction callback is used 17 times. The 0.8 rule "a value a
37
+ package built is not a store" narrows to a call with no function argument: `db` is a
38
+ package import, and `db.transaction(cb)` hands back the application's value.
39
+
40
+ ### Documented
41
+
42
+ - counting-decisions §3 gains the transaction-callback row of the bindings table.
43
+ - Fixtures: `transacao_valor` (18 FP — the same before and after; one unresolved call moves
44
+ from `row.save` to the raw-query row that deserves it); `inertia_pages` grows a page that
45
+ uses nothing of its rows (29 FP).
46
+ - Measured and set aside: a model's instance method that writes — one in 52 models on the
47
+ three applications, not a rule.
48
+
49
+ ## 0.9.0
50
+
51
+ **Rule set `afp@1.8.0`.** Two rules from a team's second review of their own count, one
52
+ report fixed, and a rule about the library itself. A 0.8.0 baseline has to be recounted:
53
+ the totals move 0, −3 and +1 FP — a writing transaction leaves EO for EI, two EIs gain an
54
+ FTR — and the coverage line now means one thing in both reports.
55
+
56
+ ### Fixed
57
+
58
+ - **The unresolved calls are listed, once each, and the inventory and the count show one
59
+ number.** The 0.8 CHANGELOG said `fp:inventory` listed them; it printed a total — and the
60
+ count printed a different total, because it summed each transaction's unresolved calls
61
+ (a body five routes reach counted five times) while the inventory added the route and
62
+ store problems. Now `inventory.unresolved` and `count.confidence.unresolved` are the same
63
+ list — one entry per site (`file:line expression — reason`, how many transactions reach
64
+ it), route and store problems included — and `unresolvedCalls` is its length in both.
65
+ `fp:inventory` prints every site, `fp:count` up to 25, `--json` / `--out` carry them. The
66
+ number changes without any code changing: it counts places to look at now, which is what
67
+ a person acts on.
68
+ - **A method the model declares is application code.** `const items = await
69
+ order.pendingItems().forUpdate()`, then `item.save()` in a loop: the rows come from a
70
+ method declared on the model class, and the chain was read as an access to the model at
71
+ its root — the write went to the wrong store, or was reported unreadable. The method's
72
+ return annotation (`typeof Item`), or its returns when every one is `Item.query()…`, names
73
+ the store; a builder chain after it hands the same rows on, an aggregate a number; the
74
+ chain is read from its innermost call outwards. On the reviewing team's application the
75
+ one writing transaction still counted as EO after 0.8 turns EI (7 → 4 FP).
76
+
77
+ ### New
78
+
79
+ - **A listener written inline is a listener, and a string event is an event.**
80
+ `emitter.on('order:closed', async function ({ orderId }) { … })` binds a body to a
81
+ string, and the collector read only `emitter.on(EventClass, [ListenerClass])`: an
82
+ application that binds every listener this way had none followed, its jobs "reached by no
83
+ transaction", their writes nobody's FTR. The inline function or arrow is a handler located
84
+ by its line; the string is a binding key that `emitter.emit('…', payload)` / `emitSerial`
85
+ reach — the same decision as `Event.dispatch()`; a name built at runtime binds nothing;
86
+ `start/**` is scanned. Fixture `eventos_inline`: 27 FP where the previous rule set said 13.
87
+
88
+ ### The library itself
89
+
90
+ - **The library names no application.** Its rules are found by recounting real
91
+ applications, and their names had leaked in: two heuristics keyed on one team's result
92
+ keys and one front-end's page layout, forty comments quoting somebody's routes and models,
93
+ a few shipped-doc lines. A heuristic keyed on one application's vocabulary is not a rule.
94
+ The result-key list keeps only framework and language conventions (one list page of one
95
+ application goes from 76 back to 83 DET, same FP); a module-scoped `pages/` directory is
96
+ read structurally; comments illustrate with the library's own domain; docs cite the
97
+ measurement, not the domain. `tests/unit/no_project_literals.spec.ts` holds the words that
98
+ belong to the validated applications — in the test, nowhere else — and fails when one
99
+ appears in `src`, `docs`, README or CHANGELOG.
100
+
101
+ ### Documented
102
+
103
+ - counting-decisions §3 gains the model-method row of the bindings table; §9 gains "A
104
+ listener written inline is a listener; a string event is an event".
105
+ - Fixtures: `escritas_indiretas` grows to 97 FP (a model's own query method);
106
+ `eventos_inline` (27 FP) is new.
107
+
9
108
  ## 0.8.0
10
109
 
11
110
  **Rule set `afp@1.7.0`.** One rule, wide, found by a team reviewing a 0.6.0 count of
@@ -44,7 +143,7 @@ new X()`, `(await q.first()) ?? new X()`), as `auth.user` / `auth.getUserOrFail(
44
143
  - **A service is what the container returns.** `const svc = await
45
144
  app.container.make(X)` binds `svc` to X as `new X()` already did, so `svc.method()` is
46
145
  followed. On the reviewed application this shape carried the write of
47
- `POST /gestao/atribuicao` and 31 more sites.
146
+ the assignment route and 31 more sites.
48
147
  - A method chain on store rows handed to a delivery (`rows.map(f).join('\n')`) delivers
49
148
  the rows, not one field.
50
149
 
@@ -60,7 +159,7 @@ app.container.make(X)` binds `svc` to X as `new X()` already did, so `svc.method
60
159
  spread, a function receiving the rows, a package's `<DataTable data={…} />`, two files
61
160
  answering to one name, one prop carrying several stores, members that are not columns
62
161
  — the store leaves whole and the count says why, by transaction. Measured on the three
63
- applications: **0 FP moved** (sae: three pages read, 22 stores reported; the other two
162
+ applications: **0 FP moved** (one of them: three pages read, 22 stores reported; the other two
64
163
  hand every raw store through a transformer). Fixture `inertia_pages` (25 FP): the DETs
65
164
  change, the points do not.
66
165
 
@@ -102,10 +201,10 @@ and most of that is new elementary processes the count had never seen.
102
201
  attribute; a yes/no, a formatted value, a framework service's answer and an Inertia
103
202
  lazy prop are one value. Without a delivery point the output falls back to the
104
203
  stores read, as before. On the applications: a home page at 1 DET became 10; a
105
- petitions list at 8 DET became 60 — its nested transformers, which the unresolved
204
+ list page at 8 DET became 60 — its nested transformers, which the unresolved
106
205
  variant had hidden; every listing page stopped counting its filters twice.
107
206
  - **A function of the same file is followed** (`local-function`), and so is
108
- `rows.map(paraLinha)`. Not only DETs: the stores those helpers read are FTRs now, and
207
+ `rows.map(toRow)`. Not only DETs: the stores those helpers read are FTRs now, and
109
208
  two `POST`s that wrote through a local helper moved from EO to EI. Models and modules
110
209
  imported **inside** a body (`const { default: X } = await import('#…')`) bind like a
111
210
  static import — an importer that wrote four tables this way had touched nothing.
@@ -183,7 +282,7 @@ of it is EOs losing DETs they never showed and one data function that was four.
183
282
  benchmark depends on it); a child hanging off two parents stays apart and the report
184
283
  says why. Cascade delete was measured and rejected as the signal: on one application
185
284
  11 of 13 cascades pointed at the tenant table. On the three applications the rule
186
- folds four stores in all — the `Inpi*` mirror of an external registry becomes **one**
285
+ folds four stores in all — the four-table mirror of an external registry becomes **one**
187
286
  EIF with 4 RET, which is what the CPM says.
188
287
  - **A data function is identified by its table**, as counting-decisions §5 always said.
189
288
  Keyed by the class, renaming a model billed as a deletion plus an addition.
package/README.md CHANGED
@@ -13,7 +13,7 @@ node ace fp:count
13
13
 
14
14
  ```
15
15
  Unadjusted count: 46 FP
16
- Ruleset: afp@1.7.0
16
+ Ruleset: afp@1.9.0
17
17
 
18
18
  type n FP
19
19
  ILF 2 14
@@ -14,27 +14,27 @@
14
14
  const commands = [
15
15
  {
16
16
  commandName: "fp:inventory",
17
- importer: () => import("../fp_inventory-B322MvZT.js")
17
+ importer: () => import("../fp_inventory-BJlIoetL.js")
18
18
  },
19
19
  {
20
20
  commandName: "fp:metrics",
21
- importer: () => import("../fp_metrics-DjADb7F6.js")
21
+ importer: () => import("../fp_metrics-BDKaNL0B.js")
22
22
  },
23
23
  {
24
24
  commandName: "fp:count",
25
- importer: () => import("../fp_count-B_KPNKMO.js")
25
+ importer: () => import("../fp_count-B5hxDyGJ.js")
26
26
  },
27
27
  {
28
28
  commandName: "fp:explain",
29
- importer: () => import("../fp_explain-C2s6Kpaj.js")
29
+ importer: () => import("../fp_explain-C4X_-Rp7.js")
30
30
  },
31
31
  {
32
32
  commandName: "fp:diff",
33
- importer: () => import("../fp_diff-B3NhXzrb.js")
33
+ importer: () => import("../fp_diff-7S8NcKIY.js")
34
34
  },
35
35
  {
36
36
  commandName: "fp:calibrate",
37
- importer: () => import("../fp_calibrate-B_oD9b02.js")
37
+ importer: () => import("../fp_calibrate-DUPUXVaJ.js")
38
38
  }
39
39
  ];
40
40
  let cache = null;
@@ -1,4 +1,4 @@
1
- import { s as printResult, t as runCalibrate } from "./runners-BpBJsGMj.js";
1
+ import { s as printResult, t as runCalibrate } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_calibrate.ts
@@ -1,4 +1,4 @@
1
- import { n as runCount, s as printResult } from "./runners-BpBJsGMj.js";
1
+ import { n as runCount, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_count.ts
@@ -1,4 +1,4 @@
1
- import { r as runDiff, s as printResult } from "./runners-BpBJsGMj.js";
1
+ import { r as runDiff, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_diff.ts
@@ -1,4 +1,4 @@
1
- import { i as runExplain, s as printResult } from "./runners-BpBJsGMj.js";
1
+ import { i as runExplain, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_explain.ts
@@ -1,4 +1,4 @@
1
- import { a as runInventory, s as printResult } from "./runners-BpBJsGMj.js";
1
+ import { a as runInventory, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_inventory.ts
@@ -1,4 +1,4 @@
1
- import { o as runMetrics, s as printResult } from "./runners-BpBJsGMj.js";
1
+ import { o as runMetrics, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_metrics.ts
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { configure } from "./configure.js";
2
2
  import { a as AEP_FACTORS, c as IncomparableSourcesError, f as defineConfig, i as measureStructure, l as SISP_FACTORS, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
3
3
  import "./src/types.js";
4
- import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-DhJO-qvQ.js";
5
- import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-C6kHKB9-.js";
4
+ import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-CneCj3sT.js";
5
+ import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-4sZBYGKU.js";
6
6
  export { AEP_FACTORS, BUILTIN_CALL_RESOLVERS, CoverageTooLowError, DEFAULT_TECHNICAL_PATTERNS, FACTOR_PRESETS, IncomparableRulesetsError, IncomparableSourcesError, RULESET, RULESET_VERSION, SISP_FACTORS, analyze, calibrate, configure, defineConfig, diffCounts, ignoreCalls, measureConformance, measureStructure, parseSamples };
@@ -1,4 +1,4 @@
1
- import { _ as relativeTo, a as chainShapeOf, c as unwrap$1, d as collectEventBindings, f as detectAccess, g as isSeeder, h as isApplicationCode, i as resolveCall, l as DISPATCH_METHODS, m as rootSymbolOf, o as mappedLiteralOf, p as hooksFiredBy, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as EXECUTION_METHODS, v as samePath, y as toPosix } from "./resolvers-DhJO-qvQ.js";
1
+ import { _ as relativeTo, a as chainShapeOf, c as unwrap$1, d as collectEventBindings, f as detectAccess, g as isSeeder, h as isApplicationCode, i as resolveCall, l as DISPATCH_METHODS, m as rootSymbolOf, o as mappedLiteralOf, p as hooksFiredBy, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as EXECUTION_METHODS, v as samePath, y as toPosix } from "./resolvers-CneCj3sT.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path, { dirname, join, resolve } from "node:path";
4
4
  import { Node, Project, SyntaxKind } from "ts-morph";
@@ -969,7 +969,7 @@ const problemAt = (call, expression, reason) => ({
969
969
  /**
970
970
  * Ace commands as entry points — counting-decisions §5, plan 0.7 §C.
971
971
  *
972
- * IFPUG counts batch processes an operator starts. `node ace noticias:importar`
972
+ * IFPUG counts batch processes an operator starts. `node ace articles:import`
973
973
  * reads a feed and writes news: an EI exactly like a `POST`, and until this the
974
974
  * collector emitted only `kind: 'http'`, although the type had `command` and §5
975
975
  * had decided its identity since 0.1.0.
@@ -1036,7 +1036,7 @@ function baseCommandNameIn(file) {
1036
1036
  }
1037
1037
  return null;
1038
1038
  }
1039
- /** `static commandName = 'noticias:importar'` — a literal; a computed name is nobody's identity */
1039
+ /** `static commandName = 'articles:import'` — a literal; a computed name is nobody's identity */
1040
1040
  function commandNameOf(cls) {
1041
1041
  const property = cls.getStaticProperty("commandName");
1042
1042
  if (!property || !Node.isPropertyDeclaration(property)) return null;
@@ -1190,7 +1190,8 @@ const FRAMEWORK_SERVICES = new Set([
1190
1190
  "bouncer",
1191
1191
  "ui",
1192
1192
  "colors",
1193
- "app"
1193
+ "app",
1194
+ "prompt"
1194
1195
  ]);
1195
1196
  /** Is this call one that cannot reach a data store? */
1196
1197
  function isNoise(call, owner) {
@@ -1295,15 +1296,17 @@ const PASSES_THROUGH$1 = new Set([
1295
1296
  "toJSON",
1296
1297
  "serialize"
1297
1298
  ]);
1298
- /** keys of a result that carry its rows: picking one of these is not picking one value */
1299
+ /**
1300
+ * Keys under which a paginator or a wrapper hands its rows on: picking one of these
1301
+ * is not picking one value. Framework and JavaScript conventions only — a key named
1302
+ * in one application's language is that application's, not a rule.
1303
+ */
1299
1304
  const PASSES_ROWS = new Set([
1300
1305
  "data",
1301
1306
  "rows",
1302
1307
  "items",
1303
1308
  "results",
1304
- "list",
1305
- "linhas",
1306
- "itens"
1309
+ "records"
1307
1310
  ]);
1308
1311
  /** properties of a result that are one value, not its rows */
1309
1312
  const SCALAR_PROPS = new Set([
@@ -1790,7 +1793,7 @@ function classifyCall(value, path, ctx, out, depth, pick) {
1790
1793
  /**
1791
1794
  * `rows.map(…).join('\n')` in a body whose parameter is `rows`: the whole input
1792
1795
  * transformed, so the input says what leaves — the caller's arguments decide,
1793
- * and the body itself says nothing. `noticia.publicadaEm?.toISO()` on that
1796
+ * and the body itself says nothing. `article.publicadaEm?.toISO()` on that
1794
1797
  * parameter is one FIELD of it, and stays one value below.
1795
1798
  */
1796
1799
  if (Node.isPropertyAccessExpression(callee) && transformsParameter(callee, ctx.body)) {
@@ -1803,7 +1806,7 @@ function classifyCall(value, path, ctx, out, depth, pick) {
1803
1806
  }
1804
1807
  if (Node.isPropertyAccessExpression(callee)) {
1805
1808
  /**
1806
- * `comunicado.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
1809
+ * `notice.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
1807
1810
  * the chain, or an expression, then a method — one value. A chain rooted at
1808
1811
  * `this` (`this.service.find()`) is a call into a service, and not this.
1809
1812
  */
@@ -1838,7 +1841,7 @@ function classifyCall(value, path, ctx, out, depth, pick) {
1838
1841
  }
1839
1842
  /**
1840
1843
  * `xs.map(cb)` with no literal in the callback: what the callback RETURNS,
1841
- * once — a function passed by reference (`rows.map(paraLinha)`) is a call to
1844
+ * once — a function passed by reference (`rows.map(toRow)`) is a call to
1842
1845
  * that function over the rows; an expression body (`(m) => new T(m).toObject()`)
1843
1846
  * is classified as if it were the value; anything else is one repeating attribute.
1844
1847
  */
@@ -1877,7 +1880,7 @@ function callbackValueOf(callback) {
1877
1880
  }
1878
1881
  /**
1879
1882
  * Does this chain read a property (not a method) or hold an expression before
1880
- * the method is applied? `comunicado.enviadoEm.toISO()` does; `rows.map(f).join()`
1883
+ * the method is applied? `notice.enviadoEm.toISO()` does; `rows.map(f).join()`
1881
1884
  * does not; a chain rooted at `this` is a service, and does not.
1882
1885
  */
1883
1886
  function readsField(node) {
@@ -1906,7 +1909,7 @@ function readsField(node) {
1906
1909
  }
1907
1910
  /**
1908
1911
  * Is this chain a method (or methods) applied to a plain parameter of the body,
1909
- * with no field read in between? `rows.map(f).join(s)` is; `noticia.capa?.toISO()`
1912
+ * with no field read in between? `rows.map(f).join(s)` is; `article.capa?.toISO()`
1910
1913
  * reads a field first and is not.
1911
1914
  */
1912
1915
  function transformsParameter(callee, body) {
@@ -1956,7 +1959,7 @@ function returnsPrimitive(call) {
1956
1959
  }
1957
1960
  }
1958
1961
  /**
1959
- * `x.data`, `resultado.linhas`, `rows[0]`: a part of a value the body holds.
1962
+ * `x.data`, `result.rows`, `rows[0]`: a part of a value the body holds.
1960
1963
  *
1961
1964
  * on a variable bound to a followed call that key of what the call returns
1962
1965
  * on a store-bound variable the store (`rows[0]`, `.data`) or a field (scalar)
@@ -1993,7 +1996,7 @@ function classifyAccess(value, path, ctx, out, depth) {
1993
1996
  return;
1994
1997
  }
1995
1998
  /**
1996
- * `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
1999
+ * `result.rows`: one key of what the call returns; `meta.pagina` on a
1997
2000
  * destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
1998
2001
  */
1999
2002
  const own = property && !PASSES_THROUGH$1.has(property) ? property : void 0;
@@ -2165,7 +2168,7 @@ function readPages(deliveries, env) {
2165
2168
  for (const { store } of readable) {
2166
2169
  if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
2167
2170
  const members = [...reading.unknownMembers.get(store) ?? []];
2168
- markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}" never reads ${store}`);
2171
+ markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `handed to page "${page}", and nothing the reader can see uses ${store}: every column counted, in the open`);
2169
2172
  }
2170
2173
  }
2171
2174
  return reading;
@@ -2183,33 +2186,37 @@ const SKIPPED_DIRS = new Set([
2183
2186
  ".adonisjs"
2184
2187
  ]);
2185
2188
  /**
2186
- * `inertia.render('livros/index')` → `inertia/pages/livros/index.tsx` (the default),
2187
- * `app/<first>/ui/pages/<rest>.tsx` (a domain-module layout), or any `pages/` directory
2188
- * under the root holding that path. The `resolve` function of the front-end is not
2189
+ * `inertia.render('livros/index')` → `inertia/pages/livros/index.tsx` (the default), or any
2190
+ * `pages/` directory under the root holding that path — a module-scoped one included. The `resolve` function of the front-end is not
2189
2191
  * run — a convention is read, a function is not.
2190
2192
  */
2191
2193
  function inertiaPageFiles(root, page) {
2192
2194
  const found = /* @__PURE__ */ new Set();
2195
+ /**
2196
+ * `orders/show` → `<…>/orders/<…>/pages/show.tsx`: a front-end that keeps one `pages/`
2197
+ * directory per module names the module in the page name's first segment. Read
2198
+ * structurally — a `pages/` directory whose path carries that segment — never as a
2199
+ * fixed layout: the default is `inertia/pages/<name>`, and anything else is one
2200
+ * application's convention.
2201
+ */
2193
2202
  const [first, ...rest] = page.split("/");
2194
- if (rest.length > 0) for (const extension of [
2195
- ".tsx",
2196
- ".jsx",
2197
- ".vue",
2198
- ".svelte"
2199
- ]) {
2200
- const file = join(root, "app", first, "ui", "pages", `${rest.join("/")}${extension}`);
2201
- if (existsSync(file)) found.add(file);
2202
- }
2203
- for (const dir of pagesDirectories(root)) for (const extension of [
2204
- ".tsx",
2205
- ".jsx",
2206
- ".vue",
2207
- ".svelte"
2208
- ]) {
2209
- const file = join(dir, `${page}${extension}`);
2210
- if (existsSync(file)) found.add(file);
2211
- const index = join(dir, page, `index${extension}`);
2212
- if (existsSync(index)) found.add(index);
2203
+ for (const dir of pagesDirectories(root)) {
2204
+ const scopedToModule = rest.length > 0 && toPosix(dir).split("/").includes(first);
2205
+ for (const extension of [
2206
+ ".tsx",
2207
+ ".jsx",
2208
+ ".vue",
2209
+ ".svelte"
2210
+ ]) {
2211
+ const file = join(dir, `${page}${extension}`);
2212
+ if (existsSync(file)) found.add(file);
2213
+ const index = join(dir, page, `index${extension}`);
2214
+ if (existsSync(index)) found.add(index);
2215
+ if (scopedToModule) {
2216
+ const scoped = join(dir, `${rest.join("/")}${extension}`);
2217
+ if (existsSync(scoped)) found.add(scoped);
2218
+ }
2219
+ }
2213
2220
  }
2214
2221
  return [...found].sort();
2215
2222
  }
@@ -3031,6 +3038,86 @@ function createAnalyzer(app, stores, options = {}) {
3031
3038
  }
3032
3039
  return found.size === 1 ? [...found][0] : null;
3033
3040
  };
3041
+ /**
3042
+ * `order.pendingItems({ client })`: a method declared on the model class, returning
3043
+ * `Item.query()…`. The model file is application code, and the method's annotation
3044
+ * (`typeof Item` names the store) or its returns say what it hands back (plan 0.9 §B).
3045
+ */
3046
+ const modelMethodStore = (callee, scope) => {
3047
+ const root = rootSymbolOf(callee.getExpression());
3048
+ const store = root ? scope.get(root) : void 0;
3049
+ if (!store) return null;
3050
+ const declared = storesByName.get(store);
3051
+ const method = (declared ? sourceFile(declared.provenance.file) : null)?.getClasses().flatMap((c) => c.getMethods()).find((m) => m.getName() === callee.getName());
3052
+ if (!method) return null;
3053
+ const annotation = method.getReturnTypeNode()?.getText();
3054
+ if (annotation) {
3055
+ const named = annotation.match(/typeof\s+([A-Za-z_]\w*)/)?.[1];
3056
+ return storeNamedBy(named ?? annotation, storesByName);
3057
+ }
3058
+ const found = /* @__PURE__ */ new Set();
3059
+ for (const statement of method.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
3060
+ const value = statement.getExpression();
3061
+ if (!value) return null;
3062
+ const returnedRoot = rootSymbolOf(unwrapAwait(value));
3063
+ if (returnedRoot && storesByName.has(returnedRoot)) found.add(returnedRoot);
3064
+ else return null;
3065
+ }
3066
+ return found.size === 1 ? [...found][0] : null;
3067
+ };
3068
+ /**
3069
+ * `await db.transaction(async (trx) => { … return … })` — also `Model.transaction`
3070
+ * and `trx.transaction` (a savepoint): a body whose VALUE is what it returns. The
3071
+ * callback's own locals are bound first, in source order, so `return { row: created }`
3072
+ * can be read; then every return names one store, or nothing binds (plan 0.10 §B).
3073
+ */
3074
+ const transactionCallbackOf = (call) => {
3075
+ const callee = call.getExpression();
3076
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "transaction") return null;
3077
+ return call.getArguments().find((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a)) ?? null;
3078
+ };
3079
+ const returnsOf = (callback) => {
3080
+ if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return [];
3081
+ const callbackBody = callback.getBody();
3082
+ if (!Node.isBlock(callbackBody)) return [callbackBody];
3083
+ const returned = [];
3084
+ for (const statement of callbackBody.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
3085
+ const owner = statement.getFirstAncestor((n) => Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) || Node.isMethodDeclaration(n));
3086
+ const expression = statement.getExpression();
3087
+ if (owner === callback && expression) returned.push(expression);
3088
+ }
3089
+ return returned;
3090
+ };
3091
+ const bindCallbackLocals = (callback) => {
3092
+ for (const declaration of callback.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3093
+ const nameNode = declaration.getNameNode();
3094
+ const initializer = declaration.getInitializer();
3095
+ if (!initializer || !Node.isIdentifier(nameNode)) continue;
3096
+ const store = storeOfValue(initializer);
3097
+ if (store) symbols.set(nameNode.getText(), store);
3098
+ }
3099
+ };
3100
+ /** the store every return of the callback names — whole, or under one key of a returned literal */
3101
+ const transactionValueStore = (callback, key) => {
3102
+ bindCallbackLocals(callback);
3103
+ const returned = returnsOf(callback);
3104
+ if (returned.length === 0) return null;
3105
+ const found = /* @__PURE__ */ new Set();
3106
+ for (const expression of returned) {
3107
+ let value = unwrapAwait(expression);
3108
+ if (key) {
3109
+ if (!Node.isObjectLiteralExpression(value)) return null;
3110
+ const property = value.getProperty(key);
3111
+ if (!property) return null;
3112
+ value = Node.isPropertyAssignment(property) ? property.getInitializer() : Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
3113
+ if (!value) return null;
3114
+ }
3115
+ const store = storeOfValue(value);
3116
+ if (!store) return null;
3117
+ found.add(store);
3118
+ }
3119
+ return found.size === 1 ? [...found][0] : null;
3120
+ };
3034
3121
  const isAuthUser = (node) => {
3035
3122
  if (!authUserStore) return false;
3036
3123
  const chain = Node.isCallExpression(node) ? node.getExpression() : node;
@@ -3062,9 +3149,27 @@ function createAnalyzer(app, stores, options = {}) {
3062
3149
  if (isAuthUser(node)) return authUserStore;
3063
3150
  if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
3064
3151
  if (Node.isCallExpression(node)) {
3152
+ const callee = node.getExpression();
3153
+ const callback = transactionCallbackOf(node);
3154
+ if (callback) return transactionValueStore(callback);
3155
+ if (Node.isPropertyAccessExpression(callee)) {
3156
+ /**
3157
+ * `q.forUpdate()`, `q.where(…)`: a query-builder chain hands the same rows on —
3158
+ * read from the innermost call outwards, so a model's method at the root of
3159
+ * the chain is seen before Lucid's API is assumed for the whole chain. An
3160
+ * aggregate (`.count()`) hands back a number.
3161
+ */
3162
+ const receiver = unwrapAwait(callee.getExpression());
3163
+ if (Node.isCallExpression(receiver)) {
3164
+ if (AGGREGATES.has(callee.getName())) return null;
3165
+ const inner = storeOfValue(receiver, depth + 1);
3166
+ if (inner) return inner;
3167
+ }
3168
+ const fromModel = modelMethodStore(callee, symbols);
3169
+ if (fromModel) return fromModel;
3170
+ }
3065
3171
  const access = detectAccess(node, symbols, relationsByStore);
3066
3172
  if (access) return access.method === "related" && access.viaRelation ? access.viaRelation : access.store;
3067
- const callee = node.getExpression();
3068
3173
  if (Node.isPropertyAccessExpression(callee) && ONE_OF_ROWS.has(callee.getName())) return storeOfValue(callee.getExpression(), depth + 1);
3069
3174
  return returnedStoreOf(node);
3070
3175
  }
@@ -3086,9 +3191,22 @@ function createAnalyzer(app, stores, options = {}) {
3086
3191
  const nameNode = node.getNameNode();
3087
3192
  if (!initializer) return;
3088
3193
  if (Node.isIdentifier(nameNode)) {
3089
- if (symbols.has(nameNode.getText())) return;
3194
+ /**
3195
+ * Re-read even when the parameter pass already bound the name by the chain's
3196
+ * root: `const items = await order.pendingItems().forUpdate()` is rooted at
3197
+ * `order` and holds `Item` rows — what the model's method returns wins over
3198
+ * where the chain started.
3199
+ */
3090
3200
  const store = storeOfValue(initializer);
3091
3201
  if (store) symbols.set(nameNode.getText(), store);
3202
+ } else if (Node.isObjectBindingPattern(nameNode)) {
3203
+ const call = unwrapAwait(initializer);
3204
+ const callback = Node.isCallExpression(call) ? transactionCallbackOf(call) : null;
3205
+ if (!callback) return;
3206
+ for (const element of nameNode.getElements()) {
3207
+ const store = transactionValueStore(callback, element.getPropertyNameNode()?.getText() ?? element.getName());
3208
+ if (store) symbols.set(element.getName(), store);
3209
+ }
3092
3210
  }
3093
3211
  return;
3094
3212
  }
@@ -3096,7 +3214,7 @@ function createAnalyzer(app, stores, options = {}) {
3096
3214
  const declared = node.getInitializer();
3097
3215
  if (!Node.isVariableDeclarationList(declared)) return;
3098
3216
  const nameNode = declared.getDeclarations()[0]?.getNameNode();
3099
- if (!nameNode || !Node.isIdentifier(nameNode) || symbols.has(nameNode.getText())) return;
3217
+ if (!nameNode || !Node.isIdentifier(nameNode)) return;
3100
3218
  const store = storeOfValue(node.getExpression());
3101
3219
  if (store) symbols.set(nameNode.getText(), store);
3102
3220
  return;
@@ -3107,7 +3225,7 @@ function createAnalyzer(app, stores, options = {}) {
3107
3225
  const callback = node.getArguments()[0];
3108
3226
  if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return;
3109
3227
  const parameter = callback.getParameters()[0]?.getNameNode();
3110
- if (!parameter || !Node.isIdentifier(parameter) || symbols.has(parameter.getText())) return;
3228
+ if (!parameter || !Node.isIdentifier(parameter)) return;
3111
3229
  const store = storeOfValue(callee.getExpression());
3112
3230
  if (store) symbols.set(parameter.getText(), store);
3113
3231
  }
@@ -3176,8 +3294,8 @@ function createAnalyzer(app, stores, options = {}) {
3176
3294
  * A select list that is not literal is reported, and the store falls back
3177
3295
  * to every column, which overestimates in the open.
3178
3296
  *
3179
- * `related('itens').query().count()` reads the RELATION target, and the
3180
- * parent only as a receiver; `preload('itens')` reads the target whole.
3297
+ * `related('items').query().count()` reads the RELATION target, and the
3298
+ * parent only as a receiver; `preload('items')` reads the target whole.
3181
3299
  */
3182
3300
  if (access.mode === "read") {
3183
3301
  const chain = chainShapeOf(call);
@@ -3318,7 +3436,7 @@ function createAnalyzer(app, stores, options = {}) {
3318
3436
  * Both project-wide facts come from one pass, computed once: which stores the
3319
3437
  * application WRITES (maintenance, §6.5.4) and which it ADDRESSES directly
3320
3438
  * (grouping, counting-decisions §10). A store reached only through a relation
3321
- * — `preload('itens')`, `related('itens').create()` — is read or written, but
3439
+ * — `preload('items')`, `related('items').create()` — is read or written, but
3322
3440
  * not addressed: the user never sees it outside its parent.
3323
3441
  */
3324
3442
  let projectWide;
@@ -3465,8 +3583,8 @@ function createAnalyzer(app, stores, options = {}) {
3465
3583
  }
3466
3584
  const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
3467
3585
  /**
3468
- * `egresso.curso` where the body returns the row itself (`return
3469
- * Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
3586
+ * `member.curso` where the body returns the row itself (`return
3587
+ * Member.query()…first()`, path ''): the pick lands INSIDE a returned
3470
3588
  * value — one field of a store is one value; one key of a returned call
3471
3589
  * is that call picked deeper.
3472
3590
  */
@@ -3727,7 +3845,7 @@ function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ ne
3727
3845
  }
3728
3846
  }
3729
3847
  /**
3730
- * `const { default: Noticia } = await import('#noticias/models/noticia')`: a
3848
+ * `const { default: Article } = await import('#articles/models/article')`: a
3731
3849
  * model imported INSIDE the body — an ace command does this to keep the app
3732
3850
  * from booting for `--help`. The store is the same; only the binding moved.
3733
3851
  */
@@ -3808,7 +3926,7 @@ function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ ne
3808
3926
  }
3809
3927
  }
3810
3928
  /**
3811
- * `const { preIntake } = input` where `input.preIntake` is a registered path:
3929
+ * `const { order } = input` where `input.order` is a registered path:
3812
3930
  * each element inherits the store of its path. Read AFTER the parameters, which
3813
3931
  * is where the paths come from.
3814
3932
  */
@@ -3931,6 +4049,17 @@ const ONE_OF_ROWS = new Set([
3931
4049
  "first",
3932
4050
  "last"
3933
4051
  ]);
4052
+ /** query-builder terminals that hand back one number or one yes/no, never rows */
4053
+ const AGGREGATES = new Set([
4054
+ "count",
4055
+ "sum",
4056
+ "avg",
4057
+ "min",
4058
+ "max",
4059
+ "exists",
4060
+ "pojo",
4061
+ "pluck"
4062
+ ]);
3934
4063
  /** array methods whose callback receives one row of the receiver */
3935
4064
  const ITERATES_ROWS = new Set([
3936
4065
  "map",
@@ -4015,8 +4144,8 @@ function importMapsOf(file, app) {
4015
4144
  }
4016
4145
  }
4017
4146
  /**
4018
- * `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
4019
- * `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
4147
+ * `const { default: SyncCatalog } = await import('#catalog/actions/sync_catalog')`
4148
+ * `const { runInProgress } = await import('#catalog/services/runs')`
4020
4149
  *
4021
4150
  * A module imported INSIDE a body — the shape ace commands use so `--help` does
4022
4151
  * not boot the application. The binding moved; the body it names did not, and
@@ -4128,8 +4257,15 @@ function isUnreadableWrite(call, symbols, imports, packageImports, body) {
4128
4257
  const nameNode = declaration.getNameNode();
4129
4258
  if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
4130
4259
  const initializer = declaration.getInitializer();
4131
- const origin = initializer ? rootSymbolOf(unwrapAwait(initializer)) : null;
4132
- if (origin && packageImports.has(origin)) return false;
4260
+ const initialized = initializer ? unwrapAwait(initializer) : null;
4261
+ /**
4262
+ * `await db.transaction(async (trx) => …)` is rooted at a package too, but its value
4263
+ * is what the CALLBACK returns — the application's. Only a call with no function
4264
+ * argument is a package's own object.
4265
+ */
4266
+ const handsBackACallback = initialized && Node.isCallExpression(initialized) && initialized.getArguments().some((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a));
4267
+ const origin = initialized ? rootSymbolOf(initialized) : null;
4268
+ if (origin && packageImports.has(origin) && !handsBackACallback) return false;
4133
4269
  }
4134
4270
  return true;
4135
4271
  }
@@ -5074,13 +5210,16 @@ const RULESET = "afp";
5074
5210
  * 1.7.0, wide: a write binds to what the variable IS — a destructured named
5075
5211
  * interface, a followed method's return, a relation off a row, a loop over rows,
5076
5212
  * the guard's user, a service the container made — and a write nobody can type is
5077
- * an unresolved call instead of silence.
5213
+ * an unresolved call instead of silence. Two in 1.8.0: a method the model
5214
+ * declares names the store its caller writes, and a listener written inline on a
5215
+ * string event is followed like a listener class. One in 1.9.0: the value of a
5216
+ * transaction callback is what its returns name.
5078
5217
  *
5079
5218
  * Without the bump, a baseline saved by the previous version compares cleanly
5080
5219
  * against this one and bills the tool's own improvement as work done. The guard
5081
5220
  * exists for exactly that, and only this constant arms it.
5082
5221
  */
5083
- const RULESET_VERSION = "1.7.0";
5222
+ const RULESET_VERSION = "1.9.0";
5084
5223
  function count(input, options = {}) {
5085
5224
  const warnings = [];
5086
5225
  const usage = usageOf(input);
@@ -5228,7 +5367,7 @@ function unreadableDeliveryWarnings(input) {
5228
5367
  if (blind.length === 0) return [];
5229
5368
  return [
5230
5369
  `${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
5231
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
5370
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.delivered.opaqueFields.join(", ")}`),
5232
5371
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5233
5372
  ];
5234
5373
  }
@@ -5244,7 +5383,7 @@ function unreadablePageWarnings(input) {
5244
5383
  for (const entry of input.entryPoints) {
5245
5384
  const behavior = input.behaviors.get(entry.id);
5246
5385
  if (!behavior || behavior.writes) continue;
5247
- for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.trigger} ${entry.signature}: ${store} leaves whole — ${reason}`);
5386
+ for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.identity}: ${store} leaves whole — ${reason}`);
5248
5387
  }
5249
5388
  if (lines.length === 0) return [];
5250
5389
  return [
@@ -5378,7 +5517,7 @@ function unreadableInputWarnings(input) {
5378
5517
  if (blind.length === 0) return [];
5379
5518
  return [
5380
5519
  `${blind.length} transaction(s) read the request without enumerating fields (\`all()\`, \`body()\`, \`except()\`), so their input DETs could not be counted and each sits at the floor of its band. This UNDERSTATES the total — the fix is a validator, not a configuration:`,
5381
- ...blind.slice(0, 10).map(({ entry }) => ` ${entry.trigger} ${entry.signature}`),
5520
+ ...blind.slice(0, 10).map(({ entry }) => ` ${entry.identity}`),
5382
5521
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5383
5522
  ];
5384
5523
  }
@@ -5399,7 +5538,7 @@ function unreadableOutputWarnings(input) {
5399
5538
  if (blind.length === 0) return [];
5400
5539
  return [
5401
5540
  `${blind.length} transaction(s) pass through a transformer that spreads something the analysis cannot read, counted as 1 DET each — a FLOOR. This UNDERSTATES the output; the fix is in the transformer (\`this.pick(...)\` or named keys), not a configuration:`,
5402
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.opaqueOutputFields.join(", ")}`),
5541
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.opaqueOutputFields.join(", ")}`),
5403
5542
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5404
5543
  ];
5405
5544
  }
@@ -5537,20 +5676,32 @@ function totalsOf(functions) {
5537
5676
  * reader must see that without having to go looking.
5538
5677
  */
5539
5678
  function confidenceOf(input, warnings) {
5540
- let unresolvedCalls = 0;
5541
5679
  let withoutHandler = 0;
5680
+ const sites = /* @__PURE__ */ new Map();
5681
+ for (const site of input.unresolved ?? []) sites.set(`${site.file}:${site.line}:${site.expression}`, { ...site });
5542
5682
  for (const entry of input.entryPoints) {
5543
5683
  const behavior = input.behaviors.get(entry.id);
5544
5684
  if (!behavior) {
5545
5685
  withoutHandler++;
5546
5686
  continue;
5547
5687
  }
5548
- unresolvedCalls += behavior.unresolved.length;
5688
+ if (input.unresolved) continue;
5689
+ for (const call of behavior.unresolved) {
5690
+ const key = `${call.file}:${call.line}:${call.expression}`;
5691
+ const site = sites.get(key);
5692
+ if (site) site.transactions++;
5693
+ else sites.set(key, {
5694
+ ...call,
5695
+ transactions: 1
5696
+ });
5697
+ }
5549
5698
  }
5699
+ const unresolved = [...sites.values()].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
5550
5700
  return {
5551
- unresolvedCalls,
5701
+ unresolvedCalls: unresolved.length,
5552
5702
  entryPointsWithoutHandler: withoutHandler,
5553
- warnings
5703
+ warnings,
5704
+ unresolved
5554
5705
  };
5555
5706
  }
5556
5707
  //#endregion
@@ -5627,7 +5778,27 @@ async function analyze(root, options = {}) {
5627
5778
  });
5628
5779
  const behaviors = new Map(entryPoints.filter((entry) => entry.handler).map((entry) => [entry.id, analyzer.analyze(entry.handler)]));
5629
5780
  const resolved = [...behaviors.values()].filter((behavior) => behavior.unresolved.length === 0).length;
5630
- const unresolvedCalls = storeProblems.length + routeProblems.length + [...behaviors.values()].reduce((total, behavior) => total + behavior.unresolved.length, 0);
5781
+ /**
5782
+ * One site, however many transactions reach it: a body five routes walk is one gap
5783
+ * to close, not five. The count shows the same number — a reviewing team found 10
5784
+ * in one report and 13 in the other and could act on neither.
5785
+ */
5786
+ const sites = /* @__PURE__ */ new Map();
5787
+ for (const problem of [...storeProblems, ...routeProblems]) sites.set(`${problem.file}:${problem.line}:${problem.expression}`, {
5788
+ ...problem,
5789
+ transactions: 0
5790
+ });
5791
+ for (const behavior of behaviors.values()) for (const call of behavior.unresolved) {
5792
+ const key = `${call.file}:${call.line}:${call.expression}`;
5793
+ const site = sites.get(key);
5794
+ if (site) site.transactions++;
5795
+ else sites.set(key, {
5796
+ ...call,
5797
+ transactions: 1
5798
+ });
5799
+ }
5800
+ const unresolvedSites = [...sites.values()].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
5801
+ const unresolvedCalls = unresolvedSites.length;
5631
5802
  /**
5632
5803
  * Every path that LEAVES is relative to the application root.
5633
5804
  *
@@ -5740,7 +5911,11 @@ async function analyze(root, options = {}) {
5740
5911
  entryPointsResolved: resolved,
5741
5912
  unresolvedCalls,
5742
5913
  ratio: entryPoints.length === 0 ? 1 : resolved / entryPoints.length
5743
- }
5914
+ },
5915
+ unresolved: unresolvedSites.map((site) => ({
5916
+ ...site,
5917
+ file: emit(site.file)
5918
+ }))
5744
5919
  };
5745
5920
  const minimum = options.minCoverage ?? 0;
5746
5921
  if (inventory.coverage.ratio < minimum) throw new CoverageTooLowError(inventory.coverage.ratio, minimum);
@@ -5754,6 +5929,7 @@ async function analyze(root, options = {}) {
5754
5929
  behaviors,
5755
5930
  jsonSchemas,
5756
5931
  jobs: collectJobs(app),
5932
+ unresolved: inventory.unresolved,
5757
5933
  writtenAnywhere: analyzer.writtenAnywhere(),
5758
5934
  addressedAnywhere: analyzer.addressedAnywhere(),
5759
5935
  seededAnywhere: analyzer.seededAnywhere()
@@ -387,10 +387,10 @@ function classOfReceiver(receiver) {
387
387
  return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
388
388
  }
389
389
  /**
390
- * `const svc = await app.container.make(IntakeService)`: the container hands
390
+ * `const svc = await app.container.make(AssignmentService)`: the container hands
391
391
  * back an instance of the class named — the same binding as `new`, written the
392
392
  * way a controller writes it when the service has dependencies of its own. On a
393
- * reviewed application this shape carried the write of `POST /gestao/atribuicao`
393
+ * reviewed application this shape carried the write of `POST /assignments`
394
394
  * and 31 more sites, and none was followed (plan 0.8 §B).
395
395
  */
396
396
  if (init?.isKind(SyntaxKind.CallExpression)) {
@@ -414,6 +414,7 @@ function collectEventBindings(app) {
414
414
  compilerOptions: { allowJs: false }
415
415
  });
416
416
  for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
417
+ project.addSourceFilesAtPaths(`${toPosix(app.root)}/start/**/*.ts`);
417
418
  const bindings = /* @__PURE__ */ new Map();
418
419
  for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
419
420
  const expression = call.getExpression();
@@ -421,11 +422,16 @@ function collectEventBindings(app) {
421
422
  if (expression.getName() !== "on") continue;
422
423
  const [event, handlers] = call.getArguments();
423
424
  if (!event || !handlers) continue;
424
- const eventFile = resolveEventClass(event, file, app);
425
- if (!eventFile) continue;
425
+ /**
426
+ * `emitter.on('order:closed', …)`: a STRING event, keyed by its name, which
427
+ * `emitter.emit('order:closed', payload)` reaches. An application that binds every
428
+ * listener this way had none of them followed (plan 0.9 §C).
429
+ */
430
+ const key = Node.isStringLiteral(event) ? eventKey(event.getLiteralValue()) : resolveEventClass(event, file, app);
431
+ if (!key) continue;
426
432
  const refs = listenersOf(handlers, file, app);
427
433
  if (refs.length === 0) continue;
428
- bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
434
+ bindings.set(key, [...bindings.get(key) ?? [], ...refs]);
429
435
  }
430
436
  return bindings;
431
437
  }
@@ -451,10 +457,23 @@ function resolveEventClass(expression, from, app) {
451
457
  return registryEntry(registry, expression.getName(), from.getProject(), app);
452
458
  }
453
459
  /** listener bodies named by the second argument of `emitter.on` */
460
+ /** the binding key of a string event: `emitter.on('order:closed', …)` */
461
+ const eventKey = (name) => `event:${name}`;
454
462
  function listenersOf(handlers, from, app) {
455
463
  const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
456
464
  const refs = [];
457
465
  for (const entry of entries) {
466
+ /**
467
+ * `emitter.on(event, async function (payload) { … })`: the listener IS the body,
468
+ * located by its line — the way a route's inline closure already is a handler.
469
+ */
470
+ if (Node.isArrowFunction(entry) || Node.isFunctionExpression(entry)) {
471
+ refs.push({
472
+ file: toPosix(from.getFilePath()),
473
+ line: entry.getStartLineNumber()
474
+ });
475
+ continue;
476
+ }
458
477
  /**
459
478
  * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
460
479
  * and taking `handle` on faith there would look for a body that is not
@@ -528,6 +547,8 @@ function registryEntry(registryFile, key, project, app) {
528
547
  }
529
548
  //#endregion
530
549
  //#region src/inventory/resolvers/event_dispatch.ts
550
+ /** the emitter's ways of firing a string event */
551
+ const EMITS = new Set(["emit", "emitSerial"]);
531
552
  /**
532
553
  * "Event" pattern: the handler announces, and listeners act.
533
554
  *
@@ -550,6 +571,18 @@ const eventDispatchResolver = {
550
571
  if (ctx.eventBindings.size === 0) return [];
551
572
  const expression = call.getExpression();
552
573
  if (!expression.isKind(SyntaxKind.PropertyAccessExpression)) return [];
574
+ /**
575
+ * `emitter.emit('order:closed', payload)`: a string event reaches the listeners bound
576
+ * to that name — the same decision as `Event.dispatch()`. A name built at runtime
577
+ * binds nothing.
578
+ */
579
+ if (EMITS.has(expression.getName())) {
580
+ const emitterNode = expression.getExpression();
581
+ const isEmitter = Node.isIdentifier(emitterNode) ? emitterNode.getText() === "emitter" : Node.isPropertyAccessExpression(emitterNode) && emitterNode.getName() === "emitter";
582
+ const name = call.getArguments()[0];
583
+ if (!isEmitter || !name || !Node.isStringLiteral(name)) return [];
584
+ return ctx.eventBindings.get(eventKey(name.getLiteralValue())) ?? [];
585
+ }
553
586
  if (expression.getName() !== "dispatch") return [];
554
587
  const receiver = expression.getExpression();
555
588
  if (!Node.isIdentifier(receiver) && !Node.isPropertyAccessExpression(receiver)) return [];
@@ -639,7 +672,7 @@ const jobDispatchResolver = {
639
672
  * "Local function" pattern: a helper declared in the same file, not imported.
640
673
  *
641
674
  * const lista = await proximos(id) // function proximos() { … }
642
- * return rows.map(paraLinha) // const paraLinha = (row) => …
675
+ * return rows.map(toRow) // const toRow = (row) => …
643
676
  *
644
677
  * A query object that keeps its helpers beside it is common, and before this
645
678
  * every such call was unresolved: the store a helper read was reached by nobody
@@ -663,7 +696,7 @@ const localFunctionResolver = {
663
696
  }];
664
697
  }
665
698
  };
666
- /** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
699
+ /** callbacks that apply a function to each element: `rows.map(toRow)` calls `toRow` */
667
700
  const APPLIES_CALLBACK = new Set([
668
701
  "map",
669
702
  "flatMap",
@@ -676,8 +709,8 @@ const APPLIES_CALLBACK = new Set([
676
709
  "reduce"
677
710
  ]);
678
711
  /**
679
- * The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
680
- * names `paraLinha`, called once per row — the body the graph must read is the
712
+ * The function a call names: `proximos(id)` names `proximos`; `rows.map(toRow)`
713
+ * names `toRow`, called once per row — the body the graph must read is the
681
714
  * same, whichever way it was reached.
682
715
  */
683
716
  function calledFunctionOf(call) {
@@ -864,7 +897,7 @@ function transformerResourceOf(cls) {
864
897
  * { autor: AutorTransformer.transform } 0 here; the nested body contributes
865
898
  * { endereco: { rua, cidade } } leaves individually
866
899
  * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
867
- * { itens: xs.map((i) => ({ a, b })) } the leaves, once
900
+ * { items: xs.map((i) => ({ a, b })) } the leaves, once
868
901
  * ...this.pick(this.resource, [...]) the listed names
869
902
  * ...this.toObject() 0 here; the followed body contributes
870
903
  * ...anythingElse 1, opaque, reported
@@ -904,7 +937,7 @@ function outputFieldsIn(body, owner, stores, followed) {
904
937
  * { autor: AutorTransformer.transform } 0 here; the followed body contributes
905
938
  * { endereco: { rua, cidade } } leaves individually
906
939
  * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
907
- * { itens: xs.map((i) => ({ a, b })) } the leaves, once
940
+ * { items: xs.map((i) => ({ a, b })) } the leaves, once
908
941
  * ...this.pick(this.resource, [...]) the listed names
909
942
  * ...this.toObject() 0 here; the followed body contributes
910
943
  * ...anythingElse 1, opaque, reported
@@ -1241,7 +1274,7 @@ const BUILTIN_CALL_RESOLVERS = [
1241
1274
  if (!found) return [];
1242
1275
  const { file, owner } = found;
1243
1276
  /**
1244
- * `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
1277
+ * `X.transform(p).useVariant('forSummary')`: the variant is a METHOD of the
1245
1278
  * transformer, named after it, and it REPLACES `toObject()` — the shape that
1246
1279
  * leaves is the variant's. The chain is visited call by call: the `useVariant`
1247
1280
  * call resolves the variant's body, and the `transform` call before it resolves
@@ -1,6 +1,6 @@
1
1
  import { c as IncomparableSourcesError, d as DEFAULTS, f as defineConfig, i as measureStructure, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
2
- import { y as toPosix } from "./resolvers-DhJO-qvQ.js";
3
- import { n as analyze } from "./pipeline-C6kHKB9-.js";
2
+ import { y as toPosix } from "./resolvers-CneCj3sT.js";
3
+ import { n as analyze } from "./pipeline-4sZBYGKU.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -112,12 +112,21 @@ function renderCount(result) {
112
112
  if (unresolvedCalls > 0 || entryPointsWithoutHandler > 0 || warnings.length > 0) {
113
113
  lines.push("");
114
114
  lines.push("Confidence:");
115
- if (unresolvedCalls > 0) lines.push(` ${unresolvedCalls} unresolved calls`);
115
+ if (unresolvedCalls > 0) {
116
+ lines.push(` ${unresolvedCalls} unresolved call(s) — one line per site, whatever the number of transactions reaching it:`);
117
+ const sites = result.confidence.unresolved ?? [];
118
+ for (const site of sites.slice(0, 25)) lines.push(` ${describeSite(site)}`);
119
+ if (sites.length > 25) lines.push(` … and ${sites.length - 25} more — fp:inventory lists them all`);
120
+ }
116
121
  if (entryPointsWithoutHandler > 0) lines.push(` ${entryPointsWithoutHandler} entry points without a handler`);
117
122
  for (const warning of warnings) lines.push(` ${warning}`);
118
123
  }
119
124
  return lines.join("\n");
120
125
  }
126
+ /** one unresolved site, the way both reports print it: where, what, why, how many transactions */
127
+ function describeSite(site) {
128
+ return `${site.file}:${site.line} ${site.expression} — ${site.reason}` + (site.transactions > 1 ? ` (${site.transactions} transactions)` : "");
129
+ }
121
130
  /** `fp:explain`: a function's provenance, which is what supports a dispute */
122
131
  /**
123
132
  * Structure and conformance, beside the count and never instead of it.
@@ -302,7 +311,8 @@ async function runInventory(options) {
302
311
  output: [
303
312
  `data stores: ${inventory.dataStores.length}`,
304
313
  `entry points: ${coverage.entryPointsTotal}`,
305
- `coverage: ${(coverage.ratio * 100).toFixed(1)}% (${coverage.unresolvedCalls} unresolved calls)`
314
+ `coverage: ${(coverage.ratio * 100).toFixed(1)}% (${coverage.unresolvedCalls} unresolved calls)`,
315
+ ...inventory.unresolved.map((site) => ` ${describeSite(site)}`)
306
316
  ].join("\n")
307
317
  };
308
318
  }
@@ -5,7 +5,7 @@ import type { Behavior } from '../inventory/graph/call_graph.js';
5
5
  import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
6
6
  import type { CollectedJob } from '../inventory/sources/jobs.js';
7
7
  import type { OpaqueDeclaration } from './opaque.js';
8
- import type { Complexity, CountResult, FunctionType } from '../types.js';
8
+ import type { Complexity, CountResult, FunctionType, UnresolvedSite } from '../types.js';
9
9
  import type { FunctionOverride } from '../define_config.js';
10
10
  import type { ComplexityTable } from './tables.js';
11
11
  import type { GroupingStrategy } from './data_functions.js';
@@ -44,13 +44,16 @@ export declare const RULESET = "afp";
44
44
  * 1.7.0, wide: a write binds to what the variable IS — a destructured named
45
45
  * interface, a followed method's return, a relation off a row, a loop over rows,
46
46
  * the guard's user, a service the container made — and a write nobody can type is
47
- * an unresolved call instead of silence.
47
+ * an unresolved call instead of silence. Two in 1.8.0: a method the model
48
+ * declares names the store its caller writes, and a listener written inline on a
49
+ * string event is followed like a listener class. One in 1.9.0: the value of a
50
+ * transaction callback is what its returns name.
48
51
  *
49
52
  * Without the bump, a baseline saved by the previous version compares cleanly
50
53
  * against this one and bills the tool's own improvement as work done. The guard
51
54
  * exists for exactly that, and only this constant arms it.
52
55
  */
53
- export declare const RULESET_VERSION = "1.7.0";
56
+ export declare const RULESET_VERSION = "1.9.0";
54
57
  export type CountInput = {
55
58
  app: AppContext;
56
59
  stores: CollectedDataStore[];
@@ -61,6 +64,12 @@ export type CountInput = {
61
64
  jsonSchemas?: Map<string, DiscoveredSchema>;
62
65
  /** the queue jobs and who dispatches each — a job no transaction reaches is reported (plan 0.7 §D) */
63
66
  jobs?: CollectedJob[];
67
+ /**
68
+ * The unresolved call sites as the inventory lists them — one per site, route and
69
+ * store problems included. Given, the count shows the same number and the same list;
70
+ * absent, it lists the sites the behaviors show, one each.
71
+ */
72
+ unresolved?: UnresolvedSite[];
64
73
  /**
65
74
  * Stores written anywhere in the application's code, reachable from an entry
66
75
  * point or not — AFP §6.5.4 asks who MAINTAINS the store, and a job or a
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as CoverageTooLowError } from "../pipeline-C6kHKB9-.js";
2
- import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-BpBJsGMj.js";
1
+ import { t as CoverageTooLowError } from "../pipeline-4sZBYGKU.js";
2
+ import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-b1tgLA1B.js";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -43,7 +43,7 @@ type DeliveredValue =
43
43
  args: Delivery[];
44
44
  /**
45
45
  * One key of what the call returns — `const { data } = await q.handle()`,
46
- * `relatorio.linhas` — rather than the whole result. Resolved against the
46
+ * `report.rows` — rather than the whole result. Resolved against the
47
47
  * body's classified return, keeping only that key.
48
48
  */
49
49
  pick?: string;
@@ -64,7 +64,11 @@ type DeliveredValue =
64
64
  path: string;
65
65
  expression: string;
66
66
  };
67
- /** keys of a result that carry its rows: picking one of these is not picking one value */
67
+ /**
68
+ * Keys under which a paginator or a wrapper hands its rows on: picking one of these
69
+ * is not picking one value. Framework and JavaScript conventions only — a key named
70
+ * in one application's language is that application's, not a rule.
71
+ */
68
72
  export declare const PASSES_ROWS: Set<string>;
69
73
  export type DeliveryContext = {
70
74
  body: Node;
@@ -68,7 +68,7 @@ export declare function transformerResourceOf(cls: ClassDeclaration): string | n
68
68
  * { autor: AutorTransformer.transform } 0 here; the nested body contributes
69
69
  * { endereco: { rua, cidade } } leaves individually
70
70
  * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
71
- * { itens: xs.map((i) => ({ a, b })) } the leaves, once
71
+ * { items: xs.map((i) => ({ a, b })) } the leaves, once
72
72
  * ...this.pick(this.resource, [...]) the listed names
73
73
  * ...this.toObject() 0 here; the followed body contributes
74
74
  * ...anythingElse 1, opaque, reported
@@ -101,7 +101,7 @@ type LeafOptions = {
101
101
  * { autor: AutorTransformer.transform } 0 here; the followed body contributes
102
102
  * { endereco: { rua, cidade } } leaves individually
103
103
  * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
104
- * { itens: xs.map((i) => ({ a, b })) } the leaves, once
104
+ * { items: xs.map((i) => ({ a, b })) } the leaves, once
105
105
  * ...this.pick(this.resource, [...]) the listed names
106
106
  * ...this.toObject() 0 here; the followed body contributes
107
107
  * ...anythingElse 1, opaque, reported
@@ -30,7 +30,7 @@ export type PageReading = {
30
30
  columns: Map<string, Set<string>>;
31
31
  /** store -> why the page could not be read for it (the store then leaves whole) */
32
32
  unreadable: Map<string, string>;
33
- /** store -> members the page reads that are NOT its columns (`inventor.nomeCompleto`, computed on the way) */
33
+ /** store -> members the page reads that are NOT its columns (`member.displayName`, computed on the way) */
34
34
  unknownMembers: Map<string, Set<string>>;
35
35
  };
36
36
  export type PageEnvironment = {
@@ -1,2 +1,2 @@
1
- import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-DhJO-qvQ.js";
1
+ import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-CneCj3sT.js";
2
2
  export { BUILTIN_CALL_RESOLVERS, ignoreCalls, isTechnicalWrite, resolveCall };
@@ -4,7 +4,7 @@ import type { CallResolver } from './types.js';
4
4
  * "Local function" pattern: a helper declared in the same file, not imported.
5
5
  *
6
6
  * const lista = await proximos(id) // function proximos() { … }
7
- * return rows.map(paraLinha) // const paraLinha = (row) => …
7
+ * return rows.map(toRow) // const toRow = (row) => …
8
8
  *
9
9
  * A query object that keeps its helpers beside it is common, and before this
10
10
  * every such call was unresolved: the store a helper read was reached by nobody
@@ -15,8 +15,8 @@ import type { CallResolver } from './types.js';
15
15
  */
16
16
  export declare const localFunctionResolver: CallResolver;
17
17
  /**
18
- * The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
19
- * names `paraLinha`, called once per row — the body the graph must read is the
18
+ * The function a call names: `proximos(id)` names `proximos`; `rows.map(toRow)`
19
+ * names `toRow`, called once per row — the body the graph must read is the
20
20
  * same, whichever way it was reached.
21
21
  */
22
22
  export declare function calledFunctionOf(call: CallExpression): import('ts-morph').Identifier | null;
@@ -41,4 +41,7 @@ export declare function collectEventBindings(app: AppContext): EventBindings;
41
41
  * this" would drift.
42
42
  */
43
43
  export declare function resolveEventClass(expression: Expression | Node, from: SourceFile, app: SpecifierResolver): string | null;
44
+ /** listener bodies named by the second argument of `emitter.on` */
45
+ /** the binding key of a string event: `emitter.on('order:closed', …)` */
46
+ export declare const eventKey: (name: string) => string;
44
47
  export {};
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-C6kHKB9-.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-4sZBYGKU.js";
2
2
  export { CoverageTooLowError, analyze };
@@ -1,7 +1,9 @@
1
- import type { CountResult, CountedFunction, Inventory } from '../types.js';
1
+ import type { CountResult, CountedFunction, Inventory, UnresolvedSite } from '../types.js';
2
2
  import type { FunctionPointDiff } from '../albrecht/diff.js';
3
3
  import type { Conformance, StructureMetrics } from '../metrics/structure.js';
4
4
  export declare function renderCount(result: CountResult): string;
5
+ /** one unresolved site, the way both reports print it: where, what, why, how many transactions */
6
+ export declare function describeSite(site: UnresolvedSite): string;
5
7
  /** `fp:explain`: a function's provenance, which is what supports a dispute */
6
8
  /**
7
9
  * Structure and conformance, beside the count and never instead of it.
@@ -190,6 +190,11 @@ export type UnresolvedCall = {
190
190
  expression: string;
191
191
  reason: string;
192
192
  };
193
+ /** an unresolved call as the reports list it: one site, however many transactions reach it */
194
+ export type UnresolvedSite = UnresolvedCall & {
195
+ /** transactions whose walk reached this call; 0 for a route or store problem */
196
+ transactions: number;
197
+ };
193
198
  export type Inventory = {
194
199
  /** format version, for diffs across releases */
195
200
  version: 1;
@@ -207,10 +212,16 @@ export type Inventory = {
207
212
  coverage: {
208
213
  entryPointsTotal: number;
209
214
  entryPointsResolved: number;
215
+ /** distinct call sites nobody could follow, route and store problems included — the number the count shows too */
210
216
  unresolvedCalls: number;
211
217
  /** fraction of entry points whose handler was traced to completion */
212
218
  ratio: number;
213
219
  };
220
+ /**
221
+ * The unresolved calls, listed: what `fp:inventory` prints and what a person acts
222
+ * on. One entry per site — a body five routes reach is one gap, not five.
223
+ */
224
+ unresolved: UnresolvedSite[];
214
225
  };
215
226
  export type FunctionType = 'ILF' | 'EIF' | 'EI' | 'EO' | 'EQ';
216
227
  export type Complexity = 'low' | 'average' | 'high';
@@ -283,9 +294,12 @@ export type CountResult = {
283
294
  };
284
295
  /** flags when the count does not deserve confidence */
285
296
  confidence: {
297
+ /** distinct call sites nobody could follow — the same number the inventory shows */
286
298
  unresolvedCalls: number;
287
299
  entryPointsWithoutHandler: number;
288
300
  warnings: string[];
301
+ /** the sites themselves, so the report can list them; absent on a count built by hand */
302
+ unresolved?: UnresolvedSite[];
289
303
  };
290
304
  };
291
305
  /** Maintenance type, for enhancement-project counting. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@filipebraida/adonis-function-points",
3
3
  "description": "Automated function point counting and code metrics for AdonisJS applications.",
4
- "version": "0.8.0",
4
+ "version": "0.10.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },