@filipebraida/adonis-function-points 0.1.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.
Files changed (80) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +427 -0
  3. package/bin/cli.js +4 -0
  4. package/build/commands/fp_calibrate.d.ts +16 -0
  5. package/build/commands/fp_count.d.ts +11 -0
  6. package/build/commands/fp_diff.d.ts +16 -0
  7. package/build/commands/fp_explain.d.ts +15 -0
  8. package/build/commands/fp_inventory.d.ts +9 -0
  9. package/build/commands/main.d.ts +5 -0
  10. package/build/commands/main.js +120 -0
  11. package/build/commands/printer.d.ts +9 -0
  12. package/build/configure.d.ts +2 -0
  13. package/build/configure.js +10 -0
  14. package/build/define_config-DOqWyPwV.js +19 -0
  15. package/build/index.d.ts +5 -0
  16. package/build/index.js +4 -0
  17. package/build/pipeline-BzP-ITGN.js +2306 -0
  18. package/build/resolvers-CU9HKYpn.js +555 -0
  19. package/build/runners-Bt8tbISi.js +630 -0
  20. package/build/scripts/smoke_package.d.ts +1 -0
  21. package/build/src/albrecht/calibration.d.ts +62 -0
  22. package/build/src/albrecht/counter.d.ts +48 -0
  23. package/build/src/albrecht/data_functions.d.ts +32 -0
  24. package/build/src/albrecht/diff.d.ts +66 -0
  25. package/build/src/albrecht/index.d.ts +13 -0
  26. package/build/src/albrecht/tables.d.ts +19 -0
  27. package/build/src/albrecht/technical_filter.d.ts +25 -0
  28. package/build/src/albrecht/transactional_functions.d.ts +35 -0
  29. package/build/src/cli/load_config.d.ts +28 -0
  30. package/build/src/cli/print.d.ts +19 -0
  31. package/build/src/cli/runners.d.ts +52 -0
  32. package/build/src/cli.d.ts +19 -0
  33. package/build/src/cli.js +198 -0
  34. package/build/src/define_config.d.ts +137 -0
  35. package/build/src/inventory/app_context.d.ts +73 -0
  36. package/build/src/inventory/detectors/lucid.d.ts +77 -0
  37. package/build/src/inventory/graph/call_graph.d.ts +80 -0
  38. package/build/src/inventory/graph/noise.d.ts +9 -0
  39. package/build/src/inventory/index.d.ts +15 -0
  40. package/build/src/inventory/paths.d.ts +22 -0
  41. package/build/src/inventory/resolvers/action_object.d.ts +14 -0
  42. package/build/src/inventory/resolvers/index.d.ts +23 -0
  43. package/build/src/inventory/resolvers/index.js +2 -0
  44. package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
  45. package/build/src/inventory/resolvers/module_function.d.ts +11 -0
  46. package/build/src/inventory/resolvers/property_service.d.ts +18 -0
  47. package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
  48. package/build/src/inventory/resolvers/static_service.d.ts +13 -0
  49. package/build/src/inventory/resolvers/transformer.d.ts +25 -0
  50. package/build/src/inventory/resolvers/types.d.ts +65 -0
  51. package/build/src/inventory/source.d.ts +39 -0
  52. package/build/src/inventory/sources/data_stores.d.ts +31 -0
  53. package/build/src/inventory/sources/json_schemas.d.ts +32 -0
  54. package/build/src/inventory/sources/routes_ast.d.ts +28 -0
  55. package/build/src/metrics/structure.d.ts +72 -0
  56. package/build/src/pipeline.d.ts +44 -0
  57. package/build/src/pipeline.js +2 -0
  58. package/build/src/reporters/table.d.ts +6 -0
  59. package/build/src/types.d.ts +256 -0
  60. package/build/src/types.js +1 -0
  61. package/build/stubs/config.stub +37 -0
  62. package/build/tmp/probe.d.ts +1 -0
  63. package/build/tmp/probe_cli.d.ts +1 -0
  64. package/build/tmp/probe_cmp.d.ts +1 -0
  65. package/build/tmp/probe_count.d.ts +1 -0
  66. package/build/tmp/probe_data.d.ts +1 -0
  67. package/build/tmp/probe_diff.d.ts +1 -0
  68. package/build/tmp/probe_gap.d.ts +1 -0
  69. package/build/tmp/probe_graph.d.ts +1 -0
  70. package/build/tmp/probe_metrics.d.ts +1 -0
  71. package/build/tmp/probe_miss.d.ts +1 -0
  72. package/build/tmp/probe_names.d.ts +1 -0
  73. package/build/tmp/probe_nodata.d.ts +1 -0
  74. package/build/tmp/probe_one.d.ts +1 -0
  75. package/build/tmp/probe_perf.d.ts +1 -0
  76. package/build/tmp/probe_routes.d.ts +1 -0
  77. package/build/tmp/probe_unres.d.ts +1 -0
  78. package/build/tmp/probe_vazquez.d.ts +1 -0
  79. package/build/tsdown.config.d.ts +2 -0
  80. package/package.json +133 -0
@@ -0,0 +1,137 @@
1
+ import type { ComplexityTable } from './albrecht/tables.js';
2
+ import type { CallResolver } from './inventory/resolvers/types.js';
3
+ import type { Complexity, FunctionType } from './types.js';
4
+ /**
5
+ * Package configuration.
6
+ *
7
+ * **Every option here has an effect, and a test proving it.** Configuration the
8
+ * code does not honour is worse than no configuration at all: whoever sets it
9
+ * believes something changed when nothing did, and the number goes into an
10
+ * invoice.
11
+ *
12
+ * That is why the list is short. What remains configurable is what is a
13
+ * **business decision** that no heuristic should make — the application
14
+ * boundary, what is maintained externally, the calibrated complexity bands.
15
+ * The rest the package discovers.
16
+ */
17
+ export type FunctionPointsConfig = {
18
+ /**
19
+ * The application boundary. A business decision, not a technical one — review
20
+ * it with whoever signs the contract, not only with the team.
21
+ */
22
+ boundary: {
23
+ /**
24
+ * Infrastructure stores, excluded from the count: session tokens, audit
25
+ * trails, queues, caches.
26
+ *
27
+ * Complements the automatic technical-data filter (AFP §6.5.2.1.1), which
28
+ * already catches session, error, search and template names. Exclusions
29
+ * made here also appear in the report, with the reason.
30
+ */
31
+ infrastructure?: string[];
32
+ /**
33
+ * Stores maintained by another system: counted as EIF instead of ILF.
34
+ *
35
+ * For example, tables mirrored from an external ERP.
36
+ */
37
+ externallyMaintained?: string[];
38
+ /**
39
+ * Entry points with no functional value to the user, by route name or by
40
+ * identity (`GET /health`).
41
+ *
42
+ * Rarely needed in practice: an infrastructure route reaches no data store
43
+ * and already drops out. It stays as a safety net and to make the intent
44
+ * explicit in the report.
45
+ */
46
+ ignoreEntryPoints?: string[];
47
+ };
48
+ /**
49
+ * Strategy for RET, the logical subgroups of an ILF/EIF.
50
+ *
51
+ * `constant` pins it at 1, which is honest: what a user recognises as a
52
+ * subgroup is not derivable from code. `composition` derives it from
53
+ * composition relations; less accurate in general, but captures real
54
+ * aggregates.
55
+ */
56
+ retStrategy: 'constant' | 'composition';
57
+ /**
58
+ * Maximum depth in the call graph, starting at the handler.
59
+ *
60
+ * Too deep and a large shared service contaminates its callers; too shallow
61
+ * and the write is missed.
62
+ */
63
+ maxDepth: number;
64
+ /**
65
+ * Extra DET for the confirmation or error message.
66
+ *
67
+ * The IFPUG manual counts one, AFP does not — a known systematic divergence
68
+ * of −1 DET per transaction against manual counts. The default follows AFP.
69
+ */
70
+ messageDet: number;
71
+ /** Complexity bands, for calibrating against manual counts. */
72
+ complexityTables?: Partial<Record<FunctionType, ComplexityTable>>;
73
+ /** Weights per type and complexity. */
74
+ weights?: Partial<Record<FunctionType, Record<Complexity, number>>>;
75
+ /**
76
+ * Custom tracing strategies, added to the built-in ones and running
77
+ * **before** them.
78
+ *
79
+ * This is what makes the architecture's central claim true: AdonisJS imposes
80
+ * no code organisation, so a project with its own convention registers it
81
+ * here.
82
+ */
83
+ resolvers?: {
84
+ call?: CallResolver[];
85
+ };
86
+ /**
87
+ * Minimum tracing coverage, from 0 to 1.
88
+ *
89
+ * Below it the analysis **fails** instead of emitting a number that looks
90
+ * right. A total resting on many unresolved calls should not become an
91
+ * invoice.
92
+ */
93
+ minCoverage?: number;
94
+ /**
95
+ * Declared DET or RET/FTR for a function the analysis cannot read, keyed by
96
+ * the name it has in the count (`Petition`, `POST /books`).
97
+ *
98
+ * The case this exists for is a schema-driven application: when the fields a
99
+ * user fills live in a JSON column whose schema is stored in the database,
100
+ * there is nothing for static analysis to read and the column counts as 1 DET
101
+ * (counting-decisions §8). The person who knows the form knows the number.
102
+ *
103
+ * `reason` is required, and that is the whole point. A declared number is
104
+ * reproducible — it lives in a versioned file, so the same revision yields
105
+ * the same count — and auditable, because `fp:explain` prints it with its
106
+ * justification. A number the tool guessed would be neither.
107
+ *
108
+ * Use sparingly. If overriding becomes a habit the count stops coming from
109
+ * the code, and the report says how much of the total came from here so that
110
+ * cannot grow unnoticed.
111
+ */
112
+ overrides?: Record<string, FunctionOverride>;
113
+ };
114
+ export type FunctionOverride = {
115
+ /** declared DET count, replacing what the analysis found */
116
+ det?: number;
117
+ /**
118
+ * Name of a JSON Schema declared in the application's code, whose fields are
119
+ * counted by the §7 leaf rules and replace the single DET the opaque column
120
+ * contributed.
121
+ *
122
+ * Prefer this to `det`. A declared number freezes: someone adds a field, the
123
+ * count does not move, and `fp:diff` reports no change for real functional
124
+ * growth — undercounting silently and progressively. Naming the schema keeps
125
+ * the number coming from the code; the only thing maintained by hand is the
126
+ * mapping, which changes when a form is born rather than when a field is.
127
+ *
128
+ * A name that matches no schema is a warning, never a silent fallback.
129
+ */
130
+ detFromSchema?: string;
131
+ /** declared RET (data function) or FTR (transaction) */
132
+ refs?: number;
133
+ /** why — required, and printed by `fp:explain` beside the number */
134
+ reason: string;
135
+ };
136
+ export declare const DEFAULTS: FunctionPointsConfig;
137
+ export declare function defineConfig(config: Partial<FunctionPointsConfig>): FunctionPointsConfig;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Discovers the shape of the analysed application instead of assuming a
3
+ * convention.
4
+ *
5
+ * Folder layout, subpath aliases and model style all vary between AdonisJS
6
+ * applications. What does not vary is that the facts are discoverable — which
7
+ * is why this package needs almost no configuration.
8
+ */
9
+ export type AppLayout = 'flat' | 'module-per-domain' | 'unknown';
10
+ export type AppContext = {
11
+ /** application root (where adonisrc.ts lives) */
12
+ root: string;
13
+ /**
14
+ * The package.json `imports` map, normalised.
15
+ *
16
+ * It must be READ, never inferred: two incompatible conventions are in use —
17
+ * by artefact type (`#models/*`) and by domain module (`#catalog/*`).
18
+ */
19
+ subpathImports: Map<string, string>;
20
+ /**
21
+ * Generated artefacts found.
22
+ *
23
+ * Absence is a reportable fact, not something to work around silently:
24
+ * without the registry the count falls back to the route parser, which is
25
+ * less precise, and the report must say so.
26
+ */
27
+ generated: {
28
+ routeRegistry?: string;
29
+ controllersMap?: string;
30
+ dataSchema?: string;
31
+ };
32
+ /** detected; used to GROUP the report, never to find files */
33
+ layout: AppLayout;
34
+ /**
35
+ * Files that register routes, starting from the adonisrc `preloads` and
36
+ * following the static imports they reach.
37
+ *
38
+ * The preload list is the authoritative source, not a path convention. Four
39
+ * topologies occur in practice: a single file, one per module, a directory,
40
+ * and a hub that only re-exports.
41
+ */
42
+ routeFiles: string[];
43
+ /**
44
+ * Directories to scan, derived from the alias targets.
45
+ *
46
+ * Not simply `app/`: applications exist where all writes live under `src/`.
47
+ * Roots nested inside other roots are collapsed.
48
+ */
49
+ scanRoots: string[];
50
+ /** versions and ORM — they pick the strategy and go into the report */
51
+ framework: FrameworkInfo;
52
+ /** `#catalog/models/book` -> absolute path, or null */
53
+ resolveSpecifier(specifier: string): string | null;
54
+ /** module a file belongs to, for grouping the report */
55
+ moduleOf(absPath: string): string;
56
+ };
57
+ export type FrameworkInfo = {
58
+ /** major of @adonisjs/core, when declared */
59
+ core?: number;
60
+ /** major of @adonisjs/lucid, when declared */
61
+ lucid?: number;
62
+ orm: 'lucid' | 'kysely' | 'unknown';
63
+ /** Tuyau provides typed input DETs; optional */
64
+ tuyau: boolean;
65
+ /**
66
+ * Within the v1 scope (core 7 + Lucid 22).
67
+ *
68
+ * Outside it the package reports instead of counting — counting wrong in
69
+ * silence is the worst possible failure for a number that becomes an invoice.
70
+ */
71
+ supported: boolean;
72
+ };
73
+ export declare function discoverApp(root: string): Promise<AppContext>;
@@ -0,0 +1,77 @@
1
+ import { Node } from 'ts-morph';
2
+ import type { CallExpression } from 'ts-morph';
3
+ /**
4
+ * Recognises Lucid data access at a call site.
5
+ *
6
+ * **Call-site level, never file level.** A large domain service can hold dozens
7
+ * of writes; asking "does this file contain a write?" would mark every importer
8
+ * as a writer.
9
+ *
10
+ * False positives deliberately avoided here:
11
+ *
12
+ * `.related('x')` a relation accessor, used both to read and to write — it
13
+ * only counts when it ends in attach/detach/sync/save/create
14
+ * `.create(` also appears in `vine.create(` and various builders; it
15
+ * only counts when the receiver resolves to a known store
16
+ * `invite.save()` a lower-case instance does not match the model name under
17
+ * textual comparison — hence the symbol map
18
+ */
19
+ export type AccessMode = 'read' | 'write';
20
+ export type PersistenceAccess = {
21
+ mode: AccessMode;
22
+ /** id of the data store reached */
23
+ store: string;
24
+ method: string;
25
+ line: number;
26
+ /**
27
+ * Store reached through a RELATION rather than directly.
28
+ *
29
+ * `Book.query().preload('author')` reads the authors table. Under AFP that is
30
+ * an FTR on `Author`, and ignoring it would drop a table read only through a
31
+ * relation out of the count (§6.5.4) when it is a legitimate EIF.
32
+ */
33
+ viaRelation?: string;
34
+ /**
35
+ * Does this access fire the model's hooks?
36
+ *
37
+ * Lucid fires instance hooks for `document.delete()` and does NOT fire them
38
+ * for `Document.query().where(…).delete()` — both of which land on
39
+ * `method === 'delete'`. Following hooks for the bulk form would invent an
40
+ * FTR, and counting more than is there is worse than counting less: an
41
+ * invented FTR moves a complexity band and goes onto an invoice.
42
+ */
43
+ firesHooks: boolean;
44
+ };
45
+ /** decorators this package knows how to follow */
46
+ export declare const HOOK_DECORATORS: Set<string>;
47
+ /**
48
+ * Hook decorators fired by an access, or `[]` when it fires none.
49
+ *
50
+ * `truncate`, `increment`, `decrement` and the pivot operations change rows
51
+ * without instantiating a model, so no hook runs.
52
+ */
53
+ export declare function hooksFiredBy(access: PersistenceAccess): string[];
54
+ /**
55
+ * Symbols that resolve to a data store within a body's scope.
56
+ *
57
+ * Includes the model name (`Invite`) and local variables derived from it
58
+ * (`const invite = await Invite.findOrFail(...)`).
59
+ */
60
+ export type StoreSymbols = Map<string, string>;
61
+ /** store -> its declared relations */
62
+ export type RelationMap = Map<string, Record<string, string>>;
63
+ export declare function detectAccess(call: CallExpression, symbols: StoreSymbols, relations?: RelationMap): PersistenceAccess | null;
64
+ /**
65
+ * Dotted path of a receiver made only of property accesses: `input.invite`
66
+ * yields "input.invite". Any call in between invalidates the path, because the
67
+ * value stops being statically traceable.
68
+ */
69
+ export declare function pathSymbolOf(node: Node): string | null;
70
+ /**
71
+ * Root of an `a.b().c()` chain — the left-most identifier.
72
+ *
73
+ * It must traverse `await`, calls, property access and `new`, otherwise
74
+ * `await new Action().handle()` and `Invite.query().where().update()` stop at
75
+ * the first node and the write disappears.
76
+ */
77
+ export declare function rootSymbolOf(node: Node): string | null;
@@ -0,0 +1,80 @@
1
+ import type { ClassDeclaration, SourceFile } from 'ts-morph';
2
+ import type { AppContext } from '../app_context.js';
3
+ import type { CollectedDataStore } from '../sources/data_stores.js';
4
+ import type { CallResolver } from '../resolvers/types.js';
5
+ import type { HandlerRef, TraceStep, UnresolvedCall } from '../../types.js';
6
+ /**
7
+ * The transaction → data function graph. It is the backbone of the count.
8
+ *
9
+ * Three of the four boundary decisions are settled here: a static route does
10
+ * not count because it reaches no data; a route from a package likewise; a
11
+ * model hook counts because it lies on the path. And AFP requires aggregating
12
+ * ALL reachable paths:
13
+ *
14
+ * "When the static code analyzer finds multiple optional paths in the context
15
+ * of a transaction, it shall consider these multiple optional paths to be
16
+ * part of the same transaction." — AFP §6.5.3
17
+ *
18
+ * Traversal is at METHOD level, never at file level: a domain service holds
19
+ * many writes, and asking about the file would mark everyone importing it as a
20
+ * writer.
21
+ */
22
+ export type ScopeEntry = {
23
+ file: string;
24
+ member?: string;
25
+ /** hash of the normalised AST — counting-decisions §5 */
26
+ bodyHash: string;
27
+ };
28
+ export type Behavior = {
29
+ writes: boolean;
30
+ /** data stores reached */
31
+ touches: string[];
32
+ /**
33
+ * Declared input fields: `request.validateUsing(x)` resolved down to the
34
+ * fields of the VineJS schema — counting-decisions §7.
35
+ */
36
+ inputFields: string[];
37
+ trace: TraceStep[];
38
+ /** bodies reached, for `fp:diff` */
39
+ scope: ScopeEntry[];
40
+ unresolved: UnresolvedCall[];
41
+ };
42
+ export type GraphOptions = {
43
+ /** how far to follow from the handler; the default comes from configuration */
44
+ maxDepth?: number;
45
+ /**
46
+ * Custom strategies, added to the built-in ones and ordered by `order`.
47
+ *
48
+ * This is what makes tracing extensible: AdonisJS imposes no organisation
49
+ * pattern, so a project with its own convention registers it here.
50
+ */
51
+ callResolvers?: CallResolver[];
52
+ };
53
+ /**
54
+ * Analyzer with state shared across handlers.
55
+ *
56
+ * The ts-morph `Project` and the symbol cache are expensive to build and
57
+ * identical for every handler of the same application. Creating one per handler
58
+ * multiplies the cost by the number of routes, which is the difference between
59
+ * minutes and seconds on an application of a few hundred routes.
60
+ */
61
+ export declare function createAnalyzer(app: AppContext, stores: CollectedDataStore[], options?: GraphOptions): {
62
+ analyze: (handler: HandlerRef) => Behavior;
63
+ /** how many files the project loaded — used to prove it does not grow */
64
+ fileCount: () => number;
65
+ };
66
+ /** Convenience for a single handler; for several, use `createAnalyzer`. */
67
+ export declare function analyzeHandler(app: AppContext, stores: CollectedDataStore[], handler: HandlerRef, options?: GraphOptions): Behavior;
68
+ /**
69
+ * Injected dependencies visible in the body: property name -> file.
70
+ *
71
+ * Two forms, both with the type annotated explicitly — `@inject()` does not
72
+ * work without it:
73
+ *
74
+ * constructor(protected billing: BillingService) {}
75
+ * private declare billing: BillingService
76
+ *
77
+ * Since the type is an imported identifier, it resolves through the same path
78
+ * as any import. No type checker is needed.
79
+ */
80
+ export declare function injectedFor(owner: ClassDeclaration | undefined, file: SourceFile, app: AppContext): Map<string, string>;
@@ -0,0 +1,9 @@
1
+ import type { CallExpression, ClassDeclaration } from 'ts-morph';
2
+ /** Is this call one that cannot reach a data store? */
3
+ export declare function isNoise(call: CallExpression, owner?: ClassDeclaration): boolean;
4
+ /**
5
+ * The body-not-found path: a symbol resolved to an application file whose
6
+ * member is not there. For a framework service that is expected — the
7
+ * implementation is in the package — and says nothing about tracing quality.
8
+ */
9
+ export declare function isNoiseMember(file: string, member?: string): boolean;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Inventory layer: extracts raw facts from the application.
3
+ *
4
+ * ARCHITECTURAL RULE: nothing here imports from `src/albrecht/**`. The
5
+ * inventory does not know what an ILF is. That separation is what would allow
6
+ * extracting this layer into its own package if the structural metrics grow.
7
+ *
8
+ * SOURCE PRECEDENCE, most to least reliable:
9
+ * 1. generated artefacts — route registry, generated data schema
10
+ * 2. runtime — router.toJSON(), Lucid metadata
11
+ * 3. AST — call graph, write detection
12
+ * 4. folder convention — report grouping only, never for finding things
13
+ */
14
+ export * from './resolvers/index.js';
15
+ export type { AppContext } from './app_context.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One canonical spelling for every path the inventory emits.
3
+ *
4
+ * Two path styles meet in this package. ts-morph always returns forward
5
+ * slashes, including on Windows; node's `path.join` returns backslashes there.
6
+ * Both end up in `HandlerRef.file`, and the call graph uses that string as a
7
+ * cache key:
8
+ *
9
+ * const key = `${ref.file}#${ref.member ?? ref.line ?? '*'}`
10
+ *
11
+ * Two spellings of the same file are two keys, so the same body would be
12
+ * analysed twice and pushed twice into the trace and the implementation scope
13
+ * — and a repeated scope entry changes the hash `fp:diff` compares.
14
+ *
15
+ * Forward slashes win because ts-morph cannot be told otherwise, node's `fs`
16
+ * accepts them on Windows, and `path.relative` normalises mixed input anyway.
17
+ * Normalising at the boundary where a path is created costs one call; leaving
18
+ * it to each comparison costs vigilance forever.
19
+ */
20
+ export declare const toPosix: (value: string) => string;
21
+ /** Compares two paths that may have come from different sources. */
22
+ export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
@@ -0,0 +1,14 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Action object" pattern: the transaction delegates to an action instantiated
4
+ * at the call site.
5
+ *
6
+ * await new ExpireInvite().handle({ invite })
7
+ *
8
+ * const mark = new MarkContentChanged()
9
+ * await mark.handle({ documentId })
10
+ *
11
+ * The second form keeps the instance in a local variable, so the declaration
12
+ * has to be followed back to the `new` — that is what `classOfReceiver` does.
13
+ */
14
+ export declare const actionObjectResolver: CallResolver;
@@ -0,0 +1,23 @@
1
+ import type { CallResolver } from './types.js';
2
+ export * from './types.js';
3
+ /**
4
+ * Built-in strategies, ordered from most to least specific.
5
+ *
6
+ * They cover the patterns that appear in real AdonisJS applications. No closed
7
+ * list can cover them all — a project with its own convention registers it in
8
+ * `config/function_points.ts`, and it runs before these.
9
+ */
10
+ export declare const BUILTIN_CALL_RESOLVERS: CallResolver[];
11
+ /**
12
+ * The FIRST strategy that claims a call wins.
13
+ *
14
+ * This is not an implementation detail: syntactically identical shapes carry
15
+ * different meanings. `CreateUserJob.dispatch(p)`, `UserService.create(p)` and
16
+ * `User.find(p)` are all `Identifier.method(args)`, and only ordering tells
17
+ * them apart. Hence specific strategies declare a lower `order` than generic
18
+ * ones, and `module-function` comes last — it would match almost anything.
19
+ */
20
+ export declare function resolveCall(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): {
21
+ by: string;
22
+ refs: import('../../types.js').HandlerRef[];
23
+ } | null;
@@ -0,0 +1,2 @@
1
+ import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-CU9HKYpn.js";
2
+ export { BUILTIN_CALL_RESOLVERS, resolveCall };
@@ -0,0 +1,18 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Job" pattern: the write happens asynchronously.
4
+ *
5
+ * await CreateUserJob.dispatch({ userId })
6
+ *
7
+ * Runs before `static-service` on purpose: the syntactic shape is identical
8
+ * (`Identifier.method(args)`) and the generic strategy would swallow the job.
9
+ * The distinction is semantic, and it matters because the counting decision
10
+ * differs.
11
+ *
12
+ * COUNTING DECISION: a job dispatched by a handler is followed as part of the
13
+ * SAME transactional function, because IFPUG counts what the user recognises —
14
+ * they click and the effect happens, even if execution is asynchronous. A
15
+ * SCHEDULED job, which nobody dispatches, is a different thing: it is an entry
16
+ * point of its own, and out of v1 scope — only HTTP routes are collected.
17
+ */
18
+ export declare const jobDispatchResolver: CallResolver;
@@ -0,0 +1,11 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Module function" pattern: no class at all.
4
+ *
5
+ * await createUser(payload)
6
+ * await syncWithProvider(order)
7
+ *
8
+ * Runs last: it matches any call to an imported identifier and would otherwise
9
+ * swallow the more precise patterns.
10
+ */
11
+ export declare const moduleFunctionResolver: CallResolver;
@@ -0,0 +1,18 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Injected dependency" pattern: the call leaves through a class property.
4
+ *
5
+ * @inject()
6
+ * class InvoiceController {
7
+ * constructor(protected billing: BillingService) {}
8
+ * async queue() { await this.billing.enqueue(invoice) }
9
+ * }
10
+ *
11
+ * This is the official AdonisJS pattern, and in applications that use it, it is
12
+ * frequently the only path from a route down to a write.
13
+ *
14
+ * **No type checker required.** `@inject()` only works with an explicit type
15
+ * annotation — that annotation is how the container knows what to inject — so
16
+ * the type is always in the AST as an imported identifier.
17
+ */
18
+ export declare const propertyServiceResolver: CallResolver;
@@ -0,0 +1,17 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Same class method" pattern: `this.privateMethod()`.
4
+ *
5
+ * async expire(uuid: string) {
6
+ * const invite = await this.findByUuid(uuid)
7
+ * await this.persistExpiration(invite)
8
+ * }
9
+ *
10
+ * A public method delegating to private ones of the same class is where writes
11
+ * often live. No other strategy covers it — `property-service` requires
12
+ * `this.dependency.method()`, with two levels of access.
13
+ *
14
+ * Runs before `property-service` because it is more specific: the receiver is
15
+ * exactly `this`.
16
+ */
17
+ export declare const sameClassMethodResolver: CallResolver;
@@ -0,0 +1,13 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Static service" pattern: a class method called without instantiating.
4
+ *
5
+ * await UserService.create(payload)
6
+ * await OrderService.finalize(order)
7
+ *
8
+ * Careful: `Order.findByOrFail(...)` has exactly the same syntactic shape. The
9
+ * difference is semantic — a model is a data store, not a body to walk into,
10
+ * and the persistence detector handles it. Hence this resolver depends on
11
+ * `ctx.dataStoresBySymbol` already being populated.
12
+ */
13
+ export declare const staticServiceResolver: CallResolver;
@@ -0,0 +1,25 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Transformer" pattern: the package supplies the API, the application
4
+ * supplies the body.
5
+ *
6
+ * class InviteTransformer extends BaseTransformer<Invite> {
7
+ * toObject() { … }
8
+ * }
9
+ *
10
+ * InviteTransformer.transform(invite)
11
+ *
12
+ * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
13
+ * symbol lands on the application file and finds no body there. The naive
14
+ * reading is that the tracer must step into node_modules; it does not. Those
15
+ * methods call BACK into `toObject()`, which the application writes, so the
16
+ * body worth analysing was in the application all along.
17
+ *
18
+ * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
19
+ * `handle` executes.
20
+ *
21
+ * COUNTING DECISION: the write a transformer performs belongs to the
22
+ * transaction that serialised through it. Without this, a table written only
23
+ * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
24
+ */
25
+ export declare const transformerResolver: CallResolver;
@@ -0,0 +1,65 @@
1
+ import type { CallExpression, SourceFile } from 'ts-morph';
2
+ import type { DataStore, HandlerRef } from '../../types.js';
3
+ /**
4
+ * AdonisJS does not impose a code organisation. The same transaction can be
5
+ * written in very different ways:
6
+ *
7
+ * fat controller await User.create(payload)
8
+ * action object await new CreateUser().handle(payload)
9
+ * static service await UserService.create(payload)
10
+ * injected service await this.users.create(payload)
11
+ * repository await this.repo.persist(user)
12
+ * job await CreateUserJob.dispatch(payload)
13
+ * query builder await db.table('users').insert(payload)
14
+ *
15
+ * No closed list covers that. Tracing is therefore assembled from registrable
16
+ * strategies, and whatever none of them resolves is REPORTED — never silently
17
+ * treated as a read.
18
+ *
19
+ * That is the most important contract in the package: saying "I don't know" is
20
+ * preferable to producing a number that looks right.
21
+ */
22
+ export type ResolverContext = {
23
+ /** file containing the call site */
24
+ file: SourceFile;
25
+ /** current depth in the call graph */
26
+ depth: number;
27
+ /** file imports: local identifier -> resolved absolute path */
28
+ imports: Map<string, string>;
29
+ /**
30
+ * Injected dependencies visible in this body: property name -> class file.
31
+ * For example `billing` -> `.../billing_service.ts`.
32
+ *
33
+ * No type checker needed: AdonisJS `@inject()` **requires** an explicit type
34
+ * annotation for the container to resolve the dependency, so
35
+ * `constructor(protected billing: BillingService)` always carries the
36
+ * type as an identifier — imported like any other.
37
+ */
38
+ injected: Map<string, string>;
39
+ /**
40
+ * Known data stores, keyed by symbol name (e.g. 'User').
41
+ *
42
+ * ORDERING INVARIANT: data stores are collected BEFORE any handler analysis.
43
+ * Without that, `UserService.create()` and `User.create()` are
44
+ * indistinguishable by shape, and a resolver would walk into the model as if
45
+ * it were business code.
46
+ */
47
+ dataStoresBySymbol: Map<string, DataStore>;
48
+ /** resolves an AdonisJS specifier (`#collect/models/invite`) to a path */
49
+ resolveSpecifier(specifier: string): string | null;
50
+ /** loads a file into the project, if it exists */
51
+ sourceFile(absPath: string): SourceFile | null;
52
+ };
53
+ /**
54
+ * Follows a call site to the next body to analyse.
55
+ *
56
+ * Returns `[]` when the strategy does not recognise the call — that is not an
57
+ * error, another strategy may recognise it. Only when none does should the call
58
+ * land in `unresolved`.
59
+ */
60
+ export interface CallResolver {
61
+ readonly name: string;
62
+ /** lower runs first; specific strategies before generic ones */
63
+ readonly order?: number;
64
+ resolve(call: CallExpression, ctx: ResolverContext): HandlerRef[];
65
+ }