@filipebraida/adonis-function-points 0.8.0 → 0.9.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,65 @@ 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.9.0
10
+
11
+ **Rule set `afp@1.8.0`.** Two rules from a team's second review of their own count, one
12
+ report fixed, and a rule about the library itself. A 0.8.0 baseline has to be recounted:
13
+ the totals move 0, −3 and +1 FP — a writing transaction leaves EO for EI, two EIs gain an
14
+ FTR — and the coverage line now means one thing in both reports.
15
+
16
+ ### Fixed
17
+
18
+ - **The unresolved calls are listed, once each, and the inventory and the count show one
19
+ number.** The 0.8 CHANGELOG said `fp:inventory` listed them; it printed a total — and the
20
+ count printed a different total, because it summed each transaction's unresolved calls
21
+ (a body five routes reach counted five times) while the inventory added the route and
22
+ store problems. Now `inventory.unresolved` and `count.confidence.unresolved` are the same
23
+ list — one entry per site (`file:line expression — reason`, how many transactions reach
24
+ it), route and store problems included — and `unresolvedCalls` is its length in both.
25
+ `fp:inventory` prints every site, `fp:count` up to 25, `--json` / `--out` carry them. The
26
+ number changes without any code changing: it counts places to look at now, which is what
27
+ a person acts on.
28
+ - **A method the model declares is application code.** `const items = await
29
+ order.pendingItems().forUpdate()`, then `item.save()` in a loop: the rows come from a
30
+ method declared on the model class, and the chain was read as an access to the model at
31
+ its root — the write went to the wrong store, or was reported unreadable. The method's
32
+ return annotation (`typeof Item`), or its returns when every one is `Item.query()…`, names
33
+ the store; a builder chain after it hands the same rows on, an aggregate a number; the
34
+ chain is read from its innermost call outwards. On the reviewing team's application the
35
+ one writing transaction still counted as EO after 0.8 turns EI (7 → 4 FP).
36
+
37
+ ### New
38
+
39
+ - **A listener written inline is a listener, and a string event is an event.**
40
+ `emitter.on('order:closed', async function ({ orderId }) { … })` binds a body to a
41
+ string, and the collector read only `emitter.on(EventClass, [ListenerClass])`: an
42
+ application that binds every listener this way had none followed, its jobs "reached by no
43
+ transaction", their writes nobody's FTR. The inline function or arrow is a handler located
44
+ by its line; the string is a binding key that `emitter.emit('…', payload)` / `emitSerial`
45
+ reach — the same decision as `Event.dispatch()`; a name built at runtime binds nothing;
46
+ `start/**` is scanned. Fixture `eventos_inline`: 27 FP where the previous rule set said 13.
47
+
48
+ ### The library itself
49
+
50
+ - **The library names no application.** Its rules are found by recounting real
51
+ applications, and their names had leaked in: two heuristics keyed on one team's result
52
+ keys and one front-end's page layout, forty comments quoting somebody's routes and models,
53
+ a few shipped-doc lines. A heuristic keyed on one application's vocabulary is not a rule.
54
+ The result-key list keeps only framework and language conventions (one list page of one
55
+ application goes from 76 back to 83 DET, same FP); a module-scoped `pages/` directory is
56
+ read structurally; comments illustrate with the library's own domain; docs cite the
57
+ measurement, not the domain. `tests/unit/no_project_literals.spec.ts` holds the words that
58
+ belong to the validated applications — in the test, nowhere else — and fails when one
59
+ appears in `src`, `docs`, README or CHANGELOG.
60
+
61
+ ### Documented
62
+
63
+ - counting-decisions §3 gains the model-method row of the bindings table; §9 gains "A
64
+ listener written inline is a listener; a string event is an event".
65
+ - Fixtures: `escritas_indiretas` grows to 97 FP (a model's own query method);
66
+ `eventos_inline` (27 FP) is new.
67
+
9
68
  ## 0.8.0
10
69
 
11
70
  **Rule set `afp@1.7.0`.** One rule, wide, found by a team reviewing a 0.6.0 count of
@@ -44,7 +103,7 @@ new X()`, `(await q.first()) ?? new X()`), as `auth.user` / `auth.getUserOrFail(
44
103
  - **A service is what the container returns.** `const svc = await
45
104
  app.container.make(X)` binds `svc` to X as `new X()` already did, so `svc.method()` is
46
105
  followed. On the reviewed application this shape carried the write of
47
- `POST /gestao/atribuicao` and 31 more sites.
106
+ the assignment route and 31 more sites.
48
107
  - A method chain on store rows handed to a delivery (`rows.map(f).join('\n')`) delivers
49
108
  the rows, not one field.
50
109
 
@@ -60,7 +119,7 @@ app.container.make(X)` binds `svc` to X as `new X()` already did, so `svc.method
60
119
  spread, a function receiving the rows, a package's `<DataTable data={…} />`, two files
61
120
  answering to one name, one prop carrying several stores, members that are not columns
62
121
  — 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
122
+ applications: **0 FP moved** (one of them: three pages read, 22 stores reported; the other two
64
123
  hand every raw store through a transformer). Fixture `inertia_pages` (25 FP): the DETs
65
124
  change, the points do not.
66
125
 
@@ -102,10 +161,10 @@ and most of that is new elementary processes the count had never seen.
102
161
  attribute; a yes/no, a formatted value, a framework service's answer and an Inertia
103
162
  lazy prop are one value. Without a delivery point the output falls back to the
104
163
  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
164
+ list page at 8 DET became 60 — its nested transformers, which the unresolved
106
165
  variant had hidden; every listing page stopped counting its filters twice.
107
166
  - **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
167
+ `rows.map(toRow)`. Not only DETs: the stores those helpers read are FTRs now, and
109
168
  two `POST`s that wrote through a local helper moved from EO to EI. Models and modules
110
169
  imported **inside** a body (`const { default: X } = await import('#…')`) bind like a
111
170
  static import — an importer that wrote four tables this way had touched nothing.
@@ -183,7 +242,7 @@ of it is EOs losing DETs they never showed and one data function that was four.
183
242
  benchmark depends on it); a child hanging off two parents stays apart and the report
184
243
  says why. Cascade delete was measured and rejected as the signal: on one application
185
244
  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**
245
+ folds four stores in all — the four-table mirror of an external registry becomes **one**
187
246
  EIF with 4 RET, which is what the CPM says.
188
247
  - **A data function is identified by its table**, as counting-decisions §5 always said.
189
248
  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.8.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-B4Pv8Thz.js")
18
18
  },
19
19
  {
20
20
  commandName: "fp:metrics",
21
- importer: () => import("../fp_metrics-DjADb7F6.js")
21
+ importer: () => import("../fp_metrics-DoS5riGr.js")
22
22
  },
23
23
  {
24
24
  commandName: "fp:count",
25
- importer: () => import("../fp_count-B_KPNKMO.js")
25
+ importer: () => import("../fp_count-Bjviq8dz.js")
26
26
  },
27
27
  {
28
28
  commandName: "fp:explain",
29
- importer: () => import("../fp_explain-C2s6Kpaj.js")
29
+ importer: () => import("../fp_explain-CrFcMeXU.js")
30
30
  },
31
31
  {
32
32
  commandName: "fp:diff",
33
- importer: () => import("../fp_diff-B3NhXzrb.js")
33
+ importer: () => import("../fp_diff-BSK1kKLQ.js")
34
34
  },
35
35
  {
36
36
  commandName: "fp:calibrate",
37
- importer: () => import("../fp_calibrate-B_oD9b02.js")
37
+ importer: () => import("../fp_calibrate-DJspMFHc.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-DqPrDZfD.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;
@@ -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,33 @@ 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
+ };
3034
3068
  const isAuthUser = (node) => {
3035
3069
  if (!authUserStore) return false;
3036
3070
  const chain = Node.isCallExpression(node) ? node.getExpression() : node;
@@ -3062,9 +3096,25 @@ function createAnalyzer(app, stores, options = {}) {
3062
3096
  if (isAuthUser(node)) return authUserStore;
3063
3097
  if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
3064
3098
  if (Node.isCallExpression(node)) {
3099
+ const callee = node.getExpression();
3100
+ if (Node.isPropertyAccessExpression(callee)) {
3101
+ /**
3102
+ * `q.forUpdate()`, `q.where(…)`: a query-builder chain hands the same rows on —
3103
+ * read from the innermost call outwards, so a model's method at the root of
3104
+ * the chain is seen before Lucid's API is assumed for the whole chain. An
3105
+ * aggregate (`.count()`) hands back a number.
3106
+ */
3107
+ const receiver = unwrapAwait(callee.getExpression());
3108
+ if (Node.isCallExpression(receiver)) {
3109
+ if (AGGREGATES.has(callee.getName())) return null;
3110
+ const inner = storeOfValue(receiver, depth + 1);
3111
+ if (inner) return inner;
3112
+ }
3113
+ const fromModel = modelMethodStore(callee, symbols);
3114
+ if (fromModel) return fromModel;
3115
+ }
3065
3116
  const access = detectAccess(node, symbols, relationsByStore);
3066
3117
  if (access) return access.method === "related" && access.viaRelation ? access.viaRelation : access.store;
3067
- const callee = node.getExpression();
3068
3118
  if (Node.isPropertyAccessExpression(callee) && ONE_OF_ROWS.has(callee.getName())) return storeOfValue(callee.getExpression(), depth + 1);
3069
3119
  return returnedStoreOf(node);
3070
3120
  }
@@ -3086,7 +3136,12 @@ function createAnalyzer(app, stores, options = {}) {
3086
3136
  const nameNode = node.getNameNode();
3087
3137
  if (!initializer) return;
3088
3138
  if (Node.isIdentifier(nameNode)) {
3089
- if (symbols.has(nameNode.getText())) return;
3139
+ /**
3140
+ * Re-read even when the parameter pass already bound the name by the chain's
3141
+ * root: `const items = await order.pendingItems().forUpdate()` is rooted at
3142
+ * `order` and holds `Item` rows — what the model's method returns wins over
3143
+ * where the chain started.
3144
+ */
3090
3145
  const store = storeOfValue(initializer);
3091
3146
  if (store) symbols.set(nameNode.getText(), store);
3092
3147
  }
@@ -3096,7 +3151,7 @@ function createAnalyzer(app, stores, options = {}) {
3096
3151
  const declared = node.getInitializer();
3097
3152
  if (!Node.isVariableDeclarationList(declared)) return;
3098
3153
  const nameNode = declared.getDeclarations()[0]?.getNameNode();
3099
- if (!nameNode || !Node.isIdentifier(nameNode) || symbols.has(nameNode.getText())) return;
3154
+ if (!nameNode || !Node.isIdentifier(nameNode)) return;
3100
3155
  const store = storeOfValue(node.getExpression());
3101
3156
  if (store) symbols.set(nameNode.getText(), store);
3102
3157
  return;
@@ -3107,7 +3162,7 @@ function createAnalyzer(app, stores, options = {}) {
3107
3162
  const callback = node.getArguments()[0];
3108
3163
  if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return;
3109
3164
  const parameter = callback.getParameters()[0]?.getNameNode();
3110
- if (!parameter || !Node.isIdentifier(parameter) || symbols.has(parameter.getText())) return;
3165
+ if (!parameter || !Node.isIdentifier(parameter)) return;
3111
3166
  const store = storeOfValue(callee.getExpression());
3112
3167
  if (store) symbols.set(parameter.getText(), store);
3113
3168
  }
@@ -3176,8 +3231,8 @@ function createAnalyzer(app, stores, options = {}) {
3176
3231
  * A select list that is not literal is reported, and the store falls back
3177
3232
  * to every column, which overestimates in the open.
3178
3233
  *
3179
- * `related('itens').query().count()` reads the RELATION target, and the
3180
- * parent only as a receiver; `preload('itens')` reads the target whole.
3234
+ * `related('items').query().count()` reads the RELATION target, and the
3235
+ * parent only as a receiver; `preload('items')` reads the target whole.
3181
3236
  */
3182
3237
  if (access.mode === "read") {
3183
3238
  const chain = chainShapeOf(call);
@@ -3318,7 +3373,7 @@ function createAnalyzer(app, stores, options = {}) {
3318
3373
  * Both project-wide facts come from one pass, computed once: which stores the
3319
3374
  * application WRITES (maintenance, §6.5.4) and which it ADDRESSES directly
3320
3375
  * (grouping, counting-decisions §10). A store reached only through a relation
3321
- * — `preload('itens')`, `related('itens').create()` — is read or written, but
3376
+ * — `preload('items')`, `related('items').create()` — is read or written, but
3322
3377
  * not addressed: the user never sees it outside its parent.
3323
3378
  */
3324
3379
  let projectWide;
@@ -3465,8 +3520,8 @@ function createAnalyzer(app, stores, options = {}) {
3465
3520
  }
3466
3521
  const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
3467
3522
  /**
3468
- * `egresso.curso` where the body returns the row itself (`return
3469
- * Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
3523
+ * `member.curso` where the body returns the row itself (`return
3524
+ * Member.query()…first()`, path ''): the pick lands INSIDE a returned
3470
3525
  * value — one field of a store is one value; one key of a returned call
3471
3526
  * is that call picked deeper.
3472
3527
  */
@@ -3727,7 +3782,7 @@ function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ ne
3727
3782
  }
3728
3783
  }
3729
3784
  /**
3730
- * `const { default: Noticia } = await import('#noticias/models/noticia')`: a
3785
+ * `const { default: Article } = await import('#articles/models/article')`: a
3731
3786
  * model imported INSIDE the body — an ace command does this to keep the app
3732
3787
  * from booting for `--help`. The store is the same; only the binding moved.
3733
3788
  */
@@ -3808,7 +3863,7 @@ function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ ne
3808
3863
  }
3809
3864
  }
3810
3865
  /**
3811
- * `const { preIntake } = input` where `input.preIntake` is a registered path:
3866
+ * `const { order } = input` where `input.order` is a registered path:
3812
3867
  * each element inherits the store of its path. Read AFTER the parameters, which
3813
3868
  * is where the paths come from.
3814
3869
  */
@@ -3931,6 +3986,17 @@ const ONE_OF_ROWS = new Set([
3931
3986
  "first",
3932
3987
  "last"
3933
3988
  ]);
3989
+ /** query-builder terminals that hand back one number or one yes/no, never rows */
3990
+ const AGGREGATES = new Set([
3991
+ "count",
3992
+ "sum",
3993
+ "avg",
3994
+ "min",
3995
+ "max",
3996
+ "exists",
3997
+ "pojo",
3998
+ "pluck"
3999
+ ]);
3934
4000
  /** array methods whose callback receives one row of the receiver */
3935
4001
  const ITERATES_ROWS = new Set([
3936
4002
  "map",
@@ -4015,8 +4081,8 @@ function importMapsOf(file, app) {
4015
4081
  }
4016
4082
  }
4017
4083
  /**
4018
- * `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
4019
- * `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
4084
+ * `const { default: SyncCatalog } = await import('#catalog/actions/sync_catalog')`
4085
+ * `const { runInProgress } = await import('#catalog/services/runs')`
4020
4086
  *
4021
4087
  * A module imported INSIDE a body — the shape ace commands use so `--help` does
4022
4088
  * not boot the application. The binding moved; the body it names did not, and
@@ -5074,13 +5140,15 @@ const RULESET = "afp";
5074
5140
  * 1.7.0, wide: a write binds to what the variable IS — a destructured named
5075
5141
  * interface, a followed method's return, a relation off a row, a loop over rows,
5076
5142
  * the guard's user, a service the container made — and a write nobody can type is
5077
- * an unresolved call instead of silence.
5143
+ * an unresolved call instead of silence. Two in 1.8.0: a method the model
5144
+ * declares names the store its caller writes, and a listener written inline on a
5145
+ * string event is followed like a listener class.
5078
5146
  *
5079
5147
  * Without the bump, a baseline saved by the previous version compares cleanly
5080
5148
  * against this one and bills the tool's own improvement as work done. The guard
5081
5149
  * exists for exactly that, and only this constant arms it.
5082
5150
  */
5083
- const RULESET_VERSION = "1.7.0";
5151
+ const RULESET_VERSION = "1.8.0";
5084
5152
  function count(input, options = {}) {
5085
5153
  const warnings = [];
5086
5154
  const usage = usageOf(input);
@@ -5537,20 +5605,32 @@ function totalsOf(functions) {
5537
5605
  * reader must see that without having to go looking.
5538
5606
  */
5539
5607
  function confidenceOf(input, warnings) {
5540
- let unresolvedCalls = 0;
5541
5608
  let withoutHandler = 0;
5609
+ const sites = /* @__PURE__ */ new Map();
5610
+ for (const site of input.unresolved ?? []) sites.set(`${site.file}:${site.line}:${site.expression}`, { ...site });
5542
5611
  for (const entry of input.entryPoints) {
5543
5612
  const behavior = input.behaviors.get(entry.id);
5544
5613
  if (!behavior) {
5545
5614
  withoutHandler++;
5546
5615
  continue;
5547
5616
  }
5548
- unresolvedCalls += behavior.unresolved.length;
5617
+ if (input.unresolved) continue;
5618
+ for (const call of behavior.unresolved) {
5619
+ const key = `${call.file}:${call.line}:${call.expression}`;
5620
+ const site = sites.get(key);
5621
+ if (site) site.transactions++;
5622
+ else sites.set(key, {
5623
+ ...call,
5624
+ transactions: 1
5625
+ });
5626
+ }
5549
5627
  }
5628
+ const unresolved = [...sites.values()].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
5550
5629
  return {
5551
- unresolvedCalls,
5630
+ unresolvedCalls: unresolved.length,
5552
5631
  entryPointsWithoutHandler: withoutHandler,
5553
- warnings
5632
+ warnings,
5633
+ unresolved
5554
5634
  };
5555
5635
  }
5556
5636
  //#endregion
@@ -5627,7 +5707,27 @@ async function analyze(root, options = {}) {
5627
5707
  });
5628
5708
  const behaviors = new Map(entryPoints.filter((entry) => entry.handler).map((entry) => [entry.id, analyzer.analyze(entry.handler)]));
5629
5709
  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);
5710
+ /**
5711
+ * One site, however many transactions reach it: a body five routes walk is one gap
5712
+ * to close, not five. The count shows the same number — a reviewing team found 10
5713
+ * in one report and 13 in the other and could act on neither.
5714
+ */
5715
+ const sites = /* @__PURE__ */ new Map();
5716
+ for (const problem of [...storeProblems, ...routeProblems]) sites.set(`${problem.file}:${problem.line}:${problem.expression}`, {
5717
+ ...problem,
5718
+ transactions: 0
5719
+ });
5720
+ for (const behavior of behaviors.values()) for (const call of behavior.unresolved) {
5721
+ const key = `${call.file}:${call.line}:${call.expression}`;
5722
+ const site = sites.get(key);
5723
+ if (site) site.transactions++;
5724
+ else sites.set(key, {
5725
+ ...call,
5726
+ transactions: 1
5727
+ });
5728
+ }
5729
+ const unresolvedSites = [...sites.values()].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
5730
+ const unresolvedCalls = unresolvedSites.length;
5631
5731
  /**
5632
5732
  * Every path that LEAVES is relative to the application root.
5633
5733
  *
@@ -5740,7 +5840,11 @@ async function analyze(root, options = {}) {
5740
5840
  entryPointsResolved: resolved,
5741
5841
  unresolvedCalls,
5742
5842
  ratio: entryPoints.length === 0 ? 1 : resolved / entryPoints.length
5743
- }
5843
+ },
5844
+ unresolved: unresolvedSites.map((site) => ({
5845
+ ...site,
5846
+ file: emit(site.file)
5847
+ }))
5744
5848
  };
5745
5849
  const minimum = options.minCoverage ?? 0;
5746
5850
  if (inventory.coverage.ratio < minimum) throw new CoverageTooLowError(inventory.coverage.ratio, minimum);
@@ -5754,6 +5858,7 @@ async function analyze(root, options = {}) {
5754
5858
  behaviors,
5755
5859
  jsonSchemas,
5756
5860
  jobs: collectJobs(app),
5861
+ unresolved: inventory.unresolved,
5757
5862
  writtenAnywhere: analyzer.writtenAnywhere(),
5758
5863
  addressedAnywhere: analyzer.addressedAnywhere(),
5759
5864
  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-DqPrDZfD.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,15 @@ 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.
48
50
  *
49
51
  * Without the bump, a baseline saved by the previous version compares cleanly
50
52
  * against this one and bills the tool's own improvement as work done. The guard
51
53
  * exists for exactly that, and only this constant arms it.
52
54
  */
53
- export declare const RULESET_VERSION = "1.7.0";
55
+ export declare const RULESET_VERSION = "1.8.0";
54
56
  export type CountInput = {
55
57
  app: AppContext;
56
58
  stores: CollectedDataStore[];
@@ -61,6 +63,12 @@ export type CountInput = {
61
63
  jsonSchemas?: Map<string, DiscoveredSchema>;
62
64
  /** the queue jobs and who dispatches each — a job no transaction reaches is reported (plan 0.7 §D) */
63
65
  jobs?: CollectedJob[];
66
+ /**
67
+ * The unresolved call sites as the inventory lists them — one per site, route and
68
+ * store problems included. Given, the count shows the same number and the same list;
69
+ * absent, it lists the sites the behaviors show, one each.
70
+ */
71
+ unresolved?: UnresolvedSite[];
64
72
  /**
65
73
  * Stores written anywhere in the application's code, reachable from an entry
66
74
  * 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-DqPrDZfD.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-BuZNr-FE.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-DqPrDZfD.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.9.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },