@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +101 -281
  3. package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
  4. package/build/commands/main.js +6 -6
  5. package/build/{fp_calibrate-DLZP5bUp.js → fp_calibrate-EAuAtdbq.js} +1 -1
  6. package/build/{fp_count-DNSwaLUD.js → fp_count-CZ0cUUBQ.js} +1 -1
  7. package/build/{fp_diff-CCKxqGKh.js → fp_diff-BTg_LX0r.js} +1 -1
  8. package/build/{fp_explain-Dpiby5Qx.js → fp_explain-D6QvDLKQ.js} +1 -1
  9. package/build/{fp_inventory-DHwZzEQf.js → fp_inventory-C43fU39x.js} +1 -1
  10. package/build/{fp_metrics-M84qLYaE.js → fp_metrics-DEMPk4xC.js} +1 -1
  11. package/build/index.d.ts +8 -4
  12. package/build/index.js +4 -4
  13. package/build/{pipeline-Dm9KvUvF.js → pipeline-Cq4dNTNE.js} +841 -264
  14. package/build/{resolvers-vMahHkAd.js → resolvers-DlKJOZnk.js} +373 -63
  15. package/build/{runners-DetZGfh5.js → runners-FYmPIPub.js} +12 -4
  16. package/build/src/albrecht/counter.d.ts +31 -5
  17. package/build/src/albrecht/data_functions.d.ts +49 -3
  18. package/build/src/albrecht/diff.d.ts +27 -0
  19. package/build/src/albrecht/index.d.ts +1 -0
  20. package/build/src/albrecht/opaque.d.ts +90 -0
  21. package/build/src/albrecht/technical_filter.d.ts +18 -11
  22. package/build/src/albrecht/transactional_functions.d.ts +7 -0
  23. package/build/src/cli.js +2 -2
  24. package/build/src/define_config.d.ts +55 -57
  25. package/build/src/inventory/graph/call_graph.d.ts +39 -0
  26. package/build/src/inventory/graph/output_fields.d.ts +99 -0
  27. package/build/src/inventory/paths.d.ts +3 -0
  28. package/build/src/inventory/resolvers/index.d.ts +28 -0
  29. package/build/src/inventory/resolvers/index.js +2 -2
  30. package/build/src/inventory/resolvers/types.d.ts +19 -0
  31. package/build/src/pipeline.js +1 -1
  32. package/build/src/types.d.ts +32 -1
  33. package/build/stubs/config.stub +29 -16
  34. 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;
@@ -8,6 +8,7 @@
8
8
  * Normative reference: OMG Automated Function Points 1.0 / ISO/IEC 19515.
9
9
  */
10
10
  export * from './tables.js';
11
+ export * from './technical_filter.js';
11
12
  export * from './counter.js';
12
13
  export * from './diff.js';
13
14
  export * from './calibration.js';
@@ -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
- * The standard treats these as user-provided inputs, so they stay overridable
16
- * through the boundary configuration.
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
- label: string;
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-Dm9KvUvF.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-DetZGfh5.js";
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
- * Strategy for RET, the logical subgroups of an ILF/EIF.
76
+ * How tables fold into data functions — counting-decisions §10.
65
77
  *
66
- * `constant` pins it at 1, which is honest: what a user recognises as a
67
- * subgroup is not derivable from code. `composition` derives it from
68
- * composition relations; less accurate in general, but captures real
69
- * aggregates.
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
- retStrategy: 'constant' | 'composition';
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
- * Declared DET or RET/FTR for a function the analysis cannot read, keyed by
130
- * the name it has in the count (`Invoice`, `POST /books`).
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
- * The case this exists for is a schema-driven application: when the fields a
133
- * user fills live in a JSON column whose schema is stored in the database,
134
- * there is nothing for static analysis to read and the column counts as 1 DET
135
- * (counting-decisions §8). The person who knows the form knows the number.
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. A number the tool guessed would be neither.
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 { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-vMahHkAd.js";
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
  }
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-Dm9KvUvF.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-Cq4dNTNE.js";
2
2
  export { CoverageTooLowError, analyze };