@filipebraida/adonis-function-points 0.3.0 → 0.5.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.
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { Node, Project, SyntaxKind } from "ts-morph";
2
3
  //#region src/inventory/paths.ts
3
4
  /**
@@ -20,8 +21,61 @@ import { Node, Project, SyntaxKind } from "ts-morph";
20
21
  * it to each comparison costs vigilance forever.
21
22
  */
22
23
  const toPosix = (value) => value.split("\\").join("/");
24
+ /**
25
+ * A path as it should appear in an EMITTED artefact: relative to the application.
26
+ *
27
+ * `CountSource.app` is documented as never being the absolute path, because that
28
+ * says where the machine keeps its files and travels with every count sent
29
+ * anywhere. One field below it, `config` shipped the absolute path — and so did
30
+ * every `trace[].file`, 858 times in a single production count. The rule was
31
+ * stated and then applied to one field.
32
+ *
33
+ * Internally the absolute path is the right thing: it is what ts-morph resolves
34
+ * and what the call graph keys its caches on. So this converts at the boundary
35
+ * where a path LEAVES, and nowhere else.
36
+ *
37
+ * A path outside the root keeps its `../` prefix, which describes where it is
38
+ * without naming the home directory.
39
+ */
40
+ const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
23
41
  /** Compares two paths that may have come from different sources. */
24
42
  const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
43
+ /**
44
+ * Is this file the application's own code, as opposed to the scaffolding around it?
45
+ *
46
+ * The top-level filter on `scanRoots` already drops `tests/`, `database/` and the
47
+ * rest — but only at the ROOT. Applications organised by domain module put both
48
+ * inside `app/`:
49
+ *
50
+ * app/billing/tests/functional/invoice.spec.ts
51
+ * app/billing/seeders/plan_seeder.ts
52
+ *
53
+ * so they land in the project, and `writtenAnywhere()` read a seeder's inserts as
54
+ * the application maintaining the table. A reference table only the seed populates
55
+ * came out as an ILF — which the CPM does not allow: data maintained by the
56
+ * development team is at most an EIF, and code data is not counted at all.
57
+ *
58
+ * The segments are AdonisJS's own: `make:test` writes to a suite directory,
59
+ * `make:seeder` to `seeders`, `make:migration` to `migrations`, `make:factory` to
60
+ * `factories`. The `.spec`/`.test` suffixes come from the suite globs in
61
+ * `adonisrc.ts`.
62
+ */
63
+ const SCAFFOLDING = new Set([
64
+ "tests",
65
+ "test",
66
+ "seeders",
67
+ "seeder",
68
+ "migrations",
69
+ "factories"
70
+ ]);
71
+ function isApplicationCode(root, file) {
72
+ const relative = relativeTo(root, file);
73
+ if (relative.startsWith("..")) return false;
74
+ const parts = relative.split("/");
75
+ const name = parts.at(-1) ?? "";
76
+ if (/\.(spec|test)\.[jt]s$/.test(name)) return false;
77
+ return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
78
+ }
25
79
  //#endregion
26
80
  //#region src/inventory/sources/event_bindings.ts
27
81
  /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
@@ -799,6 +853,15 @@ const BUILTIN_CALL_RESOLVERS = [
799
853
  * them apart. Hence specific strategies declare a lower `order` than generic
800
854
  * ones, and `module-function` comes last — it would match almost anything.
801
855
  */
856
+ /**
857
+ * Does any strategy call this a technical write?
858
+ *
859
+ * Asked separately from resolution, because the strategy that recognises the call as
860
+ * incidental is not necessarily the one that knows where it goes.
861
+ */
862
+ function isTechnicalWrite(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
863
+ return resolvers.some((resolver) => resolver.technicalWrite?.(call, ctx) === true);
864
+ }
802
865
  function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
803
866
  for (const resolver of resolvers) {
804
867
  /**
@@ -819,4 +882,4 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
819
882
  return null;
820
883
  }
821
884
  //#endregion
822
- export { rootSymbolOf as a, toPosix as c, hooksFiredBy as i, resolveCall as n, collectEventBindings as o, detectAccess as r, samePath as s, BUILTIN_CALL_RESOLVERS as t };
885
+ export { hooksFiredBy as a, isApplicationCode as c, toPosix as d, detectAccess as i, relativeTo as l, isTechnicalWrite as n, rootSymbolOf as o, resolveCall as r, collectEventBindings as s, BUILTIN_CALL_RESOLVERS as t, samePath as u };
@@ -1,6 +1,6 @@
1
1
  import { c as diffCounts, i as measureStructure, l as DEFAULTS, n as parseSamples, o as IncomparableRulesetsError, r as measureConformance, s as IncomparableSourcesError, t as calibrate, u as defineConfig } from "./calibration-8eV8CEix.js";
2
- import { c as toPosix } from "./resolvers-CRB6lXoo.js";
3
- import { n as analyze } from "./pipeline-CIAydCcT.js";
2
+ import { d as toPosix } from "./resolvers-PJwo2Z8R.js";
3
+ import { n as analyze } from "./pipeline-CNTBhs6o.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -91,7 +91,12 @@ function renderCount(result) {
91
91
  * habit: if it grows, the count comes from a spreadsheet and the tool loses
92
92
  * its reason to exist. Printing the share is what keeps that visible.
93
93
  */
94
- const overridden = result.functions.filter((fn) => fn.rationale.overrides?.length);
94
+ /**
95
+ * Only entries that DECLARED a number. A review records a decision and declares
96
+ * nothing, so counting it here would read as "35% of the total declared by
97
+ * override" about a count nobody touched.
98
+ */
99
+ const overridden = result.functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.length > 0));
95
100
  if (overridden.length > 0) {
96
101
  const points = overridden.reduce((total, fn) => total + fn.points, 0);
97
102
  const share = (points / (result.totals.unadjusted || 1) * 100).toFixed(1);
@@ -359,7 +364,17 @@ async function runMetrics(options) {
359
364
  async function runExplain(options) {
360
365
  const { config, notes } = await configFor(options.root);
361
366
  const { count } = await analyze(options.root, config);
362
- const matched = count.functions.filter((fn) => fn.name.toLowerCase().includes(options.name.toLowerCase()));
367
+ /**
368
+ * An exact name wins outright; the substring search is the fallback.
369
+ *
370
+ * `fp:explain "POST /orders/:param/submit"` returned four functions, because
371
+ * `/submit`, `/submit-ready` and `/submit-ready/return` all contain it. Asking
372
+ * about a function by its exact name and being handed its neighbours makes the
373
+ * command useless for the thing it exists for — defending one number.
374
+ */
375
+ const wanted = options.name.toLowerCase();
376
+ const exact = count.functions.filter((fn) => fn.name.toLowerCase() === wanted);
377
+ const matched = exact.length > 0 ? exact : count.functions.filter((fn) => fn.name.toLowerCase().includes(wanted));
363
378
  if (matched.length === 0) return {
364
379
  output: "",
365
380
  notes,
@@ -34,7 +34,7 @@ export declare const RULESET = "afp";
34
34
  * against this one and bills the tool's own improvement as work done. The guard
35
35
  * exists for exactly that, and only this constant arms it.
36
36
  */
37
- export declare const RULESET_VERSION = "1.2.0";
37
+ export declare const RULESET_VERSION = "1.4.0";
38
38
  export type CountInput = {
39
39
  app: AppContext;
40
40
  stores: CollectedDataStore[];
@@ -31,5 +31,14 @@ export type TransactionOptions = {
31
31
  messageDet: number;
32
32
  tables: Record<FunctionType, ComplexityTable>;
33
33
  weights: Record<FunctionType, Record<Complexity, number>>;
34
+ /**
35
+ * Application root, used only to relativise the paths that LEAVE in the trace.
36
+ *
37
+ * `CountSource.app` is documented as never being the absolute path, because it
38
+ * says where the machine keeps its files and travels with every count sent
39
+ * anywhere. The trace shipped the absolute path regardless — 858 times in a
40
+ * single production count, which is most of the artefact a ledger would store.
41
+ */
42
+ root: string;
34
43
  };
35
44
  export declare function countTransactionalFunctions(entryPoints: CollectedEntryPoint[], behaviors: Map<string, Behavior>, options: TransactionOptions): CountedFunction[];
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as CoverageTooLowError } from "../pipeline-CIAydCcT.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-CmxNHuuq.js";
1
+ import { t as CoverageTooLowError } from "../pipeline-CNTBhs6o.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-DIt1G85i.js";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -161,9 +161,34 @@ export type FunctionOverride = {
161
161
  *
162
162
  * A name that matches no schema is a warning, never a silent fallback.
163
163
  */
164
- detFromSchema?: string;
164
+ /**
165
+ * Name of a declared schema, or several whose fields are UNIONED.
166
+ *
167
+ * An ILF's DETs are the fields the user recognises in the file, and an
168
+ * application with one schema per template recognises the fields of all of them.
169
+ * Pointing at the largest and justifying it in `reason` gives the same answer
170
+ * only while they land in the same complexity band — which is a piece of
171
+ * reasoning the configuration should not have to carry.
172
+ *
173
+ * Unioned by leaf path, so a field two templates share counts once.
174
+ */
175
+ detFromSchema?: string | string[];
165
176
  /** declared RET (data function) or FTR (transaction) */
166
177
  refs?: number;
178
+ /**
179
+ * Opaque DETs someone has looked at and decided are correct at 1.
180
+ *
181
+ * `fp:count` reports every opaque column and open input object, because 1 DET is
182
+ * a floor rather than a measurement. But some of them ARE one field — a copy, a
183
+ * checksum, a bag of metadata — and there was no way to say so, so the warning
184
+ * fired on every run forever. A warning that cannot be answered is a warning the
185
+ * team learns to scroll past, which costs more than the one it reports.
186
+ *
187
+ * It silences nothing else: the count does not move, and `fp:count` still says
188
+ * how many were reviewed. Names are matched bare (`schema`) or qualified
189
+ * (`Petition.schema`).
190
+ */
191
+ opaqueReviewed?: string[];
167
192
  /** why — required, and printed by `fp:explain` beside the number */
168
193
  reason: string;
169
194
  };
@@ -30,6 +30,16 @@ export type Behavior = {
30
30
  writes: boolean;
31
31
  /** data stores reached */
32
32
  touches: string[];
33
+ /**
34
+ * Of those, the ones this transaction WRITES.
35
+ *
36
+ * `writes` is a property of the transaction — it decides EI against EO — and was
37
+ * being read as a property of every store the transaction touched: a table merely
38
+ * read by a route that writes something else counted as maintained, so almost
39
+ * nothing could be an EIF. §6.5.4 asks who maintains THIS store, which is a
40
+ * question about the access, not about the request.
41
+ */
42
+ writtenStores: string[];
33
43
  /**
34
44
  * Declared input fields: `request.validateUsing(x)` resolved down to the
35
45
  * fields of the VineJS schema — counting-decisions §7.
@@ -18,5 +18,23 @@
18
18
  * it to each comparison costs vigilance forever.
19
19
  */
20
20
  export declare const toPosix: (value: string) => string;
21
+ /**
22
+ * A path as it should appear in an EMITTED artefact: relative to the application.
23
+ *
24
+ * `CountSource.app` is documented as never being the absolute path, because that
25
+ * says where the machine keeps its files and travels with every count sent
26
+ * anywhere. One field below it, `config` shipped the absolute path — and so did
27
+ * every `trace[].file`, 858 times in a single production count. The rule was
28
+ * stated and then applied to one field.
29
+ *
30
+ * Internally the absolute path is the right thing: it is what ts-morph resolves
31
+ * and what the call graph keys its caches on. So this converts at the boundary
32
+ * where a path LEAVES, and nowhere else.
33
+ *
34
+ * A path outside the root keeps its `../` prefix, which describes where it is
35
+ * without naming the home directory.
36
+ */
37
+ export declare const relativeTo: (root: string, value: string) => string;
21
38
  /** Compares two paths that may have come from different sources. */
22
39
  export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
40
+ export declare function isApplicationCode(root: string, file: string): boolean;
@@ -17,6 +17,13 @@ export declare const BUILTIN_CALL_RESOLVERS: CallResolver[];
17
17
  * them apart. Hence specific strategies declare a lower `order` than generic
18
18
  * ones, and `module-function` comes last — it would match almost anything.
19
19
  */
20
+ /**
21
+ * Does any strategy call this a technical write?
22
+ *
23
+ * Asked separately from resolution, because the strategy that recognises the call as
24
+ * incidental is not necessarily the one that knows where it goes.
25
+ */
26
+ export declare function isTechnicalWrite(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): boolean;
20
27
  export declare function resolveCall(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): {
21
28
  by: string;
22
29
  refs: import('../../types.js').HandlerRef[];
@@ -1,2 +1,2 @@
1
- import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-CRB6lXoo.js";
2
- export { BUILTIN_CALL_RESOLVERS, resolveCall };
1
+ import { n as isTechnicalWrite, r as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-PJwo2Z8R.js";
2
+ export { BUILTIN_CALL_RESOLVERS, isTechnicalWrite, resolveCall };
@@ -97,4 +97,23 @@ export interface CallResolver {
97
97
  * defect this package can have, whoever writes it.
98
98
  */
99
99
  ignores?(call: CallExpression, ctx: ResolverContext): boolean;
100
+ /**
101
+ * "This call writes, and the write is not what the transaction is FOR."
102
+ *
103
+ * AFP §6.5.3 decides EI against EO mechanically: a transaction that modifies a data
104
+ * store is an EI. That is deliberate — repeatability over CPM fidelity — and it
105
+ * misreads one shape: a screen that records a visit, a last-seen organisation, a
106
+ * view counter. The CPM asks what the elementary process is PRIMARILY for, and for a
107
+ * `GET` that shows a record while noting the visit, the answer is presentation.
108
+ *
109
+ * So the fact is declared about the CALL, not about each transaction that reaches it:
110
+ * `persistOrganizationVisit` is called from several screens and saying it once covers
111
+ * all of them.
112
+ *
113
+ * It does NOT hide the write. The store is still maintained by this application —
114
+ * still an ILF, still an FTR of the transaction — and only the transaction's
115
+ * classification changes. A resolver that wanted the write to disappear would use
116
+ * `ignores`, and would be wrong to.
117
+ */
118
+ technicalWrite?(call: CallExpression, ctx: ResolverContext): boolean;
100
119
  }
@@ -33,7 +33,14 @@ export type CountSource = {
33
33
  */
34
34
  dirty?: boolean;
35
35
  countedAt: string;
36
- /** configuration file that shaped the count, or null for the defaults */
36
+ /**
37
+ * Configuration file that shaped the count, RELATIVE to the application root,
38
+ * or null for the defaults.
39
+ *
40
+ * Relative for the same reason `app` is a name: an absolute path says where the
41
+ * machine keeps its files, and this artefact is what goes into a ledger and to
42
+ * whoever receives the invoice.
43
+ */
37
44
  config: string | null;
38
45
  };
39
46
  export declare function describeSource(root: string, config: string | null): CountSource;
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-CIAydCcT.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-CNTBhs6o.js";
2
2
  export { CoverageTooLowError, analyze };
@@ -84,6 +84,8 @@ export type HandlerBehavior = {
84
84
  writes: boolean;
85
85
  /** DataStores reached (ids) */
86
86
  touches: string[];
87
+ /** of those, the ones this transaction writes — §6.5.4 is per store, not per request */
88
+ writtenStores: string[];
87
89
  /** declared input fields (validators) */
88
90
  inputFields: Field[];
89
91
  /**
@@ -42,7 +42,13 @@ export default defineConfig({
42
42
  * here.
43
43
  */
44
44
  // overrides: {
45
- // 'POST /forms': { det: 42, reason: 'JSON Schema form; 42 user fields' },
45
+ // // one schema, or several whose fields are unioned by leaf path
46
+ // Form: { detFromSchema: ['intakeSchema', 'reviewSchema'], reason: 'one per template' },
47
+ //
48
+ // // 1 DET is a floor, and `fp:count` says so on every run. When 1 IS the right
49
+ // // answer, record that someone checked — otherwise the warning becomes noise
50
+ // // the team learns to scroll past. It moves no number.
51
+ // Petition: { opaqueReviewed: ['schema', 'uiSchema'], reason: 'metadata; one field each' },
46
52
  // },
47
53
 
48
54
  /**
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.3.0",
4
+ "version": "0.5.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },