@filipebraida/adonis-function-points 0.10.0 → 0.11.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,59 @@ 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.11.0
10
+
11
+ Rule set unchanged (`afp@1.9.0`); no number moves. What changes is what the **coverage**
12
+ means: it measures what of the application the walk could not follow, and nothing else.
13
+
14
+ ### Fixed
15
+
16
+ - **A call into a package is outside the boundary — noted, not an unresolved call.** An
17
+ authorisation service from a package injected into the application's façade, a variant of an
18
+ attachment whose column type is a package's, a content collection built by a package factory,
19
+ a mailer method inherited from a package base: none is code the analysis should follow, so none
20
+ is a gap it failed to follow. What a package does with its own tables is technical (§4); what it
21
+ hands back is a value. Read structurally — the injected property's declared type, the model
22
+ property's declared type, the exported const's factory, the class's base, each resolving to a
23
+ specifier the application does not — never by package name. One note per site in
24
+ `inventory.notes`, naming the transactions that reach it; behind the same noise gate as an
25
+ unresolved call, so what was silent stays silent. A receiver typed by the application stays a
26
+ gap. Fixture `fronteira_pacote` (23 FP before and after; four unresolved calls become three
27
+ notes and the one gap of the application).
28
+ - **A `Map` or `Set` that arrives through the constructor, one level down a named type**
29
+ (`this.extras?.panel?.get(id)` with `extras?: Extras`, `Extras = { panel: Map<…> }`) is noise,
30
+ as `private names = new Map()` already was. Fixture `patterns/native_receivers`.
31
+ - **A command's `@flags.*` / `@args.*` property is a string, a number or a boolean**: a method on
32
+ it (`this.name.trim()`) is the language's, not a gap.
33
+ - **A method of an interface says so**: "interface method: the implementation is injected at
34
+ runtime and cannot be followed statically" — a gap, with the right words, where "probably
35
+ inherited from a package class" pointed the reader at the wrong place.
36
+ - **A local mixin factory whose returned class declares no `@column`** adds behaviour, not
37
+ attributes: a note. One that declares a column stays a gap — reading its columns off the
38
+ returned class is a rule for a release that moves numbers.
39
+
40
+ On the three validated applications, unresolved calls go from 13 / 8 / 12 to **0 / 0 / 1** —
41
+ the one an interface method, which is a gap of the application — with no point moved.
42
+
43
+ ### Documented
44
+
45
+ - counting-decisions §4 gains "A call into a package is outside the boundary".
46
+
47
+ ## 0.10.1
48
+
49
+ Rule set unchanged (`afp@1.9.0`); no number moves.
50
+
51
+ ### Fixed
52
+
53
+ - **A base class or a mixin from a package is a fact about the declaration, not an
54
+ unresolved call.** `compose(Base, Auditable)`, `withAuthFinder(…)` inline or through a local
55
+ const (which came out as "base class not found in the application" — code that is in the
56
+ application, built by a package's factory). Nobody follows a mixin, and in function points
57
+ it is nothing: what it adds is technical, never a user-recognisable attribute; the model's
58
+ own columns and the schema decide. Listed as a **note** by `fp:inventory` and in
59
+ `inventory.notes`, kept out of the coverage number. A base of the application that was not
60
+ found stays an unresolved call.
61
+
9
62
  ## 0.10.0
10
63
 
11
64
  **Rule set `afp@1.9.0`.** One rule and two report fixes, from a team's third review of their
@@ -14,27 +14,27 @@
14
14
  const commands = [
15
15
  {
16
16
  commandName: "fp:inventory",
17
- importer: () => import("../fp_inventory-BJlIoetL.js")
17
+ importer: () => import("../fp_inventory-D3_6QCD2.js")
18
18
  },
19
19
  {
20
20
  commandName: "fp:metrics",
21
- importer: () => import("../fp_metrics-BDKaNL0B.js")
21
+ importer: () => import("../fp_metrics-lnnhnVaw.js")
22
22
  },
23
23
  {
24
24
  commandName: "fp:count",
25
- importer: () => import("../fp_count-B5hxDyGJ.js")
25
+ importer: () => import("../fp_count-mDQgQeEq.js")
26
26
  },
27
27
  {
28
28
  commandName: "fp:explain",
29
- importer: () => import("../fp_explain-C4X_-Rp7.js")
29
+ importer: () => import("../fp_explain-CQESTS5W.js")
30
30
  },
31
31
  {
32
32
  commandName: "fp:diff",
33
- importer: () => import("../fp_diff-7S8NcKIY.js")
33
+ importer: () => import("../fp_diff-BnWi2Fx8.js")
34
34
  },
35
35
  {
36
36
  commandName: "fp:calibrate",
37
- importer: () => import("../fp_calibrate-DUPUXVaJ.js")
37
+ importer: () => import("../fp_calibrate-U5xdt5Qe.js")
38
38
  }
39
39
  ];
40
40
  let cache = null;
@@ -1,4 +1,4 @@
1
- import { s as printResult, t as runCalibrate } from "./runners-b1tgLA1B.js";
1
+ import { s as printResult, t as runCalibrate } from "./runners-CzzlX7h-.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-b1tgLA1B.js";
1
+ import { n as runCount, s as printResult } from "./runners-CzzlX7h-.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-b1tgLA1B.js";
1
+ import { r as runDiff, s as printResult } from "./runners-CzzlX7h-.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-b1tgLA1B.js";
1
+ import { i as runExplain, s as printResult } from "./runners-CzzlX7h-.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-b1tgLA1B.js";
1
+ import { a as runInventory, s as printResult } from "./runners-CzzlX7h-.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-b1tgLA1B.js";
1
+ import { o as runMetrics, s as printResult } from "./runners-CzzlX7h-.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-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";
4
+ import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-Bz1F-Tgf.js";
5
+ import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-CdDJRsLM.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-CneCj3sT.js";
1
+ import { _ as isSeeder, a as chainShapeOf, b as toPosix, c as transformerResourceOf, d as EXECUTION_METHODS, f as collectEventBindings, g as isApplicationCode, h as rootSymbolOf, i as resolveCall, l as unwrap$1, m as hooksFiredBy, o as mappedLiteralOf, p as detectAccess, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as DISPATCH_METHODS, v as relativeTo, y as samePath } from "./resolvers-Bz1F-Tgf.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";
@@ -384,6 +384,7 @@ async function collectDataStores(app) {
384
384
  if (app.generated.dataSchema) project.addSourceFileAtPathIfExists(app.generated.dataSchema);
385
385
  const candidates = [];
386
386
  const unresolved = [];
387
+ const notes = [];
387
388
  /**
388
389
  * Classes appearing as an ANCESTOR of some model.
389
390
  *
@@ -395,7 +396,7 @@ async function collectDataStores(app) {
395
396
  for (const file of project.getSourceFiles()) {
396
397
  if (samePath(file.getFilePath(), app.generated.dataSchema)) continue;
397
398
  for (const cls of file.getClasses()) {
398
- const described = describeStore(cls, app, project, unresolved);
399
+ const described = describeStore(cls, app, project, unresolved, notes);
399
400
  if (!described) continue;
400
401
  candidates.push(described.store);
401
402
  for (const ancestor of described.ancestors) ancestors.add(ancestor);
@@ -403,18 +404,20 @@ async function collectDataStores(app) {
403
404
  }
404
405
  return {
405
406
  stores: candidates.filter((store) => !ancestors.has(store.id)).sort(byName),
406
- unresolved
407
+ unresolved,
408
+ notes
407
409
  };
408
410
  }
409
411
  /** stable identity of a class, to separate a base from a store */
410
412
  const classKey = (cls) => `${cls.getSourceFile().getFilePath()}#${cls.getName()}`;
411
413
  const byName = (a, b) => a.name.localeCompare(b.name);
412
- function describeStore(cls, app, project, unresolved) {
414
+ function describeStore(cls, app, project, unresolved, notes) {
413
415
  const name = cls.getName();
414
416
  if (!name) return null;
415
417
  const chain = walkChain(cls, app, project);
416
418
  if (!chain.reachesLucid) return null;
417
419
  unresolved.push(...chain.unresolved);
420
+ notes.push(...chain.notes);
418
421
  const file = cls.getSourceFile().getFilePath();
419
422
  return {
420
423
  store: {
@@ -455,6 +458,7 @@ function walkChain(start, app, project) {
455
458
  * the coverage report.
456
459
  */
457
460
  const unresolved = [];
461
+ const notes = [];
458
462
  let reachesLucid = false;
459
463
  let columnSource = "ast";
460
464
  const visit = (cls) => {
@@ -475,6 +479,31 @@ function walkChain(start, app, project) {
475
479
  visit(resolved);
476
480
  continue;
477
481
  }
482
+ /**
483
+ * A base or a mixin from a PACKAGE — `compose(Base, Auditable)` imported from
484
+ * outside, `withAuthFinder(…)` inline or through a local const — is a fact about
485
+ * the declaration, not a gap in the walk: nobody follows a mixin. What it adds is
486
+ * technical (a hashed password, audit stamps) and never a user-recognisable
487
+ * attribute (§7.2); the model's own columns and the schema decide. Noted, and kept
488
+ * out of the coverage number. A base of the application that was not found stays
489
+ * a gap.
490
+ */
491
+ /**
492
+ * A mixin factory OF THE APPLICATION whose returned class declares no `@column`
493
+ * adds methods, not attributes: a fact, not a gap. One that declares columns
494
+ * stays a gap — reading them off the returned class is a rule for a release
495
+ * that moves numbers (plan 0.11 §E).
496
+ */
497
+ const columnless = localFactoryWithoutColumns(parent, cls.getSourceFile(), app, project);
498
+ if (columnless) {
499
+ notes.push(`${cls.getName() ?? "?"}: local mixin factory ${columnless} declares no column — adds behaviour, not attributes`);
500
+ continue;
501
+ }
502
+ const packaged = packageOriginOf(parent, cls.getSourceFile(), app);
503
+ if (packaged) {
504
+ notes.push(`${cls.getName() ?? "?"}: base or mixin from ${packaged.specifier} (${packaged.expression}) — technical, adds no user-recognisable attribute; the model's own columns and the schema decide`);
505
+ continue;
506
+ }
478
507
  unresolved.push({
479
508
  file: cls.getSourceFile().getFilePath(),
480
509
  line: parent.getStartLineNumber(),
@@ -489,8 +518,75 @@ function walkChain(start, app, project) {
489
518
  classes,
490
519
  attributes: [...attributes.values()],
491
520
  columnSource,
492
- unresolved
521
+ unresolved,
522
+ notes
523
+ };
524
+ }
525
+ /**
526
+ * `compose(Base, withTracking())` where `withTracking` is a function of the application
527
+ * whose body returns a class expression: the factory's name when that class declares no
528
+ * `@column`, `null` when it declares one (or when the factory cannot be read).
529
+ */
530
+ function localFactoryWithoutColumns(parent, file, app, project) {
531
+ if (!Node.isCallExpression(parent)) return null;
532
+ const callee = parent.getExpression();
533
+ if (!Node.isIdentifier(callee)) return null;
534
+ const name = callee.getText();
535
+ let declared = file.getFunction(name);
536
+ if (!declared) {
537
+ const origin = originOf$1(name, file);
538
+ const target = origin ? app.resolveSpecifier(origin.specifier) : null;
539
+ declared = (target ? project.getSourceFile(target) ?? project.addSourceFileAtPathIfExists(target) : null)?.getFunction(origin?.exportedName === "default" ? name : origin?.exportedName ?? name) ?? void 0;
540
+ }
541
+ if (!declared) return null;
542
+ return [...declared.getDescendantsOfKind(SyntaxKind.ClassExpression), ...declared.getDescendantsOfKind(SyntaxKind.ClassDeclaration)].some((c) => c.getProperties().some((p) => p.getDecorators().some((d) => /^column\b/.test(d.getFullName())))) ? null : `${name}()`;
543
+ }
544
+ /**
545
+ * Where a base or a mixin comes from, when it comes from a package: the class
546
+ * imported from a specifier the application does not resolve, a factory call whose
547
+ * callee is, or a local const initialised by such a call.
548
+ */
549
+ function packageOriginOf(parent, file, app) {
550
+ const fromPackage = (local) => {
551
+ const origin = originOf$1(local, file);
552
+ return origin && !app.resolveSpecifier(origin.specifier) ? origin.specifier : null;
553
+ };
554
+ const rootOf = (node) => {
555
+ let current = node;
556
+ for (let depth = 0; depth < 20; depth++) {
557
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
558
+ current = current.getExpression();
559
+ continue;
560
+ }
561
+ return Node.isIdentifier(current) ? current.getText() : null;
562
+ }
563
+ return null;
564
+ };
565
+ const brief = (text) => text.replace(/\s+/g, " ").slice(0, 60);
566
+ if (Node.isCallExpression(parent)) {
567
+ const root = rootOf(parent);
568
+ const specifier = root ? fromPackage(root) : null;
569
+ return specifier ? {
570
+ specifier,
571
+ expression: brief(parent.getText())
572
+ } : null;
573
+ }
574
+ if (!Node.isIdentifier(parent)) return null;
575
+ const direct = fromPackage(parent.getText());
576
+ if (direct) return {
577
+ specifier: direct,
578
+ expression: parent.getText()
493
579
  };
580
+ const initializer = file.getVariableDeclaration(parent.getText())?.getInitializer();
581
+ if (initializer && Node.isCallExpression(initializer)) {
582
+ const root = rootOf(initializer);
583
+ const specifier = root ? fromPackage(root) : null;
584
+ if (specifier) return {
585
+ specifier,
586
+ expression: `${parent.getText()} = ${brief(initializer.getText())}`
587
+ };
588
+ }
589
+ return null;
494
590
  }
495
591
  /**
496
592
  * Why the base class could not be resolved.
@@ -1193,8 +1289,7 @@ const FRAMEWORK_SERVICES = new Set([
1193
1289
  "app",
1194
1290
  "prompt"
1195
1291
  ]);
1196
- /** Is this call one that cannot reach a data store? */
1197
- function isNoise(call, owner) {
1292
+ function isNoise(call, owner, membersOf) {
1198
1293
  const expression = call.getExpression();
1199
1294
  if (!Node.isPropertyAccessExpression(expression)) return false;
1200
1295
  if (NEVER_DATA_METHODS.has(expression.getName())) return true;
@@ -1207,7 +1302,7 @@ function isNoise(call, owner) {
1207
1302
  * than a bare identifier.
1208
1303
  */
1209
1304
  if (Node.isPropertyAccessExpression(receiver) && Node.isThisExpression(receiver.getExpression()) && FRAMEWORK_SERVICES.has(receiver.getName())) return true;
1210
- return isNativeReceiver(receiver, owner);
1305
+ return isNativeReceiver(receiver, owner, membersOf);
1211
1306
  }
1212
1307
  /**
1213
1308
  * Iteration over a list, checked BEFORE the resolvers run.
@@ -1233,14 +1328,40 @@ function isIteration(method, call) {
1233
1328
  return first !== void 0 && (Node.isArrowFunction(first) || Node.isFunctionExpression(first));
1234
1329
  }
1235
1330
  /** Does the receiver resolve to a built-in, by its declaration? */
1236
- function isNativeReceiver(receiver, owner) {
1331
+ function isNativeReceiver(receiver, owner, membersOf) {
1237
1332
  if (Node.isArrayLiteralExpression(receiver) || Node.isStringLiteral(receiver)) return true;
1238
- if (Node.isPropertyAccessExpression(receiver) && receiver.getExpression().getKind()) {
1333
+ if (Node.isPropertyAccessExpression(receiver)) {
1239
1334
  const inner = receiver.getExpression();
1240
- if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName());
1335
+ if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName()) || isPrimitiveInput(owner, receiver.getName());
1336
+ /**
1337
+ * `this.extras?.painel?.get(id)` where `constructor(protected extras?: Extras)` and
1338
+ * `type Extras = { painel: Map<number, Row> }`: the lookups a transformer receives
1339
+ * arrive one level down, through a named type. The caller resolves the type; here
1340
+ * only its member's type is read.
1341
+ */
1342
+ if (Node.isPropertyAccessExpression(inner) && Node.isThisExpression(inner.getExpression()) && owner && membersOf) {
1343
+ const typeNode = declaredOn(owner, inner.getName())?.getTypeNode();
1344
+ if (!typeNode) return false;
1345
+ const memberType = membersOf(typeNode).get(receiver.getName());
1346
+ return !!memberType && NATIVE_TYPES.has(memberType.replace(/<.*/, "").trim());
1347
+ }
1241
1348
  }
1242
1349
  return false;
1243
1350
  }
1351
+ /** a class property or a constructor parameter property by name */
1352
+ function declaredOn(owner, name) {
1353
+ return owner.getProperty(name) ?? owner.getConstructors()[0]?.getParameters().find((parameter) => parameter.getName() === name);
1354
+ }
1355
+ /** an ace command's `@flags.*` / `@args.*` property is a string, a number or a boolean: any method on it is the language's */
1356
+ function isPrimitiveInput(owner, name) {
1357
+ const property = owner.getProperty(name);
1358
+ if (!property) return false;
1359
+ return property.getDecorators().some((decorator) => {
1360
+ const callee = decorator.getCallExpression()?.getExpression();
1361
+ const root = callee && Node.isPropertyAccessExpression(callee) ? callee.getExpression().getText() : "";
1362
+ return root === "flags" || root === "args";
1363
+ });
1364
+ }
1244
1365
  function isNativeProperty(owner, name) {
1245
1366
  /**
1246
1367
  * Both shapes declare a field: a class property, and a constructor parameter
@@ -3252,6 +3373,7 @@ function createAnalyzer(app, stores, options = {}) {
3252
3373
  const accesses = [];
3253
3374
  const followUps = [];
3254
3375
  const unresolved = [];
3376
+ const packageCalls = [];
3255
3377
  const reads = [];
3256
3378
  /** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
3257
3379
  const followedCalls = /* @__PURE__ */ new Map();
@@ -3371,7 +3493,14 @@ function createAnalyzer(app, stores, options = {}) {
3371
3493
  * counted as EOs under a coverage of 99.5%, because a write on an unknown
3372
3494
  * local was dropped without a word. It lowers coverage and is named.
3373
3495
  */
3374
- if (isUnreadableWrite(call, symbols, imports, packageImports, body) && !isNoise(call, owner)) {
3496
+ /**
3497
+ * A call INTO a package — `this.authz.can()` on an injected package type,
3498
+ * `row.avatar.getVariant()` on a package-typed column, `catalogue.load()` on a
3499
+ * package factory's product — is outside the boundary: what a package does with
3500
+ * its own tables is technical (§4), what it hands back is a value. Not a gap
3501
+ * the walk failed at; noted (plan 0.11 §A).
3502
+ */
3503
+ if (isUnreadableWrite(call, symbols, imports, packageImports, body) && !isNoise(call, owner, (typeNode) => membersOfType(typeNode, file, app))) {
3375
3504
  unresolved.push({
3376
3505
  file: ref.file,
3377
3506
  line: call.getStartLineNumber(),
@@ -3380,12 +3509,24 @@ function createAnalyzer(app, stores, options = {}) {
3380
3509
  });
3381
3510
  continue;
3382
3511
  }
3383
- if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
3384
- file: ref.file,
3385
- line: call.getStartLineNumber(),
3386
- expression: call.getExpression().getText().replace(/\s+/g, ""),
3387
- reason: "call that no strategy knew how to follow"
3388
- });
3512
+ if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner, (typeNode) => membersOfType(typeNode, file, app))) {
3513
+ const specifier = packageTargetOf(call, owner, file, symbols, app, storesByName, sourceFile);
3514
+ if (specifier) {
3515
+ packageCalls.push({
3516
+ file: ref.file,
3517
+ line: call.getStartLineNumber(),
3518
+ expression: call.getExpression().getText().replace(/\s+/g, ""),
3519
+ specifier
3520
+ });
3521
+ continue;
3522
+ }
3523
+ unresolved.push({
3524
+ file: ref.file,
3525
+ line: call.getStartLineNumber(),
3526
+ expression: call.getExpression().getText().replace(/\s+/g, ""),
3527
+ reason: "call that no strategy knew how to follow"
3528
+ });
3529
+ }
3389
3530
  }
3390
3531
  /**
3391
3532
  * Read after the loop: whether a key holds a nested transformer is known only
@@ -3405,6 +3546,7 @@ function createAnalyzer(app, stores, options = {}) {
3405
3546
  followUps,
3406
3547
  unresolved,
3407
3548
  validators: validator.fields,
3549
+ packageCalls,
3408
3550
  commandFields,
3409
3551
  opaqueValidators: validator.opaque,
3410
3552
  requestFields: request.fields,
@@ -3514,6 +3656,7 @@ function createAnalyzer(app, stores, options = {}) {
3514
3656
  const writtenStores = /* @__PURE__ */ new Set();
3515
3657
  const inputFields = /* @__PURE__ */ new Set();
3516
3658
  const commandFields = /* @__PURE__ */ new Set();
3659
+ const packageCalls = [];
3517
3660
  const opaqueInputFields = /* @__PURE__ */ new Set();
3518
3661
  const requestFields = /* @__PURE__ */ new Set();
3519
3662
  let opaqueRequest = false;
@@ -3677,12 +3820,36 @@ function createAnalyzer(app, stores, options = {}) {
3677
3820
  * Dropping it silently is the worst possible defect: the transaction
3678
3821
  * loses a path and nobody knows.
3679
3822
  */
3680
- if (!isNoiseMember(ref.file, ref.member)) unresolved.push({
3681
- file: ref.file,
3682
- line: ref.line ?? 0,
3683
- expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
3684
- reason: "body not found in the resolved file: probably inherited from a package class"
3685
- });
3823
+ if (!isNoiseMember(ref.file, ref.member)) {
3824
+ /**
3825
+ * `catalogue.load()` where `catalogue` is `defineCollection(…)` from a package,
3826
+ * or a class extending a package base without the member: the body is the
3827
+ * package's. Outside the boundary — noted, not a gap (plan 0.11 §A).
3828
+ */
3829
+ const source = sourceFile(ref.file);
3830
+ const fromPackage = source ? packageMemberOriginOf(source, ref.member, app) : null;
3831
+ if (fromPackage) {
3832
+ packageCalls.push({
3833
+ file: ref.file,
3834
+ line: ref.line ?? 0,
3835
+ expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
3836
+ specifier: fromPackage
3837
+ });
3838
+ return;
3839
+ }
3840
+ /**
3841
+ * `fonte.varrerPorDocumento()` where the file declares an INTERFACE with that
3842
+ * member and no class: there is no body because the implementation is injected
3843
+ * at runtime — a gap, and the right words for it.
3844
+ */
3845
+ const declaredAsInterface = !!ref.member && !!source && source.getInterfaces().some((i) => i.getMethod(ref.member) || i.getProperty(ref.member));
3846
+ unresolved.push({
3847
+ file: ref.file,
3848
+ line: ref.line ?? 0,
3849
+ expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
3850
+ reason: declaredAsInterface ? "interface method: the implementation is injected at runtime and cannot be followed statically" : "body not found in the resolved file: probably inherited from a package class"
3851
+ });
3852
+ }
3686
3853
  return;
3687
3854
  }
3688
3855
  let bodyWrites = false;
@@ -3702,6 +3869,7 @@ function createAnalyzer(app, stores, options = {}) {
3702
3869
  unresolved.push(...facts.unresolved);
3703
3870
  for (const field of facts.validators) inputFields.add(field);
3704
3871
  for (const field of facts.commandFields) commandFields.add(field);
3872
+ for (const packaged of facts.packageCalls) packageCalls.push(packaged);
3705
3873
  for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
3706
3874
  for (const field of facts.requestFields) requestFields.add(field);
3707
3875
  if (facts.opaqueRequest) opaqueRequest = true;
@@ -3768,6 +3936,7 @@ function createAnalyzer(app, stores, options = {}) {
3768
3936
  writtenStores: [...writtenStores].sort(),
3769
3937
  inputFields: [...inputFields].sort(),
3770
3938
  commandFields: [...commandFields].sort(),
3939
+ packageCalls,
3771
3940
  opaqueInputFields: [...opaqueInputFields].sort(),
3772
3941
  requestFields: [...requestFields].sort(),
3773
3942
  opaqueRequest,
@@ -4002,7 +4171,91 @@ function dependencyTypeOf(node) {
4002
4171
  if (Node.isIdentifier(target)) return target.getText();
4003
4172
  }
4004
4173
  }
4005
- /** type identifier -> application file where it is declared */
4174
+ /** the specifier a type name is imported from in `file`, when the application does not resolve it */
4175
+ function packageSpecifierOfType(typeName, file, app) {
4176
+ const bare = typeName.replace(/<.*/, "").replace(/\|.*$/, "").replace(/\[\]$/, "").trim();
4177
+ for (const declaration of file.getImportDeclarations()) {
4178
+ const specifier = declaration.getModuleSpecifierValue();
4179
+ if (![declaration.getDefaultImport()?.getText(), ...declaration.getNamedImports().map((n) => n.getAliasNode()?.getText() ?? n.getName())].includes(bare)) continue;
4180
+ return app.resolveSpecifier(specifier) ? null : specifier;
4181
+ }
4182
+ return null;
4183
+ }
4184
+ /** the declared type of `this.<property>` on the owner class: a constructor parameter or a property */
4185
+ function declaredTypeOfProperty(owner, property) {
4186
+ if (!owner) return void 0;
4187
+ const parameter = owner.getConstructors()[0]?.getParameters().find((p) => p.getName() === property);
4188
+ if (parameter) return dependencyTypeOf(parameter);
4189
+ const declared = owner.getProperties().find((p) => p.getName() === property);
4190
+ return declared ? dependencyTypeOf(declared) : void 0;
4191
+ }
4192
+ /**
4193
+ * Does this call go INTO a package? Structurally, never by name:
4194
+ *
4195
+ * this.authz.can(…) `authz` injected as a type imported from a package
4196
+ * this.resource.avatar.getVariant a column of the store whose declared type is a package's
4197
+ * attachmentManager.create(…) the receiver itself imported from a package
4198
+ *
4199
+ * The specifier comes back so the note can say which package.
4200
+ */
4201
+ function packageTargetOf(call, owner, file, symbols, app, stores, sourceFile) {
4202
+ const callee = call.getExpression();
4203
+ if (!Node.isPropertyAccessExpression(callee)) return null;
4204
+ const receiver = unwrapAwait(callee.getExpression());
4205
+ if (Node.isPropertyAccessExpression(receiver) && Node.isThisExpression(receiver.getExpression())) {
4206
+ const declared = declaredTypeOfProperty(owner, receiver.getName());
4207
+ if (declared) return packageSpecifierOfType(declared, file, app);
4208
+ }
4209
+ if (Node.isPropertyAccessExpression(receiver)) {
4210
+ const holder = receiver.getExpression();
4211
+ const holderText = holder.getText();
4212
+ const store = symbols.get(rootSymbolOf(holder) ?? "") ?? (holderText === "this.resource" && owner ? transformerResourceOf(owner) : null);
4213
+ const declared = store ? stores.get(store) : void 0;
4214
+ const modelFile = declared ? sourceFile(declared.provenance.file) : null;
4215
+ if (declared && modelFile) {
4216
+ const type = modelFile.getClasses().flatMap((c) => c.getProperties()).find((p) => p.getName() === receiver.getName())?.getTypeNode()?.getText() ?? declared.attributes.find((a) => a.name === receiver.getName())?.type;
4217
+ if (type) return packageSpecifierOfType(type, modelFile, app);
4218
+ }
4219
+ }
4220
+ if (Node.isIdentifier(receiver)) return packageSpecifierOfType(receiver.getText(), file, app);
4221
+ return null;
4222
+ }
4223
+ /**
4224
+ * A member the resolved file does not declare, because it is a package's: the
4225
+ * exported symbol is a const built by a package factory, or a class extending a
4226
+ * package base. Returns the package's specifier.
4227
+ */
4228
+ function packageMemberOriginOf(file, member, app) {
4229
+ const rootOf = (node) => {
4230
+ let current = node;
4231
+ for (let depth = 0; depth < 20; depth++) {
4232
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
4233
+ current = current.getExpression();
4234
+ continue;
4235
+ }
4236
+ return Node.isIdentifier(current) ? current.getText() : null;
4237
+ }
4238
+ return null;
4239
+ };
4240
+ const defaultExported = file.getExportAssignments().map((e) => e.getExpression().getText());
4241
+ const declarations = file.getVariableDeclarations();
4242
+ const exported = declarations.filter((d) => d.getVariableStatement()?.isExported() || defaultExported.includes(d.getName()));
4243
+ for (const declaration of exported.length > 0 ? exported : declarations) {
4244
+ const initializer = declaration.getInitializer();
4245
+ if (!initializer || !Node.isCallExpression(initializer)) continue;
4246
+ const root = rootOf(initializer);
4247
+ const specifier = root ? packageSpecifierOfType(root, file, app) : null;
4248
+ if (specifier) return specifier;
4249
+ }
4250
+ for (const cls of file.getClasses()) {
4251
+ if (member && cls.getMethod(member)) return null;
4252
+ const parent = cls.getExtends()?.getExpression();
4253
+ const root = parent ? rootOf(parent) : null;
4254
+ const specifier = root ? packageSpecifierOfType(root, file, app) : null;
4255
+ if (specifier) return specifier;
4256
+ }
4257
+ return null;
4258
+ }
4006
4259
  function resolveTypeToFile(typeName, file, app) {
4007
4260
  const bare = typeName.replace(/<.*/, "").trim();
4008
4261
  for (const declaration of file.getImportDeclarations()) {
@@ -5763,9 +6016,32 @@ var CoverageTooLowError = class extends Error {
5763
6016
  this.name = "CoverageTooLowError";
5764
6017
  }
5765
6018
  };
6019
+ /**
6020
+ * One note per call site into a package, naming the transactions that reach it —
6021
+ * outside the boundary (§4): what a package does with its own tables is technical,
6022
+ * what it hands back is a value. Never in coverage (plan 0.11 §A).
6023
+ */
6024
+ function packageCallNotes(entryPoints, behaviors, emit) {
6025
+ const sites = /* @__PURE__ */ new Map();
6026
+ for (const entry of entryPoints) {
6027
+ const behavior = behaviors.get(entry.id);
6028
+ for (const call of behavior?.packageCalls ?? []) {
6029
+ const key = `${call.file}:${call.line}:${call.expression}`;
6030
+ const site = sites.get(key) ?? {
6031
+ specifier: call.specifier,
6032
+ expression: call.expression,
6033
+ where: call.line > 0 ? `${emit(call.file)}:${call.line}` : emit(call.file),
6034
+ reached: []
6035
+ };
6036
+ if (!site.reached.includes(entry.identity)) site.reached.push(entry.identity);
6037
+ sites.set(key, site);
6038
+ }
6039
+ }
6040
+ return [...sites.values()].sort((a, b) => a.where.localeCompare(b.where)).map((site) => `${site.reached.join(", ")}: call into ${site.specifier} (${site.expression}) — outside the boundary; what a package does with its own tables is technical (§4), what it hands back is a value [${site.where}]`);
6041
+ }
5766
6042
  async function analyze(root, options = {}) {
5767
6043
  const app = await discoverApp(root);
5768
- const { stores, unresolved: storeProblems } = await collectDataStores(app);
6044
+ const { stores, unresolved: storeProblems, notes: storeNotes } = await collectDataStores(app);
5769
6045
  const routes = await collectEntryPoints(app);
5770
6046
  const routeProblems = routes.unresolved;
5771
6047
  const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
@@ -5895,6 +6171,10 @@ async function analyze(root, options = {}) {
5895
6171
  transformedStores: behavior.transformedStores,
5896
6172
  delivered: behavior.delivered,
5897
6173
  pageReads: behavior.pageReads,
6174
+ packageCalls: behavior.packageCalls.map((c) => ({
6175
+ ...c,
6176
+ file: emit(c.file)
6177
+ })),
5898
6178
  unreadablePages: behavior.unreadablePages,
5899
6179
  outputReads: behavior.outputReads,
5900
6180
  trace: behavior.trace.map((step) => ({
@@ -5915,7 +6195,8 @@ async function analyze(root, options = {}) {
5915
6195
  unresolved: unresolvedSites.map((site) => ({
5916
6196
  ...site,
5917
6197
  file: emit(site.file)
5918
- }))
6198
+ })),
6199
+ notes: [...storeNotes, ...packageCallNotes(entryPoints, behaviors, emit)]
5919
6200
  };
5920
6201
  const minimum = options.minCoverage ?? 0;
5921
6202
  if (inventory.coverage.ratio < minimum) throw new CoverageTooLowError(inventory.coverage.ratio, minimum);
@@ -1368,4 +1368,4 @@ function ignoreCalls(options) {
1368
1368
  };
1369
1369
  }
1370
1370
  //#endregion
1371
- export { relativeTo as _, chainShapeOf as a, unwrap as c, collectEventBindings as d, detectAccess as f, isSeeder as g, isApplicationCode as h, resolveCall as i, DISPATCH_METHODS as l, rootSymbolOf as m, ignoreCalls as n, mappedLiteralOf as o, hooksFiredBy as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, EXECUTION_METHODS as u, samePath as v, toPosix as y };
1371
+ export { isSeeder as _, chainShapeOf as a, toPosix as b, transformerResourceOf as c, EXECUTION_METHODS as d, collectEventBindings as f, isApplicationCode as g, rootSymbolOf as h, resolveCall as i, unwrap as l, hooksFiredBy as m, ignoreCalls as n, mappedLiteralOf as o, detectAccess as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, DISPATCH_METHODS as u, relativeTo as v, samePath as y };
@@ -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-CneCj3sT.js";
3
- import { n as analyze } from "./pipeline-4sZBYGKU.js";
2
+ import { b as toPosix } from "./resolvers-Bz1F-Tgf.js";
3
+ import { n as analyze } from "./pipeline-CdDJRsLM.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -312,7 +312,8 @@ async function runInventory(options) {
312
312
  `data stores: ${inventory.dataStores.length}`,
313
313
  `entry points: ${coverage.entryPointsTotal}`,
314
314
  `coverage: ${(coverage.ratio * 100).toFixed(1)}% (${coverage.unresolvedCalls} unresolved calls)`,
315
- ...inventory.unresolved.map((site) => ` ${describeSite(site)}`)
315
+ ...inventory.unresolved.map((site) => ` ${describeSite(site)}`),
316
+ ...inventory.notes.length > 0 ? ["notes (declarations, not gaps):", ...inventory.notes.map((note) => ` ${note}`)] : []
316
317
  ].join("\n")
317
318
  };
318
319
  }
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
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";
1
+ import { t as CoverageTooLowError } from "../pipeline-CdDJRsLM.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-CzzlX7h-.js";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,3 +1,4 @@
1
+ import { Node } from 'ts-morph';
1
2
  import type { ClassDeclaration, SourceFile } from 'ts-morph';
2
3
  import type { AppContext } from '../app_context.js';
3
4
  import type { CollectedDataStore } from '../sources/data_stores.js';
@@ -104,6 +105,12 @@ export type Behavior = {
104
105
  */
105
106
  pageReads: Record<string, string[]>;
106
107
  unreadablePages: Record<string, string>;
108
+ /**
109
+ * Calls into a PACKAGE the walk met on the way — an injected package type, a
110
+ * package-typed column, a package factory's product, a member inherited from a
111
+ * package base. Outside the boundary (§4): noted, never a gap (plan 0.11 §A).
112
+ */
113
+ packageCalls: PackageCall[];
107
114
  trace: TraceStep[];
108
115
  /** bodies reached, for `fp:diff` */
109
116
  scope: ScopeEntry[];
@@ -157,6 +164,15 @@ export declare function analyzeHandler(app: AppContext, stores: CollectedDataSto
157
164
  * as any import. No type checker is needed.
158
165
  */
159
166
  export declare function injectedFor(owner: ClassDeclaration | undefined, file: SourceFile, app: AppContext): Map<string, string>;
167
+ /** type identifier -> application file where it is declared */
168
+ /** a call whose target resolves to a package: where, what, which package */
169
+ export type PackageCall = {
170
+ file: string;
171
+ line: number;
172
+ expression: string;
173
+ specifier: string;
174
+ };
175
+ export declare function membersOfType(typeNode: Node, file: SourceFile, app: AppContext): Map<string, string>;
160
176
  /**
161
177
  * Both maps a file's imports produce: where a local name resolves, and what it
162
178
  * was called where it was exported.
@@ -1,6 +1,9 @@
1
+ import { Node } from 'ts-morph';
1
2
  import type { CallExpression, ClassDeclaration } from 'ts-morph';
2
3
  /** Is this call one that cannot reach a data store? */
3
- export declare function isNoise(call: CallExpression, owner?: ClassDeclaration): boolean;
4
+ /** resolves a type annotation to its members (`name -> type text`), when the caller can */
5
+ export type MembersOf = (typeNode: Node) => Map<string, string>;
6
+ export declare function isNoise(call: CallExpression, owner?: ClassDeclaration, membersOf?: MembersOf): boolean;
4
7
  /**
5
8
  * Iteration over a list, checked BEFORE the resolvers run.
6
9
  *
@@ -1,2 +1,2 @@
1
- import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-CneCj3sT.js";
1
+ import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-Bz1F-Tgf.js";
2
2
  export { BUILTIN_CALL_RESOLVERS, ignoreCalls, isTechnicalWrite, resolveCall };
@@ -27,5 +27,12 @@ export type DataStoreCollection = {
27
27
  stores: CollectedDataStore[];
28
28
  /** chains that left the application, required by AFP §6.5.3 */
29
29
  unresolved: UnresolvedCall[];
30
+ /**
31
+ * Facts about the declarations that are not gaps: a base class or a mixin that
32
+ * comes from a package. What it adds is technical (a hashed password, audit
33
+ * stamps, a soft-delete mark) and never a user-recognisable attribute (§7.2); the
34
+ * model's own columns and the schema decide. Listed, never counted in coverage.
35
+ */
36
+ notes: string[];
30
37
  };
31
38
  export declare function collectDataStores(app: AppContext): Promise<DataStoreCollection>;
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-4sZBYGKU.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-CdDJRsLM.js";
2
2
  export { CoverageTooLowError, analyze };
@@ -148,6 +148,13 @@ export type HandlerBehavior = {
148
148
  pageReads?: Record<string, string[]>;
149
149
  /** stores a page could not be read for, and why: they leave whole */
150
150
  unreadablePages?: Record<string, string>;
151
+ /** calls into a package met on the way: outside the boundary, noted, never a gap (plan 0.11 §A) */
152
+ packageCalls?: {
153
+ file: string;
154
+ line: number;
155
+ expression: string;
156
+ specifier: string;
157
+ }[];
151
158
  /**
152
159
  * How each store was read: rows whole, `.select()` columns, or one aggregate
153
160
  * scalar; by its own chain (`direct`) or preloaded through another store (`via`).
@@ -222,6 +229,11 @@ export type Inventory = {
222
229
  * on. One entry per site — a body five routes reach is one gap, not five.
223
230
  */
224
231
  unresolved: UnresolvedSite[];
232
+ /**
233
+ * Facts about the declarations that are not gaps — a base class or a mixin from a
234
+ * package, whose additions are technical. Listed for the reader; never in coverage.
235
+ */
236
+ notes: string[];
225
237
  };
226
238
  export type FunctionType = 'ILF' | 'EIF' | 'EI' | 'EO' | 'EQ';
227
239
  export type Complexity = 'low' | 'average' | 'high';
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.10.0",
4
+ "version": "0.11.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },