@filipebraida/adonis-function-points 0.9.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,99 @@ 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
+
62
+ ## 0.10.0
63
+
64
+ **Rule set `afp@1.9.0`.** One rule and two report fixes, from a team's third review of their
65
+ own count — the one they accepted as a baseline. The rule moves no point on the three
66
+ validated applications and no point on its fixture: it moves a gap from the wrong line to
67
+ the right one. A 0.9.0 baseline still has to be recounted, because the rule set says so.
68
+
69
+ ### Fixed
70
+
71
+ - **A warning names a transaction the way the table and the config keys do.** Four warnings
72
+ printed the route's own pattern (`GET /orders/:id`) where the table, `fp:explain` and the
73
+ keys of `overrides` / `boundary.ignoreEntryPoints` use the identity (`GET /orders/:param`,
74
+ §5). Copying from a warning into the configuration required a translation nobody was told
75
+ about. All of them print the identity now.
76
+ - **"leaves whole — page never reads X" contradicted itself.** The rule was right — a store
77
+ handed to a page that uses nothing of it the reader can see leaves whole, in the open, never
78
+ as a floor — and the sentence was not. It says now: "handed to page "x", and nothing the
79
+ reader can see uses X: every column counted, in the open".
80
+
81
+ ### Counting
82
+
83
+ - **The value of a transaction callback is what it returns.** `const { row } = await
84
+ db.transaction(async (trx) => { … return { row: created } })`, then `row.save()`: the write
85
+ was an unreadable receiver. The callback is a body — its own locals are bound first, in
86
+ source order — and every `return` names the store the value holds, whole or under one key
87
+ of a returned literal; `Model.transaction` and `trx.transaction` are the same shape. A
88
+ returned number binds nothing; a raw-query row nobody can type stays reported. On the three
89
+ applications the value of a transaction callback is used 17 times. The 0.8 rule "a value a
90
+ package built is not a store" narrows to a call with no function argument: `db` is a
91
+ package import, and `db.transaction(cb)` hands back the application's value.
92
+
93
+ ### Documented
94
+
95
+ - counting-decisions §3 gains the transaction-callback row of the bindings table.
96
+ - Fixtures: `transacao_valor` (18 FP — the same before and after; one unresolved call moves
97
+ from `row.save` to the raw-query row that deserves it); `inertia_pages` grows a page that
98
+ uses nothing of its rows (29 FP).
99
+ - Measured and set aside: a model's instance method that writes — one in 52 models on the
100
+ three applications, not a rule.
101
+
9
102
  ## 0.9.0
10
103
 
11
104
  **Rule set `afp@1.8.0`.** Two rules from a team's second review of their own count, one
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.8.0
16
+ Ruleset: afp@1.9.0
17
17
 
18
18
  type n FP
19
19
  ILF 2 14
@@ -14,27 +14,27 @@
14
14
  const commands = [
15
15
  {
16
16
  commandName: "fp:inventory",
17
- importer: () => import("../fp_inventory-B4Pv8Thz.js")
17
+ importer: () => import("../fp_inventory-D3_6QCD2.js")
18
18
  },
19
19
  {
20
20
  commandName: "fp:metrics",
21
- importer: () => import("../fp_metrics-DoS5riGr.js")
21
+ importer: () => import("../fp_metrics-lnnhnVaw.js")
22
22
  },
23
23
  {
24
24
  commandName: "fp:count",
25
- importer: () => import("../fp_count-Bjviq8dz.js")
25
+ importer: () => import("../fp_count-mDQgQeEq.js")
26
26
  },
27
27
  {
28
28
  commandName: "fp:explain",
29
- importer: () => import("../fp_explain-CrFcMeXU.js")
29
+ importer: () => import("../fp_explain-CQESTS5W.js")
30
30
  },
31
31
  {
32
32
  commandName: "fp:diff",
33
- importer: () => import("../fp_diff-BSK1kKLQ.js")
33
+ importer: () => import("../fp_diff-BnWi2Fx8.js")
34
34
  },
35
35
  {
36
36
  commandName: "fp:calibrate",
37
- importer: () => import("../fp_calibrate-DJspMFHc.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-BuZNr-FE.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-DqPrDZfD.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,10 +518,77 @@ function walkChain(start, app, project) {
489
518
  classes,
490
519
  attributes: [...attributes.values()],
491
520
  columnSource,
492
- unresolved
521
+ unresolved,
522
+ notes
493
523
  };
494
524
  }
495
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()
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;
590
+ }
591
+ /**
496
592
  * Why the base class could not be resolved.
497
593
  *
498
594
  * The right reason matters as much as the fact: saying "outside the
@@ -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
@@ -2168,7 +2289,7 @@ function readPages(deliveries, env) {
2168
2289
  for (const { store } of readable) {
2169
2290
  if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
2170
2291
  const members = [...reading.unknownMembers.get(store) ?? []];
2171
- markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}" never reads ${store}`);
2292
+ markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `handed to page "${page}", and nothing the reader can see uses ${store}: every column counted, in the open`);
2172
2293
  }
2173
2294
  }
2174
2295
  return reading;
@@ -3065,6 +3186,59 @@ function createAnalyzer(app, stores, options = {}) {
3065
3186
  }
3066
3187
  return found.size === 1 ? [...found][0] : null;
3067
3188
  };
3189
+ /**
3190
+ * `await db.transaction(async (trx) => { … return … })` — also `Model.transaction`
3191
+ * and `trx.transaction` (a savepoint): a body whose VALUE is what it returns. The
3192
+ * callback's own locals are bound first, in source order, so `return { row: created }`
3193
+ * can be read; then every return names one store, or nothing binds (plan 0.10 §B).
3194
+ */
3195
+ const transactionCallbackOf = (call) => {
3196
+ const callee = call.getExpression();
3197
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "transaction") return null;
3198
+ return call.getArguments().find((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a)) ?? null;
3199
+ };
3200
+ const returnsOf = (callback) => {
3201
+ if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return [];
3202
+ const callbackBody = callback.getBody();
3203
+ if (!Node.isBlock(callbackBody)) return [callbackBody];
3204
+ const returned = [];
3205
+ for (const statement of callbackBody.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
3206
+ const owner = statement.getFirstAncestor((n) => Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) || Node.isMethodDeclaration(n));
3207
+ const expression = statement.getExpression();
3208
+ if (owner === callback && expression) returned.push(expression);
3209
+ }
3210
+ return returned;
3211
+ };
3212
+ const bindCallbackLocals = (callback) => {
3213
+ for (const declaration of callback.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3214
+ const nameNode = declaration.getNameNode();
3215
+ const initializer = declaration.getInitializer();
3216
+ if (!initializer || !Node.isIdentifier(nameNode)) continue;
3217
+ const store = storeOfValue(initializer);
3218
+ if (store) symbols.set(nameNode.getText(), store);
3219
+ }
3220
+ };
3221
+ /** the store every return of the callback names — whole, or under one key of a returned literal */
3222
+ const transactionValueStore = (callback, key) => {
3223
+ bindCallbackLocals(callback);
3224
+ const returned = returnsOf(callback);
3225
+ if (returned.length === 0) return null;
3226
+ const found = /* @__PURE__ */ new Set();
3227
+ for (const expression of returned) {
3228
+ let value = unwrapAwait(expression);
3229
+ if (key) {
3230
+ if (!Node.isObjectLiteralExpression(value)) return null;
3231
+ const property = value.getProperty(key);
3232
+ if (!property) return null;
3233
+ value = Node.isPropertyAssignment(property) ? property.getInitializer() : Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
3234
+ if (!value) return null;
3235
+ }
3236
+ const store = storeOfValue(value);
3237
+ if (!store) return null;
3238
+ found.add(store);
3239
+ }
3240
+ return found.size === 1 ? [...found][0] : null;
3241
+ };
3068
3242
  const isAuthUser = (node) => {
3069
3243
  if (!authUserStore) return false;
3070
3244
  const chain = Node.isCallExpression(node) ? node.getExpression() : node;
@@ -3097,6 +3271,8 @@ function createAnalyzer(app, stores, options = {}) {
3097
3271
  if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
3098
3272
  if (Node.isCallExpression(node)) {
3099
3273
  const callee = node.getExpression();
3274
+ const callback = transactionCallbackOf(node);
3275
+ if (callback) return transactionValueStore(callback);
3100
3276
  if (Node.isPropertyAccessExpression(callee)) {
3101
3277
  /**
3102
3278
  * `q.forUpdate()`, `q.where(…)`: a query-builder chain hands the same rows on —
@@ -3144,6 +3320,14 @@ function createAnalyzer(app, stores, options = {}) {
3144
3320
  */
3145
3321
  const store = storeOfValue(initializer);
3146
3322
  if (store) symbols.set(nameNode.getText(), store);
3323
+ } else if (Node.isObjectBindingPattern(nameNode)) {
3324
+ const call = unwrapAwait(initializer);
3325
+ const callback = Node.isCallExpression(call) ? transactionCallbackOf(call) : null;
3326
+ if (!callback) return;
3327
+ for (const element of nameNode.getElements()) {
3328
+ const store = transactionValueStore(callback, element.getPropertyNameNode()?.getText() ?? element.getName());
3329
+ if (store) symbols.set(element.getName(), store);
3330
+ }
3147
3331
  }
3148
3332
  return;
3149
3333
  }
@@ -3189,6 +3373,7 @@ function createAnalyzer(app, stores, options = {}) {
3189
3373
  const accesses = [];
3190
3374
  const followUps = [];
3191
3375
  const unresolved = [];
3376
+ const packageCalls = [];
3192
3377
  const reads = [];
3193
3378
  /** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
3194
3379
  const followedCalls = /* @__PURE__ */ new Map();
@@ -3308,7 +3493,14 @@ function createAnalyzer(app, stores, options = {}) {
3308
3493
  * counted as EOs under a coverage of 99.5%, because a write on an unknown
3309
3494
  * local was dropped without a word. It lowers coverage and is named.
3310
3495
  */
3311
- 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))) {
3312
3504
  unresolved.push({
3313
3505
  file: ref.file,
3314
3506
  line: call.getStartLineNumber(),
@@ -3317,12 +3509,24 @@ function createAnalyzer(app, stores, options = {}) {
3317
3509
  });
3318
3510
  continue;
3319
3511
  }
3320
- if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
3321
- file: ref.file,
3322
- line: call.getStartLineNumber(),
3323
- expression: call.getExpression().getText().replace(/\s+/g, ""),
3324
- reason: "call that no strategy knew how to follow"
3325
- });
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
+ }
3326
3530
  }
3327
3531
  /**
3328
3532
  * Read after the loop: whether a key holds a nested transformer is known only
@@ -3342,6 +3546,7 @@ function createAnalyzer(app, stores, options = {}) {
3342
3546
  followUps,
3343
3547
  unresolved,
3344
3548
  validators: validator.fields,
3549
+ packageCalls,
3345
3550
  commandFields,
3346
3551
  opaqueValidators: validator.opaque,
3347
3552
  requestFields: request.fields,
@@ -3451,6 +3656,7 @@ function createAnalyzer(app, stores, options = {}) {
3451
3656
  const writtenStores = /* @__PURE__ */ new Set();
3452
3657
  const inputFields = /* @__PURE__ */ new Set();
3453
3658
  const commandFields = /* @__PURE__ */ new Set();
3659
+ const packageCalls = [];
3454
3660
  const opaqueInputFields = /* @__PURE__ */ new Set();
3455
3661
  const requestFields = /* @__PURE__ */ new Set();
3456
3662
  let opaqueRequest = false;
@@ -3614,12 +3820,36 @@ function createAnalyzer(app, stores, options = {}) {
3614
3820
  * Dropping it silently is the worst possible defect: the transaction
3615
3821
  * loses a path and nobody knows.
3616
3822
  */
3617
- if (!isNoiseMember(ref.file, ref.member)) unresolved.push({
3618
- file: ref.file,
3619
- line: ref.line ?? 0,
3620
- expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
3621
- reason: "body not found in the resolved file: probably inherited from a package class"
3622
- });
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
+ }
3623
3853
  return;
3624
3854
  }
3625
3855
  let bodyWrites = false;
@@ -3639,6 +3869,7 @@ function createAnalyzer(app, stores, options = {}) {
3639
3869
  unresolved.push(...facts.unresolved);
3640
3870
  for (const field of facts.validators) inputFields.add(field);
3641
3871
  for (const field of facts.commandFields) commandFields.add(field);
3872
+ for (const packaged of facts.packageCalls) packageCalls.push(packaged);
3642
3873
  for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
3643
3874
  for (const field of facts.requestFields) requestFields.add(field);
3644
3875
  if (facts.opaqueRequest) opaqueRequest = true;
@@ -3705,6 +3936,7 @@ function createAnalyzer(app, stores, options = {}) {
3705
3936
  writtenStores: [...writtenStores].sort(),
3706
3937
  inputFields: [...inputFields].sort(),
3707
3938
  commandFields: [...commandFields].sort(),
3939
+ packageCalls,
3708
3940
  opaqueInputFields: [...opaqueInputFields].sort(),
3709
3941
  requestFields: [...requestFields].sort(),
3710
3942
  opaqueRequest,
@@ -3939,7 +4171,91 @@ function dependencyTypeOf(node) {
3939
4171
  if (Node.isIdentifier(target)) return target.getText();
3940
4172
  }
3941
4173
  }
3942
- /** 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
+ }
3943
4259
  function resolveTypeToFile(typeName, file, app) {
3944
4260
  const bare = typeName.replace(/<.*/, "").trim();
3945
4261
  for (const declaration of file.getImportDeclarations()) {
@@ -4194,8 +4510,15 @@ function isUnreadableWrite(call, symbols, imports, packageImports, body) {
4194
4510
  const nameNode = declaration.getNameNode();
4195
4511
  if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
4196
4512
  const initializer = declaration.getInitializer();
4197
- const origin = initializer ? rootSymbolOf(unwrapAwait(initializer)) : null;
4198
- if (origin && packageImports.has(origin)) return false;
4513
+ const initialized = initializer ? unwrapAwait(initializer) : null;
4514
+ /**
4515
+ * `await db.transaction(async (trx) => …)` is rooted at a package too, but its value
4516
+ * is what the CALLBACK returns — the application's. Only a call with no function
4517
+ * argument is a package's own object.
4518
+ */
4519
+ const handsBackACallback = initialized && Node.isCallExpression(initialized) && initialized.getArguments().some((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a));
4520
+ const origin = initialized ? rootSymbolOf(initialized) : null;
4521
+ if (origin && packageImports.has(origin) && !handsBackACallback) return false;
4199
4522
  }
4200
4523
  return true;
4201
4524
  }
@@ -5142,13 +5465,14 @@ const RULESET = "afp";
5142
5465
  * the guard's user, a service the container made — and a write nobody can type is
5143
5466
  * an unresolved call instead of silence. Two in 1.8.0: a method the model
5144
5467
  * declares names the store its caller writes, and a listener written inline on a
5145
- * string event is followed like a listener class.
5468
+ * string event is followed like a listener class. One in 1.9.0: the value of a
5469
+ * transaction callback is what its returns name.
5146
5470
  *
5147
5471
  * Without the bump, a baseline saved by the previous version compares cleanly
5148
5472
  * against this one and bills the tool's own improvement as work done. The guard
5149
5473
  * exists for exactly that, and only this constant arms it.
5150
5474
  */
5151
- const RULESET_VERSION = "1.8.0";
5475
+ const RULESET_VERSION = "1.9.0";
5152
5476
  function count(input, options = {}) {
5153
5477
  const warnings = [];
5154
5478
  const usage = usageOf(input);
@@ -5296,7 +5620,7 @@ function unreadableDeliveryWarnings(input) {
5296
5620
  if (blind.length === 0) return [];
5297
5621
  return [
5298
5622
  `${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
5299
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
5623
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.delivered.opaqueFields.join(", ")}`),
5300
5624
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5301
5625
  ];
5302
5626
  }
@@ -5312,7 +5636,7 @@ function unreadablePageWarnings(input) {
5312
5636
  for (const entry of input.entryPoints) {
5313
5637
  const behavior = input.behaviors.get(entry.id);
5314
5638
  if (!behavior || behavior.writes) continue;
5315
- for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.trigger} ${entry.signature}: ${store} leaves whole — ${reason}`);
5639
+ for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.identity}: ${store} leaves whole — ${reason}`);
5316
5640
  }
5317
5641
  if (lines.length === 0) return [];
5318
5642
  return [
@@ -5446,7 +5770,7 @@ function unreadableInputWarnings(input) {
5446
5770
  if (blind.length === 0) return [];
5447
5771
  return [
5448
5772
  `${blind.length} transaction(s) read the request without enumerating fields (\`all()\`, \`body()\`, \`except()\`), so their input DETs could not be counted and each sits at the floor of its band. This UNDERSTATES the total — the fix is a validator, not a configuration:`,
5449
- ...blind.slice(0, 10).map(({ entry }) => ` ${entry.trigger} ${entry.signature}`),
5773
+ ...blind.slice(0, 10).map(({ entry }) => ` ${entry.identity}`),
5450
5774
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5451
5775
  ];
5452
5776
  }
@@ -5467,7 +5791,7 @@ function unreadableOutputWarnings(input) {
5467
5791
  if (blind.length === 0) return [];
5468
5792
  return [
5469
5793
  `${blind.length} transaction(s) pass through a transformer that spreads something the analysis cannot read, counted as 1 DET each — a FLOOR. This UNDERSTATES the output; the fix is in the transformer (\`this.pick(...)\` or named keys), not a configuration:`,
5470
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.opaqueOutputFields.join(", ")}`),
5794
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.opaqueOutputFields.join(", ")}`),
5471
5795
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5472
5796
  ];
5473
5797
  }
@@ -5692,9 +6016,32 @@ var CoverageTooLowError = class extends Error {
5692
6016
  this.name = "CoverageTooLowError";
5693
6017
  }
5694
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
+ }
5695
6042
  async function analyze(root, options = {}) {
5696
6043
  const app = await discoverApp(root);
5697
- const { stores, unresolved: storeProblems } = await collectDataStores(app);
6044
+ const { stores, unresolved: storeProblems, notes: storeNotes } = await collectDataStores(app);
5698
6045
  const routes = await collectEntryPoints(app);
5699
6046
  const routeProblems = routes.unresolved;
5700
6047
  const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
@@ -5824,6 +6171,10 @@ async function analyze(root, options = {}) {
5824
6171
  transformedStores: behavior.transformedStores,
5825
6172
  delivered: behavior.delivered,
5826
6173
  pageReads: behavior.pageReads,
6174
+ packageCalls: behavior.packageCalls.map((c) => ({
6175
+ ...c,
6176
+ file: emit(c.file)
6177
+ })),
5827
6178
  unreadablePages: behavior.unreadablePages,
5828
6179
  outputReads: behavior.outputReads,
5829
6180
  trace: behavior.trace.map((step) => ({
@@ -5844,7 +6195,8 @@ async function analyze(root, options = {}) {
5844
6195
  unresolved: unresolvedSites.map((site) => ({
5845
6196
  ...site,
5846
6197
  file: emit(site.file)
5847
- }))
6198
+ })),
6199
+ notes: [...storeNotes, ...packageCallNotes(entryPoints, behaviors, emit)]
5848
6200
  };
5849
6201
  const minimum = options.minCoverage ?? 0;
5850
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-DqPrDZfD.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
  }
@@ -46,13 +46,14 @@ export declare const RULESET = "afp";
46
46
  * the guard's user, a service the container made — and a write nobody can type is
47
47
  * an unresolved call instead of silence. Two in 1.8.0: a method the model
48
48
  * declares names the store its caller writes, and a listener written inline on a
49
- * string event is followed like a listener class.
49
+ * string event is followed like a listener class. One in 1.9.0: the value of a
50
+ * transaction callback is what its returns name.
50
51
  *
51
52
  * Without the bump, a baseline saved by the previous version compares cleanly
52
53
  * against this one and bills the tool's own improvement as work done. The guard
53
54
  * exists for exactly that, and only this constant arms it.
54
55
  */
55
- export declare const RULESET_VERSION = "1.8.0";
56
+ export declare const RULESET_VERSION = "1.9.0";
56
57
  export type CountInput = {
57
58
  app: AppContext;
58
59
  stores: CollectedDataStore[];
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
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";
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-DqPrDZfD.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.9.0",
4
+ "version": "0.11.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },