@filipebraida/adonis-function-points 0.1.0 → 0.2.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 (54) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +103 -2
  3. package/build/commands/fp_metrics.d.ts +10 -0
  4. package/build/commands/main.d.ts +6 -5
  5. package/build/commands/main.js +48 -114
  6. package/build/decorate-D6enDn9D.js +24 -0
  7. package/build/fp_calibrate-Cm079xWL.js +25 -0
  8. package/build/fp_count-CfcXPuj5.js +22 -0
  9. package/build/fp_diff-DE_t3twv.js +25 -0
  10. package/build/fp_explain-MKyEoi0h.js +24 -0
  11. package/build/fp_inventory-DSrCVhEy.js +18 -0
  12. package/build/fp_metrics-BUWj9dLw.js +20 -0
  13. package/build/index.d.ts +2 -1
  14. package/build/index.js +3 -2
  15. package/build/{pipeline-BzP-ITGN.js → pipeline-DySlMWcN.js} +298 -40
  16. package/build/{resolvers-CU9HKYpn.js → resolvers-MFjRl2ef.js} +225 -5
  17. package/build/{runners-Bt8tbISi.js → runners-DpMd-yZM.js} +252 -64
  18. package/build/src/albrecht/counter.d.ts +17 -1
  19. package/build/src/albrecht/data_functions.d.ts +6 -0
  20. package/build/src/albrecht/diff.d.ts +19 -1
  21. package/build/src/cli/runners.d.ts +12 -0
  22. package/build/src/cli.js +11 -2
  23. package/build/src/define_config.d.ts +35 -1
  24. package/build/src/inventory/graph/call_graph.d.ts +27 -0
  25. package/build/src/inventory/graph/noise.d.ts +13 -0
  26. package/build/src/inventory/resolvers/event_dispatch.d.ts +17 -0
  27. package/build/src/inventory/resolvers/index.js +1 -1
  28. package/build/src/inventory/resolvers/types.d.ts +35 -0
  29. package/build/src/inventory/sources/event_bindings.d.ts +44 -0
  30. package/build/src/metrics/structure.d.ts +16 -2
  31. package/build/src/pipeline.js +1 -1
  32. package/build/src/reporters/table.d.ts +11 -1
  33. package/build/src/types.d.ts +10 -0
  34. package/build/stubs/config.stub +27 -1
  35. package/package.json +2 -1
  36. package/build/scripts/smoke_package.d.ts +0 -1
  37. package/build/tmp/probe.d.ts +0 -1
  38. package/build/tmp/probe_cli.d.ts +0 -1
  39. package/build/tmp/probe_cmp.d.ts +0 -1
  40. package/build/tmp/probe_count.d.ts +0 -1
  41. package/build/tmp/probe_data.d.ts +0 -1
  42. package/build/tmp/probe_diff.d.ts +0 -1
  43. package/build/tmp/probe_gap.d.ts +0 -1
  44. package/build/tmp/probe_graph.d.ts +0 -1
  45. package/build/tmp/probe_metrics.d.ts +0 -1
  46. package/build/tmp/probe_miss.d.ts +0 -1
  47. package/build/tmp/probe_names.d.ts +0 -1
  48. package/build/tmp/probe_nodata.d.ts +0 -1
  49. package/build/tmp/probe_one.d.ts +0 -1
  50. package/build/tmp/probe_perf.d.ts +0 -1
  51. package/build/tmp/probe_routes.d.ts +0 -1
  52. package/build/tmp/probe_unres.d.ts +0 -1
  53. package/build/tmp/probe_vazquez.d.ts +0 -1
  54. package/build/tsdown.config.d.ts +0 -2
@@ -1,6 +1,7 @@
1
1
  import type { ClassDeclaration, SourceFile } from 'ts-morph';
2
2
  import type { AppContext } from '../app_context.js';
3
3
  import type { CollectedDataStore } from '../sources/data_stores.js';
4
+ import type { EventBindings } from '../sources/event_bindings.js';
4
5
  import type { CallResolver } from '../resolvers/types.js';
5
6
  import type { HandlerRef, TraceStep, UnresolvedCall } from '../../types.js';
6
7
  /**
@@ -34,6 +35,14 @@ export type Behavior = {
34
35
  * fields of the VineJS schema — counting-decisions §7.
35
36
  */
36
37
  inputFields: string[];
38
+ /**
39
+ * Fields read straight off the request. Kept apart from `inputFields` so the
40
+ * conformance metric keeps meaning what it says: these are DETs, and they are
41
+ * not a validator.
42
+ */
43
+ requestFields: string[];
44
+ /** the transaction reads the request in a way that enumerates nothing */
45
+ opaqueRequest: boolean;
37
46
  trace: TraceStep[];
38
47
  /** bodies reached, for `fp:diff` */
39
48
  scope: ScopeEntry[];
@@ -49,6 +58,12 @@ export type GraphOptions = {
49
58
  * pattern, so a project with its own convention registers it here.
50
59
  */
51
60
  callResolvers?: CallResolver[];
61
+ /**
62
+ * Which listeners each event reaches. Collected by the caller, because the
63
+ * binding lives in a preload file and is an application-wide fact, like the
64
+ * data stores.
65
+ */
66
+ eventBindings?: EventBindings;
52
67
  };
53
68
  /**
54
69
  * Analyzer with state shared across handlers.
@@ -60,6 +75,7 @@ export type GraphOptions = {
60
75
  */
61
76
  export declare function createAnalyzer(app: AppContext, stores: CollectedDataStore[], options?: GraphOptions): {
62
77
  analyze: (handler: HandlerRef) => Behavior;
78
+ writtenAnywhere: () => Set<string>;
63
79
  /** how many files the project loaded — used to prove it does not grow */
64
80
  fileCount: () => number;
65
81
  };
@@ -78,3 +94,14 @@ export declare function analyzeHandler(app: AppContext, stores: CollectedDataSto
78
94
  * as any import. No type checker is needed.
79
95
  */
80
96
  export declare function injectedFor(owner: ClassDeclaration | undefined, file: SourceFile, app: AppContext): Map<string, string>;
97
+ /**
98
+ * Both maps a file's imports produce: where a local name resolves, and what it
99
+ * was called where it was exported.
100
+ *
101
+ * Exported because the tests need the same answer the pipeline gets: a second
102
+ * implementation in the helpers drifted from this one and missed aliases.
103
+ */
104
+ export declare function importMapsOf(file: SourceFile, app: AppContext): {
105
+ imports: Map<string, string>;
106
+ exportedAs: Map<string, string>;
107
+ };
@@ -1,6 +1,19 @@
1
1
  import type { CallExpression, ClassDeclaration } from 'ts-morph';
2
2
  /** Is this call one that cannot reach a data store? */
3
3
  export declare function isNoise(call: CallExpression, owner?: ClassDeclaration): boolean;
4
+ /**
5
+ * Iteration over a list, checked BEFORE the resolvers run.
6
+ *
7
+ * `PAPEIS_CONCEDIVEIS.map((name) => …)` is `Identifier.method(args)`, the shape
8
+ * `static-service` exists for, so the resolver claimed it, resolved the enum
9
+ * module, found no `map` in it and reported a gap — noise never got asked,
10
+ * because it is only consulted once every resolver has declined.
11
+ *
12
+ * No resolver's pattern is `X.map(callback)`, so refusing this shape up front
13
+ * costs nothing and is not the same as silencing an unresolved call: nothing
14
+ * was ever there to resolve.
15
+ */
16
+ export declare function isIterationCall(call: CallExpression): boolean;
4
17
  /**
5
18
  * The body-not-found path: a symbol resolved to an application file whose
6
19
  * member is not there. For a framework service that is expected — the
@@ -0,0 +1,17 @@
1
+ import type { CallResolver } from './types.js';
2
+ /**
3
+ * "Event" pattern: the handler announces, and listeners act.
4
+ *
5
+ * await events.OrderPlaced.dispatch(order.id)
6
+ *
7
+ * Runs BEFORE `job-dispatch`, which matches `Identifier.dispatch(args)` — the
8
+ * shape the direct form takes. Left to it, the event class was resolved and
9
+ * searched for a `handle` it does not declare (`dispatch` comes from
10
+ * `BaseEvent`), so the call was reported as an unknown and the listeners' reads
11
+ * and writes went uncounted.
12
+ *
13
+ * COUNTING DECISION: the same one taken for a job. The user clicks, the effect
14
+ * happens, and AFP §6.5.3 requires aggregating every path the transaction
15
+ * reaches — the emitter is an implementation detail of how it gets there.
16
+ */
17
+ export declare const eventDispatchResolver: CallResolver;
@@ -1,2 +1,2 @@
1
- import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-CU9HKYpn.js";
1
+ import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-MFjRl2ef.js";
2
2
  export { BUILTIN_CALL_RESOLVERS, resolveCall };
@@ -1,5 +1,6 @@
1
1
  import type { CallExpression, SourceFile } from 'ts-morph';
2
2
  import type { DataStore, HandlerRef } from '../../types.js';
3
+ import type { EventBindings } from '../sources/event_bindings.js';
3
4
  /**
4
5
  * AdonisJS does not impose a code organisation. The same transaction can be
5
6
  * written in very different ways:
@@ -26,6 +27,16 @@ export type ResolverContext = {
26
27
  depth: number;
27
28
  /** file imports: local identifier -> resolved absolute path */
28
29
  imports: Map<string, string>;
30
+ /**
31
+ * Local identifier -> the name it was exported under, when they differ.
32
+ *
33
+ * `import { createUser as create }` binds `create` locally while the function
34
+ * is `createUser` in its own file. Following the local name looks for a body
35
+ * that does not exist, and the call is reported as unresolved for a reason
36
+ * that is not true. Identity is the (specifier, exported name) pair — the same
37
+ * mistake the data-store collector had to unlearn.
38
+ */
39
+ exportedAs: Map<string, string>;
29
40
  /**
30
41
  * Injected dependencies visible in this body: property name -> class file.
31
42
  * For example `billing` -> `.../billing_service.ts`.
@@ -45,6 +56,15 @@ export type ResolverContext = {
45
56
  * it were business code.
46
57
  */
47
58
  dataStoresBySymbol: Map<string, DataStore>;
59
+ /**
60
+ * Which listeners each event reaches, read from `emitter.on(...)`.
61
+ *
62
+ * Empty when the application declares no bindings. Like the data stores, this
63
+ * is collected BEFORE any handler analysis: a dispatch cannot be followed
64
+ * from the call site alone, because the binding lives in a preload file the
65
+ * handler never imports.
66
+ */
67
+ eventBindings: EventBindings;
48
68
  /** resolves an AdonisJS specifier (`#collect/models/invite`) to a path */
49
69
  resolveSpecifier(specifier: string): string | null;
50
70
  /** loads a file into the project, if it exists */
@@ -62,4 +82,19 @@ export interface CallResolver {
62
82
  /** lower runs first; specific strategies before generic ones */
63
83
  readonly order?: number;
64
84
  resolve(call: CallExpression, ctx: ResolverContext): HandlerRef[];
85
+ /**
86
+ * "This call is mine, and it reaches no data store."
87
+ *
88
+ * `resolve` has two outcomes where three are needed. Returning `[]` means
89
+ * *not recognised*, so a strategy that recognises a call perfectly well and
90
+ * knows it touches nothing countable had no way to say so: the call still
91
+ * landed in `unresolved`, and a project could not answer its own false
92
+ * positives without making the tool claim a body that does not exist.
93
+ *
94
+ * Declaring it here is deliberately louder than a name on a silence list.
95
+ * It costs a named strategy and a reason in the project's own config, and
96
+ * `fp:count` still reports the volume — because a silent drop is the worst
97
+ * defect this package can have, whoever writes it.
98
+ */
99
+ ignores?(call: CallExpression, ctx: ResolverContext): boolean;
65
100
  }
@@ -0,0 +1,44 @@
1
+ import { Node } from 'ts-morph';
2
+ import type { Expression, SourceFile } from 'ts-morph';
3
+ import type { AppContext } from '../app_context.js';
4
+ import type { HandlerRef } from '../../types.js';
5
+ /**
6
+ * Which listeners each event reaches.
7
+ *
8
+ * `events.OrderPlaced.dispatch(id)` in a handler is the user's click, and the
9
+ * write happens in a listener. AFP §6.5.3 requires aggregating every path a
10
+ * transaction reaches, so this is the same decision already taken for a job
11
+ * dispatch: the effect belongs to the transaction that caused it, whatever
12
+ * thread runs it.
13
+ *
14
+ * Without the binding the dispatch resolved to the event class, which declares
15
+ * no `dispatch` of its own — it inherits `BaseEvent` — so the call was reported
16
+ * as an unknown AND every read and write inside the listener went uncounted.
17
+ * That is the worst pairing: the gap is visible and the number is short.
18
+ *
19
+ * The binding is declared, not conventional: `emitter.on(event, [listeners])`
20
+ * in a preload file. It is therefore READ, never inferred from a name — the
21
+ * same rule the subpath imports follow.
22
+ */
23
+ /** event class file (POSIX) -> the listener bodies it reaches */
24
+ export type EventBindings = Map<string, HandlerRef[]>;
25
+ /**
26
+ * All these functions need of the application: how to resolve a specifier.
27
+ *
28
+ * Narrowed on purpose, so the resolver can ask the same question of a call site
29
+ * without being handed the whole context — and so this file cannot start
30
+ * depending on more of it by accident.
31
+ */
32
+ type SpecifierResolver = Pick<AppContext, 'resolveSpecifier'>;
33
+ export declare function collectEventBindings(app: AppContext): EventBindings;
34
+ /**
35
+ * The event class a dispatch or a binding names.
36
+ *
37
+ * Two shapes reach here: the class imported directly, and the generated
38
+ * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
39
+ * and therefore the common one. Exported because the resolver has to ask the
40
+ * same question of a call site, and two implementations of "which event is
41
+ * this" would drift.
42
+ */
43
+ export declare function resolveEventClass(expression: Expression | Node, from: SourceFile, app: SpecifierResolver): string | null;
44
+ export {};
@@ -50,8 +50,22 @@ export declare function measureStructure(inventory: Inventory, count: CountResul
50
50
  * is what turns into a standards audit.
51
51
  */
52
52
  export type Conformance = {
53
- /** write transactions whose input fields come from a validator */
54
- writesWithValidator: {
53
+ /**
54
+ * Of the transactions that TAKE input, how many declare it with a validator.
55
+ *
56
+ * The first version of this measured validators over all writes, and read
57
+ * 39% on a healthy application — which invited the conclusion that 61% of its
58
+ * writes were unvalidated. They were not: most were workflow triggers
59
+ * (`POST /orders/:id/submit`, `POST /orders/:id/clear`) that carry nothing
60
+ * beyond the route parameter, exactly as counting-decisions §7 describes. A
61
+ * metric that makes a reader draw a false conclusion is worse than no metric,
62
+ * and this one made its own author draw it.
63
+ *
64
+ * The denominator is therefore the transactions that read something: a
65
+ * validator, a `request.input(…)`, or an `all()`/`body()` the analysis cannot
66
+ * enumerate.
67
+ */
68
+ inputsWithValidator: {
55
69
  ok: number;
56
70
  total: number;
57
71
  ratio: number;
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-BzP-ITGN.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-DySlMWcN.js";
2
2
  export { CoverageTooLowError, analyze };
@@ -1,6 +1,16 @@
1
- import type { CountResult, CountedFunction } from '../types.js';
1
+ import type { CountResult, CountedFunction, Inventory } from '../types.js';
2
2
  import type { FunctionPointDiff } from '../albrecht/diff.js';
3
+ import type { Conformance, StructureMetrics } from '../metrics/structure.js';
3
4
  export declare function renderCount(result: CountResult): string;
4
5
  /** `fp:explain`: a function's provenance, which is what supports a dispute */
6
+ /**
7
+ * Structure and conformance, beside the count and never instead of it.
8
+ *
9
+ * If function points pay, the team optimises function points — more models, more
10
+ * endpoints, less reuse. Density and coupling on the same report are the
11
+ * counterweight, which is why this shares the inventory rather than collecting
12
+ * anything of its own.
13
+ */
14
+ export declare function renderMetrics(structure: StructureMetrics, conformance: Conformance, coverage: Inventory['coverage']): string;
5
15
  export declare function renderExplain(fn: CountedFunction): string;
6
16
  export declare function renderDiff(diff: FunctionPointDiff): string;
@@ -86,6 +86,16 @@ export type HandlerBehavior = {
86
86
  touches: string[];
87
87
  /** declared input fields (validators) */
88
88
  inputFields: Field[];
89
+ /**
90
+ * Input fields read straight off the request, with no validator.
91
+ *
92
+ * Kept apart from `inputFields` so the conformance metric keeps meaning what it
93
+ * says. They are DETs all the same: `request.input('title')` is a
94
+ * user-recognisable field crossing the boundary, which is §7.2's definition.
95
+ */
96
+ requestFields: Field[];
97
+ /** the transaction reads the request in a way that enumerates nothing */
98
+ opaqueRequest: boolean;
89
99
  /** declared output fields (transformers, DTOs) */
90
100
  outputFields: Field[];
91
101
  /** path walked through the call graph — what `fp:explain` prints */
@@ -12,6 +12,16 @@ export default defineConfig({
12
12
  infrastructure: ['access_tokens', 'reset_password_tokens', 'audits'],
13
13
  ignoreEntryPoints: ['drive.fs.serve', 'prometheus.metrics'],
14
14
  externallyMaintained: [],
15
+
16
+ /**
17
+ * Business data the AFP naming filter excluded by accident.
18
+ *
19
+ * It drops anything containing `session`, `template`, `error` or `types`,
20
+ * which is right for infrastructure and wrong for, say, a chat session the
21
+ * user manages. Check the confidence block of `fp:count` for what it
22
+ * excluded, and list here what is really business.
23
+ */
24
+ business: [],
15
25
  },
16
26
 
17
27
  /** Logical subgroups (RET). `constant` pins it at 1, which is honest. */
@@ -32,6 +42,22 @@ export default defineConfig({
32
42
  * here.
33
43
  */
34
44
  // overrides: {
35
- // 'POST /petitions': { det: 42, reason: 'JSON Schema form; 42 user fields' },
45
+ // 'POST /forms': { det: 42, reason: 'JSON Schema form; 42 user fields' },
46
+ // },
47
+
48
+ /**
49
+ * How change is priced. A clause of the contract, not a flag.
50
+ *
51
+ * AEP anchors added at 1 and deleted at 0.4. For a MODIFIED function it grades
52
+ * the factor from 0.25 to 1.75 through Effort Complexity variation, which needs
53
+ * cyclomatic complexity this package does not measure — so it defaults to 1,
54
+ * which overestimates, and every `fp:diff` says so.
55
+ *
56
+ * `implementation` means same type, same DET, same FTR, different body: a
57
+ * refactor. Whether that is billable, and at what, is a decision for whoever
58
+ * signs the contract — not one this package should invent.
59
+ */
60
+ // diff: {
61
+ // reasonFactors: { implementation: 0.25 },
36
62
  // },
37
63
  })
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.1.0",
4
+ "version": "0.2.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "build",
11
11
  "bin/cli.js",
12
+ "CHANGELOG.md",
12
13
  "!build/bin",
13
14
  "!build/tests"
14
15
  ],
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- declare const _default: import("tsdown").UserConfig;
2
- export default _default;