@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.
Files changed (80) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +427 -0
  3. package/bin/cli.js +4 -0
  4. package/build/commands/fp_calibrate.d.ts +16 -0
  5. package/build/commands/fp_count.d.ts +11 -0
  6. package/build/commands/fp_diff.d.ts +16 -0
  7. package/build/commands/fp_explain.d.ts +15 -0
  8. package/build/commands/fp_inventory.d.ts +9 -0
  9. package/build/commands/main.d.ts +5 -0
  10. package/build/commands/main.js +120 -0
  11. package/build/commands/printer.d.ts +9 -0
  12. package/build/configure.d.ts +2 -0
  13. package/build/configure.js +10 -0
  14. package/build/define_config-DOqWyPwV.js +19 -0
  15. package/build/index.d.ts +5 -0
  16. package/build/index.js +4 -0
  17. package/build/pipeline-BzP-ITGN.js +2306 -0
  18. package/build/resolvers-CU9HKYpn.js +555 -0
  19. package/build/runners-Bt8tbISi.js +630 -0
  20. package/build/scripts/smoke_package.d.ts +1 -0
  21. package/build/src/albrecht/calibration.d.ts +62 -0
  22. package/build/src/albrecht/counter.d.ts +48 -0
  23. package/build/src/albrecht/data_functions.d.ts +32 -0
  24. package/build/src/albrecht/diff.d.ts +66 -0
  25. package/build/src/albrecht/index.d.ts +13 -0
  26. package/build/src/albrecht/tables.d.ts +19 -0
  27. package/build/src/albrecht/technical_filter.d.ts +25 -0
  28. package/build/src/albrecht/transactional_functions.d.ts +35 -0
  29. package/build/src/cli/load_config.d.ts +28 -0
  30. package/build/src/cli/print.d.ts +19 -0
  31. package/build/src/cli/runners.d.ts +52 -0
  32. package/build/src/cli.d.ts +19 -0
  33. package/build/src/cli.js +198 -0
  34. package/build/src/define_config.d.ts +137 -0
  35. package/build/src/inventory/app_context.d.ts +73 -0
  36. package/build/src/inventory/detectors/lucid.d.ts +77 -0
  37. package/build/src/inventory/graph/call_graph.d.ts +80 -0
  38. package/build/src/inventory/graph/noise.d.ts +9 -0
  39. package/build/src/inventory/index.d.ts +15 -0
  40. package/build/src/inventory/paths.d.ts +22 -0
  41. package/build/src/inventory/resolvers/action_object.d.ts +14 -0
  42. package/build/src/inventory/resolvers/index.d.ts +23 -0
  43. package/build/src/inventory/resolvers/index.js +2 -0
  44. package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
  45. package/build/src/inventory/resolvers/module_function.d.ts +11 -0
  46. package/build/src/inventory/resolvers/property_service.d.ts +18 -0
  47. package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
  48. package/build/src/inventory/resolvers/static_service.d.ts +13 -0
  49. package/build/src/inventory/resolvers/transformer.d.ts +25 -0
  50. package/build/src/inventory/resolvers/types.d.ts +65 -0
  51. package/build/src/inventory/source.d.ts +39 -0
  52. package/build/src/inventory/sources/data_stores.d.ts +31 -0
  53. package/build/src/inventory/sources/json_schemas.d.ts +32 -0
  54. package/build/src/inventory/sources/routes_ast.d.ts +28 -0
  55. package/build/src/metrics/structure.d.ts +72 -0
  56. package/build/src/pipeline.d.ts +44 -0
  57. package/build/src/pipeline.js +2 -0
  58. package/build/src/reporters/table.d.ts +6 -0
  59. package/build/src/types.d.ts +256 -0
  60. package/build/src/types.js +1 -0
  61. package/build/stubs/config.stub +37 -0
  62. package/build/tmp/probe.d.ts +1 -0
  63. package/build/tmp/probe_cli.d.ts +1 -0
  64. package/build/tmp/probe_cmp.d.ts +1 -0
  65. package/build/tmp/probe_count.d.ts +1 -0
  66. package/build/tmp/probe_data.d.ts +1 -0
  67. package/build/tmp/probe_diff.d.ts +1 -0
  68. package/build/tmp/probe_gap.d.ts +1 -0
  69. package/build/tmp/probe_graph.d.ts +1 -0
  70. package/build/tmp/probe_metrics.d.ts +1 -0
  71. package/build/tmp/probe_miss.d.ts +1 -0
  72. package/build/tmp/probe_names.d.ts +1 -0
  73. package/build/tmp/probe_nodata.d.ts +1 -0
  74. package/build/tmp/probe_one.d.ts +1 -0
  75. package/build/tmp/probe_perf.d.ts +1 -0
  76. package/build/tmp/probe_routes.d.ts +1 -0
  77. package/build/tmp/probe_unres.d.ts +1 -0
  78. package/build/tmp/probe_vazquez.d.ts +1 -0
  79. package/build/tsdown.config.d.ts +2 -0
  80. 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>;
@@ -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 };