@filipebraida/adonis-function-points 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +427 -0
- package/bin/cli.js +4 -0
- package/build/commands/fp_calibrate.d.ts +16 -0
- package/build/commands/fp_count.d.ts +11 -0
- package/build/commands/fp_diff.d.ts +16 -0
- package/build/commands/fp_explain.d.ts +15 -0
- package/build/commands/fp_inventory.d.ts +9 -0
- package/build/commands/main.d.ts +5 -0
- package/build/commands/main.js +120 -0
- package/build/commands/printer.d.ts +9 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +10 -0
- package/build/define_config-DOqWyPwV.js +19 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +4 -0
- package/build/pipeline-BzP-ITGN.js +2306 -0
- package/build/resolvers-CU9HKYpn.js +555 -0
- package/build/runners-Bt8tbISi.js +630 -0
- package/build/scripts/smoke_package.d.ts +1 -0
- package/build/src/albrecht/calibration.d.ts +62 -0
- package/build/src/albrecht/counter.d.ts +48 -0
- package/build/src/albrecht/data_functions.d.ts +32 -0
- package/build/src/albrecht/diff.d.ts +66 -0
- package/build/src/albrecht/index.d.ts +13 -0
- package/build/src/albrecht/tables.d.ts +19 -0
- package/build/src/albrecht/technical_filter.d.ts +25 -0
- package/build/src/albrecht/transactional_functions.d.ts +35 -0
- package/build/src/cli/load_config.d.ts +28 -0
- package/build/src/cli/print.d.ts +19 -0
- package/build/src/cli/runners.d.ts +52 -0
- package/build/src/cli.d.ts +19 -0
- package/build/src/cli.js +198 -0
- package/build/src/define_config.d.ts +137 -0
- package/build/src/inventory/app_context.d.ts +73 -0
- package/build/src/inventory/detectors/lucid.d.ts +77 -0
- package/build/src/inventory/graph/call_graph.d.ts +80 -0
- package/build/src/inventory/graph/noise.d.ts +9 -0
- package/build/src/inventory/index.d.ts +15 -0
- package/build/src/inventory/paths.d.ts +22 -0
- package/build/src/inventory/resolvers/action_object.d.ts +14 -0
- package/build/src/inventory/resolvers/index.d.ts +23 -0
- package/build/src/inventory/resolvers/index.js +2 -0
- package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
- package/build/src/inventory/resolvers/module_function.d.ts +11 -0
- package/build/src/inventory/resolvers/property_service.d.ts +18 -0
- package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
- package/build/src/inventory/resolvers/static_service.d.ts +13 -0
- package/build/src/inventory/resolvers/transformer.d.ts +25 -0
- package/build/src/inventory/resolvers/types.d.ts +65 -0
- package/build/src/inventory/source.d.ts +39 -0
- package/build/src/inventory/sources/data_stores.d.ts +31 -0
- package/build/src/inventory/sources/json_schemas.d.ts +32 -0
- package/build/src/inventory/sources/routes_ast.d.ts +28 -0
- package/build/src/metrics/structure.d.ts +72 -0
- package/build/src/pipeline.d.ts +44 -0
- package/build/src/pipeline.js +2 -0
- package/build/src/reporters/table.d.ts +6 -0
- package/build/src/types.d.ts +256 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config.stub +37 -0
- package/build/tmp/probe.d.ts +1 -0
- package/build/tmp/probe_cli.d.ts +1 -0
- package/build/tmp/probe_cmp.d.ts +1 -0
- package/build/tmp/probe_count.d.ts +1 -0
- package/build/tmp/probe_data.d.ts +1 -0
- package/build/tmp/probe_diff.d.ts +1 -0
- package/build/tmp/probe_gap.d.ts +1 -0
- package/build/tmp/probe_graph.d.ts +1 -0
- package/build/tmp/probe_metrics.d.ts +1 -0
- package/build/tmp/probe_miss.d.ts +1 -0
- package/build/tmp/probe_names.d.ts +1 -0
- package/build/tmp/probe_nodata.d.ts +1 -0
- package/build/tmp/probe_one.d.ts +1 -0
- package/build/tmp/probe_perf.d.ts +1 -0
- package/build/tmp/probe_routes.d.ts +1 -0
- package/build/tmp/probe_unres.d.ts +1 -0
- package/build/tmp/probe_vazquez.d.ts +1 -0
- package/build/tsdown.config.d.ts +2 -0
- package/package.json +133 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AppContext } from '../inventory/app_context.js';
|
|
2
|
+
import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
3
|
+
import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
4
|
+
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
5
|
+
import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
|
|
6
|
+
import type { Complexity, CountResult, FunctionType } from '../types.js';
|
|
7
|
+
import type { FunctionOverride } from '../define_config.js';
|
|
8
|
+
import type { ComplexityTable } from './tables.js';
|
|
9
|
+
/**
|
|
10
|
+
* Assembles the count from the inventory.
|
|
11
|
+
*
|
|
12
|
+
* The order is not arbitrary: data functions depend on HOW transactions use
|
|
13
|
+
* each store (AFP §6.5.4), and transactional functions depend on which stores
|
|
14
|
+
* ended up counted. Hence: usage first, then the technical filter, then data,
|
|
15
|
+
* then transactions.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RULESET = "afp";
|
|
18
|
+
/**
|
|
19
|
+
* Version of the rule set.
|
|
20
|
+
*
|
|
21
|
+
* It appears in every report, and `fp:diff` refuses to compare counts produced
|
|
22
|
+
* by different versions — otherwise the difference would measure the rule
|
|
23
|
+
* change rather than the work.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RULESET_VERSION = "1.0.0";
|
|
26
|
+
export type CountInput = {
|
|
27
|
+
app: AppContext;
|
|
28
|
+
stores: CollectedDataStore[];
|
|
29
|
+
entryPoints: CollectedEntryPoint[];
|
|
30
|
+
/** behaviour keyed by `EntryPoint.id`; absent means no handler */
|
|
31
|
+
behaviors: Map<string, Behavior>;
|
|
32
|
+
/** JSON Schema literals found in the code, for `detFromSchema` — §8 */
|
|
33
|
+
jsonSchemas?: Map<string, DiscoveredSchema>;
|
|
34
|
+
};
|
|
35
|
+
export type CountOptions = {
|
|
36
|
+
retStrategy?: 'constant' | 'composition';
|
|
37
|
+
/** declared DET/RET for what static analysis cannot read — see §8 */
|
|
38
|
+
overrides?: Record<string, FunctionOverride>;
|
|
39
|
+
boundary?: {
|
|
40
|
+
infrastructure?: string[];
|
|
41
|
+
externallyMaintained?: string[];
|
|
42
|
+
ignoreEntryPoints?: string[];
|
|
43
|
+
};
|
|
44
|
+
messageDet?: number;
|
|
45
|
+
complexityTables?: Partial<Record<FunctionType, ComplexityTable>>;
|
|
46
|
+
weights?: Partial<Record<FunctionType, Record<Complexity, number>>>;
|
|
47
|
+
};
|
|
48
|
+
export declare function count(input: CountInput, options?: CountOptions): CountResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
2
|
+
import type { Complexity, CountedFunction, FunctionType } from '../types.js';
|
|
3
|
+
import type { ComplexityTable } from './tables.js';
|
|
4
|
+
/**
|
|
5
|
+
* Data functions: ILF and EIF.
|
|
6
|
+
*
|
|
7
|
+
* The classification does not come from the model's own code — it comes from
|
|
8
|
+
* HOW the application's transactions use the store:
|
|
9
|
+
*
|
|
10
|
+
* "If the Data Function is maintained by any of the application's
|
|
11
|
+
* Transactional Functions, the Data Function shall be determined to be an
|
|
12
|
+
* ILF. […] If a Data Function is not used in any of the processing of an
|
|
13
|
+
* application's Transactional Functions, the Data Function shall not be
|
|
14
|
+
* counted in the application." — AFP §6.5.4
|
|
15
|
+
*
|
|
16
|
+
* That is why this module takes usage, not just the stores.
|
|
17
|
+
*/
|
|
18
|
+
export type StoreUsage = {
|
|
19
|
+
/** does any transaction of the application write to this store? */
|
|
20
|
+
written: boolean;
|
|
21
|
+
/** does any transaction reach it at all, reading or writing? */
|
|
22
|
+
used: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type DataFunctionOptions = {
|
|
25
|
+
/** `constant` pins RET at 1; `composition` derives it from composition relations */
|
|
26
|
+
retStrategy: 'constant' | 'composition';
|
|
27
|
+
/** stores maintained by another system, by boundary decision */
|
|
28
|
+
externallyMaintained: Set<string>;
|
|
29
|
+
tables: Record<FunctionType, ComplexityTable>;
|
|
30
|
+
weights: Record<FunctionType, Record<Complexity, number>>;
|
|
31
|
+
};
|
|
32
|
+
export declare function countDataFunctions(stores: CollectedDataStore[], usage: Map<string, StoreUsage>, options: DataFunctionOptions): CountedFunction[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ChangeType, CountResult, DiffResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Added, changed and removed functions between two counts — what becomes an
|
|
4
|
+
* invoice.
|
|
5
|
+
*
|
|
6
|
+
* Normative base: **OMG Automated Enhancement Points 1.0**, the sibling of AFP,
|
|
7
|
+
* written to size maintenance between two revisions.
|
|
8
|
+
*
|
|
9
|
+
* "Each Artifact shall be analyzed in both revisions to determine whether it
|
|
10
|
+
* is: Added — when it exists in revision ToRevision while it didn't exist in
|
|
11
|
+
* FromRevision. […] Modified — when it exists in both revisions but whose
|
|
12
|
+
* source code changed." — AEP §6.3
|
|
13
|
+
*
|
|
14
|
+
* Two decisions make this workable:
|
|
15
|
+
*
|
|
16
|
+
* 1. **It operates on two saved counts**, never on two checkouts. Booting the
|
|
17
|
+
* older revision, with possibly different dependencies, is the kind of
|
|
18
|
+
* problem not worth solving.
|
|
19
|
+
* 2. **It refuses to compare different rule sets.** If the rules changed in
|
|
20
|
+
* between, the difference measures the rule change, not the work — and the
|
|
21
|
+
* result would go into an invoice.
|
|
22
|
+
*/
|
|
23
|
+
export declare class IncomparableRulesetsError extends Error {
|
|
24
|
+
constructor(from: string, to: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Factors per change type.
|
|
28
|
+
*
|
|
29
|
+
* The `added` and `removed` defaults are the explicit anchors of AEP §6.5: an
|
|
30
|
+
* added transaction is worth 1, a deleted one 0.4.
|
|
31
|
+
*
|
|
32
|
+
* `changed` defaults to 1 and **that overestimates**. AEP grades it from 0.25
|
|
33
|
+
* to 1.75 through Table 6.1, derived from Effort Complexity variation, which
|
|
34
|
+
* requires cyclomatic complexity that this package does not yet measure.
|
|
35
|
+
* Counting 1 is conservative in the sense of not inventing a number, not in the
|
|
36
|
+
* sense of billing less — and the result says so.
|
|
37
|
+
*/
|
|
38
|
+
export type ChangeFactors = Record<ChangeType, number>;
|
|
39
|
+
export declare const AEP_FACTORS: ChangeFactors;
|
|
40
|
+
export type DiffOptions = {
|
|
41
|
+
factors?: Partial<ChangeFactors>;
|
|
42
|
+
/** labels for the two measurements, for the report only */
|
|
43
|
+
labels?: {
|
|
44
|
+
from: string;
|
|
45
|
+
to: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export type FunctionPointDiff = DiffResult & {
|
|
49
|
+
/** function points weighted by the factors — this is what gets billed */
|
|
50
|
+
billable: number;
|
|
51
|
+
factors: ChangeFactors;
|
|
52
|
+
warnings: string[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Two counts of DIFFERENT applications compare cleanly and mean nothing.
|
|
56
|
+
*
|
|
57
|
+
* The ruleset guard already refuses counts produced by different rules. This
|
|
58
|
+
* refuses counts produced over different subjects, which is the same class of
|
|
59
|
+
* error and the easier one to make in CI, where both files arrive as paths.
|
|
60
|
+
*/
|
|
61
|
+
export declare class IncomparableSourcesError extends Error {
|
|
62
|
+
readonly from: string;
|
|
63
|
+
readonly to: string;
|
|
64
|
+
constructor(from: string, to: string);
|
|
65
|
+
}
|
|
66
|
+
export declare function diffCounts(from: CountResult, to: CountResult, options?: DiffOptions): FunctionPointDiff;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counting engine — IFPUG / OMG-AFP lineage.
|
|
3
|
+
*
|
|
4
|
+
* "Albrecht" names the FAMILY of rules, not just the person: measurement
|
|
5
|
+
* literature says "Albrecht function points" to separate this lineage from
|
|
6
|
+
* COSMIC, which counts data movements and yields incompatible numbers.
|
|
7
|
+
*
|
|
8
|
+
* Normative reference: OMG Automated Function Points 1.0 / ISO/IEC 19515.
|
|
9
|
+
*/
|
|
10
|
+
export * from './tables.js';
|
|
11
|
+
export * from './counter.js';
|
|
12
|
+
export * from './diff.js';
|
|
13
|
+
export * from './calibration.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Complexity, FunctionType } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* IFPUG CPM complexity tables.
|
|
4
|
+
*
|
|
5
|
+
* Configurable on purpose. A single DET of difference — typically the
|
|
6
|
+
* confirmation message, which no static analyser can see — is enough to cross
|
|
7
|
+
* the low/average band and change a function's value. Calibrating the bands
|
|
8
|
+
* against manual counts is more honest than pretending the bias is absent.
|
|
9
|
+
*/
|
|
10
|
+
export type ComplexityTable = {
|
|
11
|
+
/** upper bounds of the RET/FTR bands: [a, b] => <=a | <=b | rest */
|
|
12
|
+
refBands: [number, number];
|
|
13
|
+
/** upper bounds of the DET bands */
|
|
14
|
+
detBands: [number, number];
|
|
15
|
+
};
|
|
16
|
+
export declare const DEFAULT_TABLES: Record<FunctionType, ComplexityTable>;
|
|
17
|
+
export declare const DEFAULT_WEIGHTS: Record<FunctionType, Record<Complexity, number>>;
|
|
18
|
+
export declare function complexityOf(type: FunctionType, refs: number, det: number, tables?: Record<FunctionType, ComplexityTable>): Complexity;
|
|
19
|
+
export declare function pointsOf(type: FunctionType, complexity: Complexity, weights?: Record<FunctionType, Record<Complexity, number>>): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
2
|
+
/**
|
|
3
|
+
* Temporary and technical data filter — AFP §6.5.2.1.1.
|
|
4
|
+
*
|
|
5
|
+
* "Database tables identified as temporary or technical shall be marked as
|
|
6
|
+
* such to be presented in the final report, and shall be ignored in the rest
|
|
7
|
+
* of this process."
|
|
8
|
+
*
|
|
9
|
+
* Returns the reason when a table is technical, `null` otherwise: the report
|
|
10
|
+
* must say WHY something was excluded, not merely that it was.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Naming conventions, with the defaults given by the spec itself (§6.5.2.1.3).
|
|
14
|
+
*
|
|
15
|
+
* The standard treats these as user-provided inputs, so they stay overridable
|
|
16
|
+
* through the boundary configuration.
|
|
17
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
2
|
+
import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
3
|
+
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
4
|
+
import type { Complexity, CountedFunction, FunctionType } from '../types.js';
|
|
5
|
+
import type { ComplexityTable } from './tables.js';
|
|
6
|
+
/**
|
|
7
|
+
* Transactional functions: EI and EO.
|
|
8
|
+
*
|
|
9
|
+
* "Transactions that modify data entities content shall be considered
|
|
10
|
+
* External Inputs (EI). […] Transactions that do not modify data entities
|
|
11
|
+
* content but only use them shall be considered as External Output."
|
|
12
|
+
* — AFP §6.5.3
|
|
13
|
+
*
|
|
14
|
+
* There is no EQ here, and that is the standard's decision rather than a
|
|
15
|
+
* simplification of ours:
|
|
16
|
+
*
|
|
17
|
+
* "Since the primary intent cannot be assessed by an automated function point
|
|
18
|
+
* counting tool, all outputs and inquiries shall be counted as external
|
|
19
|
+
* outputs (EO)." — AFP §6.5.3
|
|
20
|
+
*/
|
|
21
|
+
export type TransactionOptions = {
|
|
22
|
+
/** stores that are counted; anything else contributes no FTR */
|
|
23
|
+
countedStores: Map<string, CollectedDataStore>;
|
|
24
|
+
/**
|
|
25
|
+
* Extra DET for the confirmation or error message.
|
|
26
|
+
*
|
|
27
|
+
* The IFPUG manual counts one; AFP does not. The default follows AFP, and it
|
|
28
|
+
* stays configurable because this is a known systematic divergence of −1 DET
|
|
29
|
+
* per transaction against manual counts.
|
|
30
|
+
*/
|
|
31
|
+
messageDet: number;
|
|
32
|
+
tables: Record<FunctionType, ComplexityTable>;
|
|
33
|
+
weights: Record<FunctionType, Record<Complexity, number>>;
|
|
34
|
+
};
|
|
35
|
+
export declare function countTransactionalFunctions(entryPoints: CollectedEntryPoint[], behaviors: Map<string, Behavior>, options: TransactionOptions): CountedFunction[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FunctionPointsConfig } from '../define_config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads `config/function_points.ts` from the application being analysed.
|
|
4
|
+
*
|
|
5
|
+
* Both front-ends need this and neither could do it alone: the ace commands run
|
|
6
|
+
* with `startApp: false`, so there is no booted container to read config from;
|
|
7
|
+
* and the standalone CLI has no container at all. The file is therefore
|
|
8
|
+
* imported directly, through jiti, which transforms the whole module graph —
|
|
9
|
+
* a config may import a resolver from the application, and that resolver may
|
|
10
|
+
* import files using decorators, which Node's type stripping cannot handle.
|
|
11
|
+
*
|
|
12
|
+
* **A config that exists and fails to load is an error, never a fallback.**
|
|
13
|
+
* Falling back to defaults with a warning would silently change the count, and
|
|
14
|
+
* the count becomes an invoice. Absence of a config file is a different thing,
|
|
15
|
+
* and is legitimate: it means the defaults.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CONFIG_PATHS: string[];
|
|
18
|
+
export declare class ConfigLoadError extends Error {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly cause: unknown;
|
|
21
|
+
constructor(file: string, cause: unknown);
|
|
22
|
+
}
|
|
23
|
+
export type LoadedConfig = {
|
|
24
|
+
config: FunctionPointsConfig;
|
|
25
|
+
/** absolute path of the file used, or null when the defaults apply */
|
|
26
|
+
file: string | null;
|
|
27
|
+
};
|
|
28
|
+
export declare function loadConfig(root: string): Promise<LoadedConfig>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RunResult } from './runners.js';
|
|
2
|
+
/**
|
|
3
|
+
* One place that decides how a `RunResult` reaches a terminal.
|
|
4
|
+
*
|
|
5
|
+
* Shared so the two front-ends cannot drift in what they show — including the
|
|
6
|
+
* failure path: a command that found nothing must exit non-zero in CI exactly
|
|
7
|
+
* as it does under ace.
|
|
8
|
+
*/
|
|
9
|
+
export type Printer = {
|
|
10
|
+
log: (message: string) => void;
|
|
11
|
+
error: (message: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Diagnostics about the run itself — which config was used, calibration
|
|
14
|
+
* warnings. A separate channel because they must NOT land on stdout: `--json`
|
|
15
|
+
* exists to be piped, and a note printed there makes the output unparseable.
|
|
16
|
+
*/
|
|
17
|
+
note: (message: string) => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function printResult(result: RunResult, printer: Printer): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each command actually does, with no front-end attached.
|
|
3
|
+
*
|
|
4
|
+
* There are two front-ends — the ace commands, for a project that installed the
|
|
5
|
+
* package, and the standalone binary, for CI — and a rule that keeps them
|
|
6
|
+
* honest: **they must not be able to disagree**. Everything that decides a
|
|
7
|
+
* number lives here; the front-ends only parse arguments and print.
|
|
8
|
+
*
|
|
9
|
+
* Loading `config/function_points.ts` is part of that. It used to happen in
|
|
10
|
+
* neither front-end, so every option but `--min-coverage` was silently ignored
|
|
11
|
+
* by the command a user actually runs, while the tests proved the options
|
|
12
|
+
* worked by calling `analyze()` directly.
|
|
13
|
+
*/
|
|
14
|
+
export type RunResult = {
|
|
15
|
+
/** what to print on stdout */
|
|
16
|
+
output: string;
|
|
17
|
+
/** lines to report as errors; a non-empty list means failure */
|
|
18
|
+
errors?: string[];
|
|
19
|
+
/** notes about the run itself, printed before the output */
|
|
20
|
+
notes?: string[];
|
|
21
|
+
};
|
|
22
|
+
type Common = {
|
|
23
|
+
root: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function runInventory(options: Common & {
|
|
26
|
+
out?: string;
|
|
27
|
+
}): Promise<RunResult>;
|
|
28
|
+
export declare function runCount(options: Common & {
|
|
29
|
+
out?: string;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
minCoverage?: number;
|
|
32
|
+
}): Promise<RunResult>;
|
|
33
|
+
export declare function runExplain(options: Common & {
|
|
34
|
+
name: string;
|
|
35
|
+
}): Promise<RunResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Compares a saved count against the current tree, or against a second saved
|
|
38
|
+
* count.
|
|
39
|
+
*
|
|
40
|
+
* The two-file form is the shape CI has: a pipeline counts the base revision
|
|
41
|
+
* and the head revision, and neither of them is "the current working tree" by
|
|
42
|
+
* the time they are compared. Since the analyser needs nothing installed in the
|
|
43
|
+
* application, counting an older revision is a `git worktree` away.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runDiff(options: Common & {
|
|
46
|
+
previous: string;
|
|
47
|
+
current?: string;
|
|
48
|
+
}): Promise<RunResult>;
|
|
49
|
+
export declare function runCalibrate(options: Common & {
|
|
50
|
+
samples: string;
|
|
51
|
+
}): Promise<RunResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's own version, for `--version`.
|
|
3
|
+
*
|
|
4
|
+
* Walks up from this module until it finds a package.json, so it works both
|
|
5
|
+
* from source (`src/cli.ts`) and from the build (`build/src/cli.js`), which sit
|
|
6
|
+
* at different depths.
|
|
7
|
+
*/
|
|
8
|
+
export declare function packageVersion(): string;
|
|
9
|
+
/** minimal parser: a flag is `--name value` or `--name=value`, plus booleans */
|
|
10
|
+
export declare function parseArgv(argv: string[]): {
|
|
11
|
+
command: string;
|
|
12
|
+
positional: string[];
|
|
13
|
+
flags: Map<string, string | true>;
|
|
14
|
+
};
|
|
15
|
+
import type { Printer } from './cli/print.js';
|
|
16
|
+
export declare function run(argv: string[], printer?: Printer): Promise<number>;
|
|
17
|
+
export declare class UsageError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export declare function main(argv?: string[]): Promise<number>;
|
package/build/src/cli.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { t as CoverageTooLowError } from "../pipeline-BzP-ITGN.js";
|
|
2
|
+
import { a as runInventory, i as runExplain, n as runCount, o as ConfigLoadError, r as runDiff, s as printResult, t as runCalibrate } from "../runners-Bt8tbISi.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/cli.ts
|
|
7
|
+
/**
|
|
8
|
+
* Standalone entry point, for CI and for one-off runs with `npx`.
|
|
9
|
+
*
|
|
10
|
+
* It does NOT replace installing the package: a project that installs it keeps
|
|
11
|
+
* the `node ace fp:*` commands. Both front-ends call the same runners, so they
|
|
12
|
+
* cannot disagree about a number.
|
|
13
|
+
*
|
|
14
|
+
* Nothing is booted here either — the engine only ever reads files — which is
|
|
15
|
+
* what makes a run possible with no `.env`, no database and no install in the
|
|
16
|
+
* analysed project.
|
|
17
|
+
*/
|
|
18
|
+
const USAGE = `adonis-function-points — automated function point counting for AdonisJS
|
|
19
|
+
|
|
20
|
+
Usage
|
|
21
|
+
adonis-function-points <command> [options]
|
|
22
|
+
|
|
23
|
+
Commands
|
|
24
|
+
count count the unadjusted function points
|
|
25
|
+
inventory the raw facts: stores, routes, tracing coverage
|
|
26
|
+
explain <name> why one function was counted that way
|
|
27
|
+
diff <a.json> [b.json] additions / modifications / deletions, and billable FP
|
|
28
|
+
one file compares against the current tree; two
|
|
29
|
+
compare the files, which is the shape CI has
|
|
30
|
+
calibrate <samples.csv> correction factors against a manual count
|
|
31
|
+
|
|
32
|
+
Options
|
|
33
|
+
--root <path> application to analyse (default: the current directory)
|
|
34
|
+
--out <path> write the result as JSON to this path
|
|
35
|
+
--json print JSON instead of a table
|
|
36
|
+
--min-coverage <0..1> fail below this tracing coverage
|
|
37
|
+
-h, --help this message
|
|
38
|
+
-v, --version package version
|
|
39
|
+
`;
|
|
40
|
+
/**
|
|
41
|
+
* The package's own version, for `--version`.
|
|
42
|
+
*
|
|
43
|
+
* Walks up from this module until it finds a package.json, so it works both
|
|
44
|
+
* from source (`src/cli.ts`) and from the build (`build/src/cli.js`), which sit
|
|
45
|
+
* at different depths.
|
|
46
|
+
*/
|
|
47
|
+
function packageVersion() {
|
|
48
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
50
|
+
const candidate = path.join(dir, "package.json");
|
|
51
|
+
if (existsSync(candidate)) {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
53
|
+
if (parsed.version) return parsed.version;
|
|
54
|
+
}
|
|
55
|
+
const parent = path.dirname(dir);
|
|
56
|
+
if (parent === dir) break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
/** minimal parser: a flag is `--name value` or `--name=value`, plus booleans */
|
|
62
|
+
function parseArgv(argv) {
|
|
63
|
+
const positional = [];
|
|
64
|
+
const flags = /* @__PURE__ */ new Map();
|
|
65
|
+
for (let index = 0; index < argv.length; index++) {
|
|
66
|
+
const token = argv[index];
|
|
67
|
+
if (!token.startsWith("-")) {
|
|
68
|
+
positional.push(token);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const [key, inline] = token.replace(/^--?/, "").split("=");
|
|
72
|
+
if (inline !== void 0) {
|
|
73
|
+
flags.set(key, inline);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const next = argv[index + 1];
|
|
77
|
+
if (next && !next.startsWith("-")) {
|
|
78
|
+
flags.set(key, next);
|
|
79
|
+
index++;
|
|
80
|
+
} else flags.set(key, true);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
command: positional[0],
|
|
84
|
+
positional: positional.slice(1),
|
|
85
|
+
flags
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const numeric = (value) => typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : void 0;
|
|
89
|
+
const text = (value) => typeof value === "string" ? value : void 0;
|
|
90
|
+
const CONSOLE = {
|
|
91
|
+
log: (message) => process.stdout.write(`${message}\n`),
|
|
92
|
+
error: (message) => process.stderr.write(`${message}\n`),
|
|
93
|
+
note: (message) => process.stderr.write(`${message}\n`)
|
|
94
|
+
};
|
|
95
|
+
async function run(argv, printer = CONSOLE) {
|
|
96
|
+
const { command, positional, flags } = parseArgv(argv);
|
|
97
|
+
if (flags.has("version") || flags.has("v")) {
|
|
98
|
+
printer.log(packageVersion());
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (!command || flags.has("help") || flags.has("h")) {
|
|
102
|
+
printer.log(USAGE.trimEnd());
|
|
103
|
+
return command ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
if (![
|
|
106
|
+
"count",
|
|
107
|
+
"inventory",
|
|
108
|
+
"explain",
|
|
109
|
+
"diff",
|
|
110
|
+
"calibrate"
|
|
111
|
+
].includes(command)) {
|
|
112
|
+
printer.error(`unknown command "${command}"`);
|
|
113
|
+
printer.log(USAGE.trimEnd());
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
const root = path.resolve(text(flags.get("root")) ?? process.cwd());
|
|
117
|
+
/**
|
|
118
|
+
* Pointing at the wrong directory is the likeliest mistake in CI, and its
|
|
119
|
+
* symptom is a confident zero: a monorepo root has a package.json, so that
|
|
120
|
+
* check passed and the count came back 0 FP with 100% coverage and exit 0.
|
|
121
|
+
*
|
|
122
|
+
* `adonisrc.ts` is what actually marks an AdonisJS application root.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* `diff a.json b.json` analyses nothing: both sides are already counted. It
|
|
126
|
+
* runs in a pipeline step that may not even sit inside the application — the
|
|
127
|
+
* CI shape this exists for — so requiring an application root there would
|
|
128
|
+
* refuse the one case it was added for.
|
|
129
|
+
*/
|
|
130
|
+
const analysesTheTree = !(command === "diff" && positional.length >= 2);
|
|
131
|
+
const marker = ["adonisrc.ts", "adonisrc.js"].find((name) => existsSync(path.join(root, name)));
|
|
132
|
+
if (analysesTheTree && !marker) {
|
|
133
|
+
printer.error(`no adonisrc.ts in ${root}: this is not an AdonisJS application root.\nIn a monorepo, point --root at the application itself (apps/<name>).`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const need = (what, value) => {
|
|
137
|
+
if (!value) throw new UsageError(`${command} needs ${what}`);
|
|
138
|
+
return value;
|
|
139
|
+
};
|
|
140
|
+
let result;
|
|
141
|
+
switch (command) {
|
|
142
|
+
case "count":
|
|
143
|
+
result = await runCount({
|
|
144
|
+
root,
|
|
145
|
+
out: text(flags.get("out")),
|
|
146
|
+
json: flags.get("json") === true,
|
|
147
|
+
minCoverage: numeric(flags.get("min-coverage"))
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
case "inventory":
|
|
151
|
+
result = await runInventory({
|
|
152
|
+
root,
|
|
153
|
+
out: text(flags.get("out"))
|
|
154
|
+
});
|
|
155
|
+
break;
|
|
156
|
+
case "explain":
|
|
157
|
+
result = await runExplain({
|
|
158
|
+
root,
|
|
159
|
+
name: need("a function name", positional[0])
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
case "diff":
|
|
163
|
+
result = await runDiff({
|
|
164
|
+
root,
|
|
165
|
+
previous: need("a saved count", positional[0]),
|
|
166
|
+
current: positional[1]
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
case "calibrate":
|
|
170
|
+
result = await runCalibrate({
|
|
171
|
+
root,
|
|
172
|
+
samples: need("a samples CSV", positional[0])
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
/* c8 ignore next 2 -- unreachable: KNOWN is checked above */
|
|
176
|
+
default: return 1;
|
|
177
|
+
}
|
|
178
|
+
return printResult(result, printer);
|
|
179
|
+
}
|
|
180
|
+
var UsageError = class extends Error {};
|
|
181
|
+
async function main(argv = process.argv.slice(2)) {
|
|
182
|
+
try {
|
|
183
|
+
return await run(argv, CONSOLE);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
/**
|
|
186
|
+
* These three are not crashes — they are the package refusing to produce a
|
|
187
|
+
* number it cannot stand behind. They deserve their message, not a stack.
|
|
188
|
+
*/
|
|
189
|
+
if (error instanceof UsageError || error instanceof ConfigLoadError || error instanceof CoverageTooLowError) {
|
|
190
|
+
process.stderr.write(`${error.message}\n`);
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : error}\n`);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { UsageError, main, packageVersion, parseArgv, run };
|