@filipebraida/adonis-function-points 0.4.0 → 0.6.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 +155 -0
- package/README.md +101 -281
- package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DLZP5bUp.js → fp_calibrate-EAuAtdbq.js} +1 -1
- package/build/{fp_count-DNSwaLUD.js → fp_count-CZ0cUUBQ.js} +1 -1
- package/build/{fp_diff-CCKxqGKh.js → fp_diff-BTg_LX0r.js} +1 -1
- package/build/{fp_explain-Dpiby5Qx.js → fp_explain-D6QvDLKQ.js} +1 -1
- package/build/{fp_inventory-DHwZzEQf.js → fp_inventory-C43fU39x.js} +1 -1
- package/build/{fp_metrics-M84qLYaE.js → fp_metrics-DEMPk4xC.js} +1 -1
- package/build/index.d.ts +8 -4
- package/build/index.js +4 -4
- package/build/{pipeline-Dm9KvUvF.js → pipeline-Cq4dNTNE.js} +841 -264
- package/build/{resolvers-vMahHkAd.js → resolvers-DlKJOZnk.js} +373 -63
- package/build/{runners-DetZGfh5.js → runners-FYmPIPub.js} +12 -4
- package/build/src/albrecht/counter.d.ts +31 -5
- package/build/src/albrecht/data_functions.d.ts +49 -3
- package/build/src/albrecht/diff.d.ts +27 -0
- package/build/src/albrecht/index.d.ts +1 -0
- package/build/src/albrecht/opaque.d.ts +90 -0
- package/build/src/albrecht/technical_filter.d.ts +18 -11
- package/build/src/albrecht/transactional_functions.d.ts +7 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +55 -57
- package/build/src/inventory/graph/call_graph.d.ts +39 -0
- package/build/src/inventory/graph/output_fields.d.ts +99 -0
- package/build/src/inventory/paths.d.ts +3 -0
- package/build/src/inventory/resolvers/index.d.ts +28 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/types.d.ts +19 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +32 -1
- package/build/stubs/config.stub +29 -16
- package/package.json +1 -1
|
@@ -37,6 +37,29 @@ export declare class IncomparableRulesetsError extends Error {
|
|
|
37
37
|
*/
|
|
38
38
|
export type ChangeFactors = Record<ChangeType, number>;
|
|
39
39
|
export declare const AEP_FACTORS: ChangeFactors;
|
|
40
|
+
/**
|
|
41
|
+
* The Roteiro de Métricas de Software do SISP, v3.0 (Portaria SGD/MGI nº 3656,
|
|
42
|
+
* de 2026), §7.3 "Projeto de Melhoria" — what a Brazilian public contract names
|
|
43
|
+
* instead of AEP:
|
|
44
|
+
*
|
|
45
|
+
* PF_MELHORIA = PF_INCLUÍDO + FI × PF_ALTERADO + 0,50 × PF_EXCLUÍDO + PF_CONVERSÃO
|
|
46
|
+
*
|
|
47
|
+
* where FI, the impact factor on an altered function, is 63% when the contractor
|
|
48
|
+
* developed or already maintains the function, and 84% when it did not (and must
|
|
49
|
+
* document it). This preset carries the 63% — a factory billing maintenance of
|
|
50
|
+
* its own work — and `diff.factors: { changed: 0.84 }` is the other case.
|
|
51
|
+
* PF_CONVERSÃO is data conversion, which this package does not count.
|
|
52
|
+
*
|
|
53
|
+
* Read from the guide's own PDF, not from memory: an earlier draft of this
|
|
54
|
+
* preset said 0,50 / 0,30, and v2.0 (2012) priced exclusion at 0,40. A contract
|
|
55
|
+
* binds to a revision, so the report prints which preset priced the total.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SISP_FACTORS: ChangeFactors;
|
|
58
|
+
export type FactorPreset = 'aep' | 'sisp';
|
|
59
|
+
export declare const FACTOR_PRESETS: Record<FactorPreset, {
|
|
60
|
+
label: string;
|
|
61
|
+
factors: ChangeFactors;
|
|
62
|
+
}>;
|
|
40
63
|
/**
|
|
41
64
|
* Factors for a modified function, by WHAT changed about it.
|
|
42
65
|
*
|
|
@@ -52,6 +75,8 @@ export declare const AEP_FACTORS: ChangeFactors;
|
|
|
52
75
|
*/
|
|
53
76
|
export type ChangeReasonFactors = Partial<Record<ChangeReason, number>>;
|
|
54
77
|
export type DiffOptions = {
|
|
78
|
+
/** which published set of factors to start from; `factors` overrides it field by field */
|
|
79
|
+
preset?: FactorPreset;
|
|
55
80
|
factors?: Partial<ChangeFactors>;
|
|
56
81
|
/** per-reason factors for modified functions; each falls back to `factors.changed` */
|
|
57
82
|
reasonFactors?: ChangeReasonFactors;
|
|
@@ -64,6 +89,8 @@ export type DiffOptions = {
|
|
|
64
89
|
export type FunctionPointDiff = DiffResult & {
|
|
65
90
|
/** function points weighted by the factors — this is what gets billed */
|
|
66
91
|
billable: number;
|
|
92
|
+
/** the preset the factors started from — printed, because the total is quoted under it */
|
|
93
|
+
preset: FactorPreset;
|
|
67
94
|
factors: ChangeFactors;
|
|
68
95
|
/** what the modified functions were actually billed at, by reason */
|
|
69
96
|
reasonFactors: ChangeReasonFactors;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
2
|
+
import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
|
|
3
|
+
import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
4
|
+
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
5
|
+
import type { Complexity, CountedFunction, FunctionType } from '../types.js';
|
|
6
|
+
import type { ComplexityTable } from './tables.js';
|
|
7
|
+
/**
|
|
8
|
+
* DETs the analysis cannot read, and what a person declared about them —
|
|
9
|
+
* counting-decisions §8 and §9.
|
|
10
|
+
*
|
|
11
|
+
* Three shapes are opaque: a JSON column (`ast:surveys.answers`), an open
|
|
12
|
+
* input object (`validator:answerSurveyValidator.answers`), and a spread a
|
|
13
|
+
* transformer emits (`transformer:X.<this.resource.serialize()>`). Each counts
|
|
14
|
+
* 1 DET — a floor, never a zero — and is marked `(opaque)` in the rationale.
|
|
15
|
+
*
|
|
16
|
+
* A declaration is about the ORIGIN of the placeholder, not about a function,
|
|
17
|
+
* and it applies to every function that carries the DET: the data function and
|
|
18
|
+
* each transaction that takes or shows the column. Keyed by function it had to
|
|
19
|
+
* be written twice and still missed the third place, so the same column was
|
|
20
|
+
* worth two numbers in one count — and matching by bare name meant reviewing
|
|
21
|
+
* `Attachment.metadata` reviewed every `metadata` column of every table.
|
|
22
|
+
*/
|
|
23
|
+
export declare const OPAQUE_TYPE: RegExp;
|
|
24
|
+
export declare const isOpaqueType: (type?: string) => boolean;
|
|
25
|
+
export type OpaqueDeclaration = {
|
|
26
|
+
/**
|
|
27
|
+
* Name(s) of a JSON Schema declared in the application's code, whose fields
|
|
28
|
+
* are counted by the §7 leaf rules and replace the single DET the placeholder
|
|
29
|
+
* contributed. Several are unioned by leaf path: a field two templates share
|
|
30
|
+
* counts once.
|
|
31
|
+
*
|
|
32
|
+
* Prefer this to `overrides.<fn>.det`. A declared number freezes; naming the
|
|
33
|
+
* schema keeps the number coming from the code, and the only thing maintained
|
|
34
|
+
* by hand is the mapping — which changes when a form is born, not when a field
|
|
35
|
+
* is. A name that matches no schema is a warning, never a silent fallback.
|
|
36
|
+
*/
|
|
37
|
+
schemas?: string | string[];
|
|
38
|
+
/**
|
|
39
|
+
* Someone looked, and 1 is the right answer — a copy, a checksum, a bag of
|
|
40
|
+
* metadata. Moves no number; stops the warning for this origin; is printed by
|
|
41
|
+
* `fp:explain` with its reason, and is not counted in the "declared by override"
|
|
42
|
+
* share, because nothing was declared.
|
|
43
|
+
*/
|
|
44
|
+
reviewed?: true;
|
|
45
|
+
/** why — required, and printed beside the number */
|
|
46
|
+
reason: string;
|
|
47
|
+
};
|
|
48
|
+
export type ApplyOpaqueOptions = {
|
|
49
|
+
declarations: Record<string, OpaqueDeclaration>;
|
|
50
|
+
stores: CollectedDataStore[];
|
|
51
|
+
schemas: Map<string, DiscoveredSchema>;
|
|
52
|
+
tables: Record<FunctionType, ComplexityTable>;
|
|
53
|
+
weights: Record<FunctionType, Record<Complexity, number>>;
|
|
54
|
+
};
|
|
55
|
+
export type AppliedOpaque = {
|
|
56
|
+
functions: CountedFunction[];
|
|
57
|
+
/** origin -> how it was answered */
|
|
58
|
+
answered: Map<string, 'replaced' | 'reviewed'>;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Applies the declarations to every function carrying the origin they name.
|
|
63
|
+
*
|
|
64
|
+
* schemas the placeholder's 1 DET becomes the schema's leaves, and the line
|
|
65
|
+
* says which schema stood in
|
|
66
|
+
* reviewed the placeholder stays 1, marked reviewed; the warning stops
|
|
67
|
+
*
|
|
68
|
+
* A declaration keyed by the physical table (`surveys.answers`) is accepted
|
|
69
|
+
* as well as one keyed by the model (`Survey.answers`): the count prints
|
|
70
|
+
* the model, `fp:explain` prints the table, and a person copies from either.
|
|
71
|
+
*/
|
|
72
|
+
export declare function applyOpaque(functions: CountedFunction[], options: ApplyOpaqueOptions): AppliedOpaque;
|
|
73
|
+
export type OpaqueReportInput = {
|
|
74
|
+
functions: CountedFunction[];
|
|
75
|
+
stores: CollectedDataStore[];
|
|
76
|
+
entryPoints: CollectedEntryPoint[];
|
|
77
|
+
behaviors: Map<string, Behavior>;
|
|
78
|
+
answered: Map<string, 'replaced' | 'reviewed'>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The floors still standing, by origin — the one blind spot this package used
|
|
82
|
+
* to keep to itself.
|
|
83
|
+
*
|
|
84
|
+
* Grouped by origin because that is what a declaration answers: one line for
|
|
85
|
+
* `Form.definition` however many transactions show it. A transformer's spread has
|
|
86
|
+
* its own warning and is not repeated here. Only what is unanswered is a request
|
|
87
|
+
* to do something; what was answered is counted at the end so the fact is
|
|
88
|
+
* recorded rather than erased.
|
|
89
|
+
*/
|
|
90
|
+
export declare function opaqueWarnings(input: OpaqueReportInput): string[];
|
|
@@ -9,17 +9,24 @@ import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
|
9
9
|
* Returns the reason when a table is technical, `null` otherwise: the report
|
|
10
10
|
* must say WHY something was excluded, not merely that it was.
|
|
11
11
|
*/
|
|
12
|
+
export type TechnicalPattern = {
|
|
13
|
+
/** printed in the report beside the exclusion */
|
|
14
|
+
label: string;
|
|
15
|
+
/** tested against the physical table name; a string is compiled case-insensitively */
|
|
16
|
+
pattern: RegExp | string;
|
|
17
|
+
};
|
|
12
18
|
/**
|
|
13
|
-
* Naming conventions, with the defaults given by the spec itself (§6.5.2.1.3)
|
|
19
|
+
* Naming conventions, with the defaults given by the spec itself (§6.5.2.1.3)
|
|
20
|
+
* plus the one AdonisJS asks for.
|
|
21
|
+
*
|
|
22
|
+
* The standard treats these as user-provided inputs, so `boundary.technicalPatterns`
|
|
23
|
+
* REPLACES this list when set — a team that finds `.+types?` catching its
|
|
24
|
+
* business data drops it there — and `boundary.business` restores one table.
|
|
14
25
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
26
|
+
* `token` is not in the spec's list and is here because the framework's own
|
|
27
|
+
* tables are: `auth_access_tokens`, `remember_me_tokens`, `password_reset_tokens`.
|
|
28
|
+
* A token is the machinery of authentication, not data the user maintains, and
|
|
29
|
+
* on two applications it came out as an ILF at 7 PF each.
|
|
17
30
|
*/
|
|
18
|
-
export declare const DEFAULT_TECHNICAL_PATTERNS:
|
|
19
|
-
|
|
20
|
-
pattern: RegExp;
|
|
21
|
-
}[];
|
|
22
|
-
export declare function isTechnical(store: CollectedDataStore, patterns?: {
|
|
23
|
-
label: string;
|
|
24
|
-
pattern: RegExp;
|
|
25
|
-
}[]): string | null;
|
|
31
|
+
export declare const DEFAULT_TECHNICAL_PATTERNS: TechnicalPattern[];
|
|
32
|
+
export declare function isTechnical(store: CollectedDataStore, patterns?: TechnicalPattern[]): string | null;
|
|
@@ -3,6 +3,7 @@ import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
|
3
3
|
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
4
4
|
import type { Complexity, CountedFunction, FunctionType } from '../types.js';
|
|
5
5
|
import type { ComplexityTable } from './tables.js';
|
|
6
|
+
import type { StoreGrouping } from './data_functions.js';
|
|
6
7
|
/**
|
|
7
8
|
* Transactional functions: EI and EO.
|
|
8
9
|
*
|
|
@@ -21,6 +22,12 @@ import type { ComplexityTable } from './tables.js';
|
|
|
21
22
|
export type TransactionOptions = {
|
|
22
23
|
/** stores that are counted; anything else contributes no FTR */
|
|
23
24
|
countedStores: Map<string, CollectedDataStore>;
|
|
25
|
+
/**
|
|
26
|
+
* How the stores fold into data functions — counting-decisions §10. A
|
|
27
|
+
* transaction touching a detail and its master touches ONE logical file: one
|
|
28
|
+
* FTR, and the detail's link to the master is not an output DET.
|
|
29
|
+
*/
|
|
30
|
+
grouping: StoreGrouping;
|
|
24
31
|
/**
|
|
25
32
|
* Extra DET for the confirmation or error message.
|
|
26
33
|
*
|
package/build/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CoverageTooLowError } from "../pipeline-
|
|
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-
|
|
1
|
+
import { t as CoverageTooLowError } from "../pipeline-Cq4dNTNE.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-FYmPIPub.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComplexityTable } from './albrecht/tables.js';
|
|
2
|
-
import type { ChangeFactors, ChangeReasonFactors } from './albrecht/diff.js';
|
|
2
|
+
import type { ChangeFactors, ChangeReasonFactors, FactorPreset } from './albrecht/diff.js';
|
|
3
|
+
import type { TechnicalPattern } from './albrecht/technical_filter.js';
|
|
4
|
+
import type { OpaqueDeclaration } from './albrecht/opaque.js';
|
|
3
5
|
import type { CallResolver } from './inventory/resolvers/types.js';
|
|
4
6
|
import type { Complexity, FunctionType } from './types.js';
|
|
5
7
|
/**
|
|
@@ -50,6 +52,16 @@ export type FunctionPointsConfig = {
|
|
|
50
52
|
* filter missed, this one restores what it caught by accident.
|
|
51
53
|
*/
|
|
52
54
|
business?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* The naming conventions of the technical-data filter (AFP §6.5.2.1.3),
|
|
57
|
+
* tested against the physical table name.
|
|
58
|
+
*
|
|
59
|
+
* When set, this list REPLACES the defaults — the spec treats the patterns
|
|
60
|
+
* as user input, and a team whose business tables end in `_types` needs to
|
|
61
|
+
* drop that one, not add to it. `DEFAULT_TECHNICAL_PATTERNS` is exported to
|
|
62
|
+
* start from. Every exclusion still appears in the report with its label.
|
|
63
|
+
*/
|
|
64
|
+
technicalPatterns?: TechnicalPattern[];
|
|
53
65
|
/**
|
|
54
66
|
* Entry points with no functional value to the user, by route name or by
|
|
55
67
|
* identity (`GET /health`).
|
|
@@ -61,14 +73,17 @@ export type FunctionPointsConfig = {
|
|
|
61
73
|
ignoreEntryPoints?: string[];
|
|
62
74
|
};
|
|
63
75
|
/**
|
|
64
|
-
*
|
|
76
|
+
* How tables fold into data functions — counting-decisions §10.
|
|
65
77
|
*
|
|
66
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
78
|
+
* `usage` (the default): a composition child (`hasMany` / `hasOne`) that no
|
|
79
|
+
* application code addresses directly is a RET of its parent, not an ILF of
|
|
80
|
+
* its own — the user only ever sees it inside the parent. `none` keeps every
|
|
81
|
+
* table its own data function at RET 1, which is what rule sets before 1.5.0
|
|
82
|
+
* did; it exists to compare against an old count, not as a preference.
|
|
70
83
|
*/
|
|
71
|
-
|
|
84
|
+
dataFunctions?: {
|
|
85
|
+
grouping?: 'usage' | 'none';
|
|
86
|
+
};
|
|
72
87
|
/**
|
|
73
88
|
* Maximum depth in the call graph, starting at the handler.
|
|
74
89
|
*
|
|
@@ -103,6 +118,16 @@ export type FunctionPointsConfig = {
|
|
|
103
118
|
* this package invented would be worse. So the number comes from the contract.
|
|
104
119
|
*/
|
|
105
120
|
diff?: {
|
|
121
|
+
/**
|
|
122
|
+
* Which published set of factors prices the change: `aep` (the default —
|
|
123
|
+
* added 1, changed 1, removed 0.4) or `sisp` (Roteiro de Métricas do SISP
|
|
124
|
+
* v3.0 §7.3 — inclusão 1,00, alteração × FI 0,63, exclusão 0,50; what a
|
|
125
|
+
* Brazilian public contract usually names). The FI is 0,84 when the
|
|
126
|
+
* contractor did not develop or maintain the function: set it with
|
|
127
|
+
* `factors: { changed: 0.84 }`. A contract binds to a revision of the guide
|
|
128
|
+
* — v2.0 priced exclusion at 0,40 — so check yours and override if it differs.
|
|
129
|
+
*/
|
|
130
|
+
preset?: FactorPreset;
|
|
106
131
|
factors?: Partial<ChangeFactors>;
|
|
107
132
|
reasonFactors?: ChangeReasonFactors;
|
|
108
133
|
};
|
|
@@ -126,71 +151,44 @@ export type FunctionPointsConfig = {
|
|
|
126
151
|
*/
|
|
127
152
|
minCoverage?: number;
|
|
128
153
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
154
|
+
* What a person declares about a DET the analysis cannot read, keyed by its
|
|
155
|
+
* ORIGIN — counting-decisions §8:
|
|
156
|
+
*
|
|
157
|
+
* 'Survey.answers' a JSON column (model or table name)
|
|
158
|
+
* 'answerSurveyValidator.answers' an open field of a validator
|
|
131
159
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
160
|
+
* A declaration applies to every function carrying that DET: the data
|
|
161
|
+
* function and each transaction that takes or shows the column. Keyed by
|
|
162
|
+
* function it had to be repeated, and still left the transactions nobody
|
|
163
|
+
* wrote it for at the floor — the same column worth two numbers in one count.
|
|
164
|
+
*
|
|
165
|
+
* `schemas` names the JSON Schema(s) in the code whose fields replace the
|
|
166
|
+
* floor; `reviewed` records that 1 is the right answer. Both require `reason`,
|
|
167
|
+
* and `fp:count` reports how much of the total came from a declaration so it
|
|
168
|
+
* cannot grow unnoticed.
|
|
169
|
+
*/
|
|
170
|
+
opaque?: Record<string, OpaqueDeclaration>;
|
|
171
|
+
/**
|
|
172
|
+
* A declared DET or RET for ONE function, keyed by the name it has in the
|
|
173
|
+
* count (`Invoice`, `POST /books`) — the last resort, for a fact that is not
|
|
174
|
+
* in the code at all (a schema that lives only in the database).
|
|
136
175
|
*
|
|
137
176
|
* `reason` is required, and that is the whole point. A declared number is
|
|
138
177
|
* reproducible — it lives in a versioned file, so the same revision yields
|
|
139
178
|
* the same count — and auditable, because `fp:explain` prints it with its
|
|
140
|
-
* justification.
|
|
141
|
-
*
|
|
142
|
-
* Use sparingly. If overriding becomes a habit the count stops coming from
|
|
143
|
-
* the code, and the report says how much of the total came from here so that
|
|
144
|
-
* cannot grow unnoticed.
|
|
179
|
+
* justification. But it freezes: prefer `opaque.<origin>.schemas` whenever the
|
|
180
|
+
* fields are declared anywhere in the code.
|
|
145
181
|
*/
|
|
146
182
|
overrides?: Record<string, FunctionOverride>;
|
|
147
183
|
};
|
|
148
184
|
export type FunctionOverride = {
|
|
149
185
|
/** declared DET count, replacing what the analysis found */
|
|
150
186
|
det?: number;
|
|
151
|
-
/**
|
|
152
|
-
* Name of a JSON Schema declared in the application's code, whose fields are
|
|
153
|
-
* counted by the §7 leaf rules and replace the single DET the opaque column
|
|
154
|
-
* contributed.
|
|
155
|
-
*
|
|
156
|
-
* Prefer this to `det`. A declared number freezes: someone adds a field, the
|
|
157
|
-
* count does not move, and `fp:diff` reports no change for real functional
|
|
158
|
-
* growth — undercounting silently and progressively. Naming the schema keeps
|
|
159
|
-
* the number coming from the code; the only thing maintained by hand is the
|
|
160
|
-
* mapping, which changes when a form is born rather than when a field is.
|
|
161
|
-
*
|
|
162
|
-
* A name that matches no schema is a warning, never a silent fallback.
|
|
163
|
-
*/
|
|
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[];
|
|
176
187
|
/** declared RET (data function) or FTR (transaction) */
|
|
177
188
|
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[];
|
|
192
189
|
/** why — required, and printed by `fp:explain` beside the number */
|
|
193
190
|
reason: string;
|
|
194
191
|
};
|
|
192
|
+
export type { OpaqueDeclaration };
|
|
195
193
|
export declare const DEFAULTS: FunctionPointsConfig;
|
|
196
194
|
export declare function defineConfig(config: Partial<FunctionPointsConfig>): FunctionPointsConfig;
|
|
@@ -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.
|
|
@@ -45,6 +55,33 @@ export type Behavior = {
|
|
|
45
55
|
requestFields: string[];
|
|
46
56
|
/** the transaction reads the request in a way that enumerates nothing */
|
|
47
57
|
opaqueRequest: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* What leaves the boundary, when a transformer on the path says so —
|
|
60
|
+
* counting-decisions §6. Qualified by the transformer: `LivroTransformer.titulo`.
|
|
61
|
+
*
|
|
62
|
+
* Empty means no transformer was reached, and the output DETs fall back to the
|
|
63
|
+
* columns of the stores read. It does NOT mean the transaction emits nothing.
|
|
64
|
+
*/
|
|
65
|
+
outputFields: string[];
|
|
66
|
+
/** of those, the spreads the walker could not read: 1 DET each, a floor, reported */
|
|
67
|
+
opaqueOutputFields: string[];
|
|
68
|
+
/** stores a transformer on the path is FOR: their columns are not output DETs, their keys are */
|
|
69
|
+
transformedStores: string[];
|
|
70
|
+
/**
|
|
71
|
+
* How each store was read, over every chain on the path: whether rows left
|
|
72
|
+
* whole, which columns a `.select()` named, and whether an aggregate
|
|
73
|
+
* (`.count()`, `.exists()`) left one scalar. What an output shows when no
|
|
74
|
+
* transformer covers the store.
|
|
75
|
+
*/
|
|
76
|
+
outputReads: Record<string, {
|
|
77
|
+
whole: boolean;
|
|
78
|
+
selected: string[];
|
|
79
|
+
aggregate: boolean;
|
|
80
|
+
/** read by a chain of its own, not only preloaded through another store */
|
|
81
|
+
direct: boolean;
|
|
82
|
+
/** stores it was preloaded through */
|
|
83
|
+
via: string[];
|
|
84
|
+
}>;
|
|
48
85
|
trace: TraceStep[];
|
|
49
86
|
/** bodies reached, for `fp:diff` */
|
|
50
87
|
scope: ScopeEntry[];
|
|
@@ -78,6 +115,8 @@ export type GraphOptions = {
|
|
|
78
115
|
export declare function createAnalyzer(app: AppContext, stores: CollectedDataStore[], options?: GraphOptions): {
|
|
79
116
|
analyze: (handler: HandlerRef) => Behavior;
|
|
80
117
|
writtenAnywhere: () => Set<string>;
|
|
118
|
+
addressedAnywhere: () => Set<string>;
|
|
119
|
+
seededAnywhere: () => Set<string>;
|
|
81
120
|
/** how many files the project loaded — used to prove it does not grow */
|
|
82
121
|
fileCount: () => number;
|
|
83
122
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Node } from 'ts-morph';
|
|
2
|
+
import type { CallExpression, ClassDeclaration } from 'ts-morph';
|
|
3
|
+
import type { CollectedDataStore } from '../sources/data_stores.js';
|
|
4
|
+
/**
|
|
5
|
+
* What an output transaction actually EMITS — counting-decisions §6.
|
|
6
|
+
*
|
|
7
|
+
* AFP §7.3 counts one DET per unique field that leaves the boundary. Without
|
|
8
|
+
* reading what leaves, the only repeatable answer is "every column of every
|
|
9
|
+
* table read", and that is what the count did: a detail screen passing through
|
|
10
|
+
* three transformers came out at 67 DET. The transformer is where the
|
|
11
|
+
* application says which fields cross, so it is read here.
|
|
12
|
+
*
|
|
13
|
+
* Two facts are extracted, and both are about the BODY, so they are cached with
|
|
14
|
+
* the rest of its facts:
|
|
15
|
+
*
|
|
16
|
+
* outputs the keys a transformer method returns — `LivroTransformer.titulo`
|
|
17
|
+
* selected the columns a query names in `.select()` — per store
|
|
18
|
+
*
|
|
19
|
+
* Everything the walker cannot read is a placeholder, never a guess: an
|
|
20
|
+
* unreadable spread counts 1 DET as a floor and is reported, the same treatment
|
|
21
|
+
* an open `vine.object` gets on the input side (§9).
|
|
22
|
+
*/
|
|
23
|
+
export type OutputFacts = {
|
|
24
|
+
/** qualified keys emitted by this transformer body */
|
|
25
|
+
outputs: string[];
|
|
26
|
+
/** of those, the placeholders: a spread the walker could not read */
|
|
27
|
+
opaqueOutputs: string[];
|
|
28
|
+
/**
|
|
29
|
+
* The store this transformer is FOR — `BaseTransformer<Livro>` — when it is a
|
|
30
|
+
* known store. A transformer decides what leaves for its resource, not for the
|
|
31
|
+
* page: a store read beside it and passed raw is not covered.
|
|
32
|
+
*/
|
|
33
|
+
resource: string | null;
|
|
34
|
+
};
|
|
35
|
+
/** how a body reads a store, per chain — what leaves when nothing transforms it */
|
|
36
|
+
export type StoreRead = {
|
|
37
|
+
store: string;
|
|
38
|
+
shape: 'whole' | 'select' | 'aggregate';
|
|
39
|
+
/** for `select`: the columns named */
|
|
40
|
+
columns: string[];
|
|
41
|
+
/**
|
|
42
|
+
* The store this one was preloaded THROUGH (`Livro.query().preload('autor')`),
|
|
43
|
+
* when it was not read by a chain of its own. A relation loaded for a
|
|
44
|
+
* transformer is consumed by it, not shown.
|
|
45
|
+
*/
|
|
46
|
+
via?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Does this class extend a transformer base from a package?
|
|
50
|
+
*
|
|
51
|
+
* Decided by the base's name AND by its import being a bare specifier, so an
|
|
52
|
+
* application class that merely happens to own a `transform` method is not
|
|
53
|
+
* mistaken for one. Shared with the `transformer` resolver: one definition of
|
|
54
|
+
* what a transformer is, or the resolver follows a body this walker refuses.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isTransformerClass(cls: ClassDeclaration): boolean;
|
|
57
|
+
/** `class X extends BaseTransformer<Livro>` -> 'Livro' */
|
|
58
|
+
export declare function transformerResourceOf(cls: ClassDeclaration): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The keys a transformer method returns.
|
|
61
|
+
*
|
|
62
|
+
* `followed` says whether a call inside the literal is a body the graph walks —
|
|
63
|
+
* `AutorTransformer.transform(x)`, `this.toObject()` — in which case its keys
|
|
64
|
+
* arrive through that body and the key holding it is not a DET of its own: the
|
|
65
|
+
* user sees the author's name, not an "autor" field.
|
|
66
|
+
*
|
|
67
|
+
* { titulo: l.titulo } 1 — `titulo`
|
|
68
|
+
* { autor: AutorTransformer.transform } 0 here; the nested body contributes
|
|
69
|
+
* { endereco: { rua, cidade } } leaves individually
|
|
70
|
+
* { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
|
|
71
|
+
* { itens: xs.map((i) => ({ a, b })) } the leaves, once
|
|
72
|
+
* ...this.pick(this.resource, [...]) the listed names
|
|
73
|
+
* ...this.toObject() 0 here; the followed body contributes
|
|
74
|
+
* ...anythingElse 1, opaque, reported
|
|
75
|
+
*
|
|
76
|
+
* A key that is the identifier of the transformer's resource is not a DET, for
|
|
77
|
+
* the same reason `isPrimary` is not one on the data function.
|
|
78
|
+
*/
|
|
79
|
+
export declare function outputFieldsIn(body: Node, owner: ClassDeclaration | undefined, stores: Map<string, CollectedDataStore>, followed: (call: CallExpression) => boolean): OutputFacts;
|
|
80
|
+
/**
|
|
81
|
+
* The shape of the chain an access belongs to — read from the WHOLE chain, root
|
|
82
|
+
* to end, because every call on it is detected as an access and each must reach
|
|
83
|
+
* the same answer: `Livro.query().where(…).count()` is an aggregate whether the
|
|
84
|
+
* detector is looking at `query` or at `count`.
|
|
85
|
+
*
|
|
86
|
+
* whole rows leave: every column of the store (unless a transformer covers it)
|
|
87
|
+
* select only the columns named
|
|
88
|
+
* aggregate `.count()`, `.exists()`: one derived scalar leaves, not the table
|
|
89
|
+
*/
|
|
90
|
+
export type ChainShape = {
|
|
91
|
+
selected: string[];
|
|
92
|
+
aggregate: boolean;
|
|
93
|
+
/** a `.select()` whose column list is not literal */
|
|
94
|
+
unreadable: {
|
|
95
|
+
line: number;
|
|
96
|
+
expression: string;
|
|
97
|
+
}[];
|
|
98
|
+
};
|
|
99
|
+
export declare function chainShapeOf(access: CallExpression): ChainShape;
|
|
@@ -37,3 +37,6 @@ export declare const toPosix: (value: string) => string;
|
|
|
37
37
|
export declare const relativeTo: (root: string, value: string) => string;
|
|
38
38
|
/** Compares two paths that may have come from different sources. */
|
|
39
39
|
export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
|
|
40
|
+
/** a seeder, by the directory `make:seeder` writes to — scaffolding, but a fact the report uses */
|
|
41
|
+
export declare function isSeeder(root: string, file: string): boolean;
|
|
42
|
+
export declare function isApplicationCode(root: string, file: string): boolean;
|
|
@@ -17,7 +17,35 @@ 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[];
|
|
23
30
|
} | null;
|
|
31
|
+
export type IgnoreCallsOptions = {
|
|
32
|
+
/** the strategy's name — printed in the report beside the volume it declared data-free */
|
|
33
|
+
name: string;
|
|
34
|
+
/** method names, matched on the callee: `getUrl` matches `x.getUrl(...)` */
|
|
35
|
+
methods?: string[];
|
|
36
|
+
/** a pattern over the callee's text: `/\bauthz\.can$/` */
|
|
37
|
+
matching?: RegExp;
|
|
38
|
+
/** lower runs first; defaults to 1, before every built-in */
|
|
39
|
+
order?: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A strategy that recognises a family of calls and knows they reach no data
|
|
43
|
+
* store — a rate limiter, an attachment's URL, an authorisation check.
|
|
44
|
+
*
|
|
45
|
+
* Read from a real configuration, every such strategy was the same eight lines:
|
|
46
|
+
* a helper to get the method name off the ts-morph node (the app does not depend
|
|
47
|
+
* on ts-morph), a `resolve` that returns nothing, and one comparison. What the
|
|
48
|
+
* design wants is kept — it is still a NAMED strategy, and `fp:count` still
|
|
49
|
+
* reports the volume it declared data-free — and the ceremony is not.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ignoreCalls(options: IgnoreCallsOptions): CallResolver;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { BUILTIN_CALL_RESOLVERS, resolveCall };
|
|
1
|
+
import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-DlKJOZnk.js";
|
|
2
|
+
export { BUILTIN_CALL_RESOLVERS, ignoreCalls, 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
|
}
|
package/build/src/pipeline.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as analyze, t as CoverageTooLowError } from "../pipeline-
|
|
1
|
+
import { n as analyze, t as CoverageTooLowError } from "../pipeline-Cq4dNTNE.js";
|
|
2
2
|
export { CoverageTooLowError, analyze };
|