lattice-graph 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 +21 -0
- package/README.md +391 -0
- package/package.json +56 -0
- package/src/commands/build.ts +208 -0
- package/src/commands/init.ts +111 -0
- package/src/commands/lint.ts +245 -0
- package/src/commands/populate.ts +224 -0
- package/src/commands/update.ts +175 -0
- package/src/config.ts +93 -0
- package/src/extract/extractor.ts +13 -0
- package/src/extract/parser.ts +117 -0
- package/src/extract/python/calls.ts +121 -0
- package/src/extract/python/extractor.ts +171 -0
- package/src/extract/python/frameworks.ts +142 -0
- package/src/extract/python/imports.ts +115 -0
- package/src/extract/python/symbols.ts +121 -0
- package/src/extract/tags.ts +77 -0
- package/src/extract/typescript/calls.ts +110 -0
- package/src/extract/typescript/extractor.ts +130 -0
- package/src/extract/typescript/imports.ts +71 -0
- package/src/extract/typescript/symbols.ts +252 -0
- package/src/graph/database.ts +95 -0
- package/src/graph/queries.ts +336 -0
- package/src/graph/writer.ts +147 -0
- package/src/main.ts +525 -0
- package/src/output/json.ts +79 -0
- package/src/output/text.ts +265 -0
- package/src/types/config.ts +32 -0
- package/src/types/graph.ts +87 -0
- package/src/types/lint.ts +21 -0
- package/src/types/result.ts +58 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { BoundaryEntry, EventConnection, FlowEntry } from "../graph/queries.ts";
|
|
2
|
+
import type { Node } from "../types/graph.ts";
|
|
3
|
+
|
|
4
|
+
/** Formats a node reference as "name (file:line)". */
|
|
5
|
+
function nodeRef(node: Node): string {
|
|
6
|
+
return `${node.name} (${node.file}:${node.lineStart})`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Tree node for flow visualization. */
|
|
10
|
+
type FlowTreeNode = {
|
|
11
|
+
readonly node: Node;
|
|
12
|
+
readonly boundary: string | undefined;
|
|
13
|
+
readonly emits: string | undefined;
|
|
14
|
+
readonly children: readonly FlowTreeNode[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Context data for the `lattice context` command. */
|
|
18
|
+
type ContextData = {
|
|
19
|
+
readonly node: Node;
|
|
20
|
+
readonly flows: readonly string[];
|
|
21
|
+
readonly callers: readonly Node[];
|
|
22
|
+
readonly callees: readonly Node[];
|
|
23
|
+
readonly boundary: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Impact data for the `lattice impact` command. */
|
|
27
|
+
type ImpactData = {
|
|
28
|
+
readonly directCallers: readonly Node[];
|
|
29
|
+
readonly transitiveCallers: readonly Node[];
|
|
30
|
+
readonly affectedFlows: readonly string[];
|
|
31
|
+
readonly affectedTests: readonly Node[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Formats the overview output showing all flows, boundaries, and events.
|
|
36
|
+
*
|
|
37
|
+
* @param flows - All flow entry points
|
|
38
|
+
* @param boundaries - All boundary-tagged nodes
|
|
39
|
+
* @param events - All event connections
|
|
40
|
+
* @returns Compact text output
|
|
41
|
+
*/
|
|
42
|
+
function formatOverview(
|
|
43
|
+
flows: readonly FlowEntry[],
|
|
44
|
+
boundaries: readonly BoundaryEntry[],
|
|
45
|
+
events: readonly EventConnection[],
|
|
46
|
+
): string {
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
|
|
49
|
+
lines.push("Flows:");
|
|
50
|
+
for (const flow of flows) {
|
|
51
|
+
const route = flow.node.metadata?.route;
|
|
52
|
+
const routeStr = route ? `→ ${route} ` : "→ ";
|
|
53
|
+
lines.push(` ${flow.value.padEnd(20)} ${routeStr}(${flow.node.file}:${flow.node.lineStart})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("Boundaries:");
|
|
58
|
+
const boundaryGroups = groupBy(boundaries, (b) => b.value);
|
|
59
|
+
for (const [name, entries] of boundaryGroups) {
|
|
60
|
+
const files = new Set(entries.map((e) => e.node.file));
|
|
61
|
+
lines.push(
|
|
62
|
+
` ${name.padEnd(20)} → ${entries.length} function${entries.length !== 1 ? "s" : ""} across ${files.size} file${files.size !== 1 ? "s" : ""}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push("Events:");
|
|
68
|
+
for (const event of events) {
|
|
69
|
+
lines.push(
|
|
70
|
+
` ${event.eventName.padEnd(20)} → emitted by ${event.emitterName}, handled by ${event.handlerName}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Formats a flow call tree with boundary markers and event annotations.
|
|
79
|
+
*
|
|
80
|
+
* @param tree - Root node of the flow tree
|
|
81
|
+
* @returns Indented tree output
|
|
82
|
+
*/
|
|
83
|
+
function formatFlowTree(tree: FlowTreeNode): string {
|
|
84
|
+
const lines: string[] = [];
|
|
85
|
+
formatTreeNode(tree, lines, 0);
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Recursively formats a tree node with indentation. */
|
|
90
|
+
function formatTreeNode(node: FlowTreeNode, lines: string[], depth: number): void {
|
|
91
|
+
const indent = depth === 0 ? "" : `${" ".repeat(depth)}→ `;
|
|
92
|
+
const boundary = node.boundary ? ` [${node.boundary}]` : "";
|
|
93
|
+
const emits = node.emits ? ` emits ${node.emits}` : "";
|
|
94
|
+
lines.push(`${indent}${nodeRef(node.node)}${boundary}${emits}`);
|
|
95
|
+
|
|
96
|
+
for (const child of node.children) {
|
|
97
|
+
formatTreeNode(child, lines, depth + 1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Formats a symbol's context: signature, flows, callers, callees, boundary.
|
|
103
|
+
*
|
|
104
|
+
* @param data - Context data for the symbol
|
|
105
|
+
* @returns Compact text output
|
|
106
|
+
*/
|
|
107
|
+
function formatContext(data: ContextData): string {
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
|
|
110
|
+
lines.push(`${data.node.name} (${data.node.file}:${data.node.lineStart})`);
|
|
111
|
+
|
|
112
|
+
if (data.node.signature) {
|
|
113
|
+
lines.push(` signature: ${data.node.signature}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (data.flows.length > 0) {
|
|
117
|
+
lines.push(` flows: ${data.flows.join(", ")} (derived)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (data.callers.length > 0) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(" callers:");
|
|
123
|
+
for (const caller of data.callers) {
|
|
124
|
+
lines.push(` ← ${nodeRef(caller)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (data.callees.length > 0) {
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push(" callees:");
|
|
131
|
+
for (const callee of data.callees) {
|
|
132
|
+
lines.push(` → ${nodeRef(callee)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (data.boundary) {
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push(` boundary: ${data.boundary}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Formats impact analysis: direct callers, transitive callers, affected flows, tests.
|
|
146
|
+
*
|
|
147
|
+
* @param data - Impact analysis data
|
|
148
|
+
* @returns Compact text output
|
|
149
|
+
*/
|
|
150
|
+
function formatImpact(data: ImpactData): string {
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
|
|
153
|
+
lines.push("Direct callers:");
|
|
154
|
+
for (const caller of data.directCallers) {
|
|
155
|
+
lines.push(` ← ${nodeRef(caller)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (data.transitiveCallers.length > 0) {
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("Transitive callers:");
|
|
161
|
+
for (const caller of data.transitiveCallers) {
|
|
162
|
+
lines.push(` ← ${nodeRef(caller)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (data.affectedFlows.length > 0) {
|
|
167
|
+
lines.push("");
|
|
168
|
+
lines.push(`Affected flows: ${data.affectedFlows.join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (data.affectedTests.length > 0) {
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("Tests:");
|
|
174
|
+
for (const test of data.affectedTests) {
|
|
175
|
+
lines.push(` ${test.id}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Formats a list of callers.
|
|
184
|
+
*
|
|
185
|
+
* @param callers - Nodes that call the target
|
|
186
|
+
* @returns Compact text output
|
|
187
|
+
*/
|
|
188
|
+
function formatCallers(callers: readonly Node[]): string {
|
|
189
|
+
return callers.map((c) => `← ${nodeRef(c)}`).join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Formats a list of callees.
|
|
194
|
+
*
|
|
195
|
+
* @param callees - Nodes that the target calls
|
|
196
|
+
* @returns Compact text output
|
|
197
|
+
*/
|
|
198
|
+
function formatCallees(callees: readonly Node[]): string {
|
|
199
|
+
return callees.map((c) => `→ ${nodeRef(c)}`).join("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Formats boundaries grouped by system name.
|
|
204
|
+
*
|
|
205
|
+
* @param entries - All boundary entries
|
|
206
|
+
* @returns Compact text output
|
|
207
|
+
*/
|
|
208
|
+
function formatBoundaries(entries: readonly BoundaryEntry[]): string {
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
const groups = groupBy(entries, (e) => e.value);
|
|
211
|
+
|
|
212
|
+
for (const [name, group] of groups) {
|
|
213
|
+
lines.push(`${name}:`);
|
|
214
|
+
for (const entry of group) {
|
|
215
|
+
lines.push(` ${nodeRef(entry.node)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Formats event connections.
|
|
224
|
+
*
|
|
225
|
+
* @param events - All event connections
|
|
226
|
+
* @returns Compact text output
|
|
227
|
+
*/
|
|
228
|
+
function formatEvents(events: readonly EventConnection[]): string {
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
for (const event of events) {
|
|
231
|
+
lines.push(
|
|
232
|
+
`${event.eventName} → emitted by ${event.emitterName}, handled by ${event.handlerName}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Groups an array by a key function. */
|
|
239
|
+
function groupBy<T>(items: readonly T[], keyFn: (item: T) => string): Map<string, T[]> {
|
|
240
|
+
const groups = new Map<string, T[]>();
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
const key = keyFn(item);
|
|
243
|
+
const existing = groups.get(key);
|
|
244
|
+
if (existing) {
|
|
245
|
+
existing.push(item);
|
|
246
|
+
} else {
|
|
247
|
+
groups.set(key, [item]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return groups;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
type ContextData,
|
|
255
|
+
type FlowTreeNode,
|
|
256
|
+
formatBoundaries,
|
|
257
|
+
formatCallees,
|
|
258
|
+
formatCallers,
|
|
259
|
+
formatContext,
|
|
260
|
+
formatEvents,
|
|
261
|
+
formatFlowTree,
|
|
262
|
+
formatImpact,
|
|
263
|
+
formatOverview,
|
|
264
|
+
type ImpactData,
|
|
265
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Configuration for a Python language section. */
|
|
2
|
+
type PythonConfig = {
|
|
3
|
+
readonly sourceRoots: readonly string[];
|
|
4
|
+
readonly testPaths: readonly string[];
|
|
5
|
+
readonly frameworks: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Configuration for a TypeScript language section. */
|
|
9
|
+
type TypeScriptConfig = {
|
|
10
|
+
readonly sourceRoots: readonly string[];
|
|
11
|
+
readonly testPaths: readonly string[];
|
|
12
|
+
readonly tsconfig: string | undefined;
|
|
13
|
+
readonly frameworks: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Lint-specific configuration. */
|
|
17
|
+
type LintConfig = {
|
|
18
|
+
readonly strict: boolean;
|
|
19
|
+
readonly ignore: readonly string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Top-level Lattice configuration parsed from lattice.toml. */
|
|
23
|
+
type LatticeConfig = {
|
|
24
|
+
readonly languages: readonly string[];
|
|
25
|
+
readonly root: string;
|
|
26
|
+
readonly exclude: readonly string[];
|
|
27
|
+
readonly python: PythonConfig | undefined;
|
|
28
|
+
readonly typescript: TypeScriptConfig | undefined;
|
|
29
|
+
readonly lint: LintConfig;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type { LatticeConfig, LintConfig, PythonConfig, TypeScriptConfig };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Valid kinds for graph nodes. */
|
|
2
|
+
const NODE_KINDS = ["function", "method", "class", "type", "module"] as const;
|
|
3
|
+
type NodeKind = (typeof NODE_KINDS)[number];
|
|
4
|
+
|
|
5
|
+
/** Valid kinds for graph edges. */
|
|
6
|
+
const EDGE_KINDS = ["calls", "imports", "implements", "contains", "event"] as const;
|
|
7
|
+
type EdgeKind = (typeof EDGE_KINDS)[number];
|
|
8
|
+
|
|
9
|
+
/** Valid kinds for lattice tags. */
|
|
10
|
+
const TAG_KINDS = ["flow", "boundary", "emits", "handles"] as const;
|
|
11
|
+
type TagKind = (typeof TAG_KINDS)[number];
|
|
12
|
+
|
|
13
|
+
/** Certainty level of an edge relationship. */
|
|
14
|
+
const CERTAINTY_LEVELS = ["certain", "uncertain"] as const;
|
|
15
|
+
type Certainty = (typeof CERTAINTY_LEVELS)[number];
|
|
16
|
+
|
|
17
|
+
/** Reasons an extractor may fail to resolve a reference. */
|
|
18
|
+
const UNRESOLVED_REASONS = [
|
|
19
|
+
"dynamic_dispatch",
|
|
20
|
+
"unknown_module",
|
|
21
|
+
"computed_property",
|
|
22
|
+
"untyped_call",
|
|
23
|
+
] as const;
|
|
24
|
+
type UnresolvedReason = (typeof UNRESOLVED_REASONS)[number];
|
|
25
|
+
|
|
26
|
+
/** A symbol in the codebase (function, class, method, type, module). */
|
|
27
|
+
type Node = {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly kind: NodeKind;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly file: string;
|
|
32
|
+
readonly lineStart: number;
|
|
33
|
+
readonly lineEnd: number;
|
|
34
|
+
readonly language: string;
|
|
35
|
+
readonly signature: string | undefined;
|
|
36
|
+
readonly isTest: boolean;
|
|
37
|
+
readonly metadata: Record<string, string> | undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** A directed relationship between two nodes. */
|
|
41
|
+
type Edge = {
|
|
42
|
+
readonly sourceId: string;
|
|
43
|
+
readonly targetId: string;
|
|
44
|
+
readonly kind: EdgeKind;
|
|
45
|
+
readonly certainty: Certainty;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A lattice annotation on a node. */
|
|
49
|
+
type Tag = {
|
|
50
|
+
readonly nodeId: string;
|
|
51
|
+
readonly kind: TagKind;
|
|
52
|
+
readonly value: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** A reference that could not be resolved during extraction. */
|
|
56
|
+
type UnresolvedReference = {
|
|
57
|
+
readonly file: string;
|
|
58
|
+
readonly line: number;
|
|
59
|
+
readonly expression: string;
|
|
60
|
+
readonly reason: UnresolvedReason;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The complete output of extracting a single file. */
|
|
64
|
+
type ExtractionResult = {
|
|
65
|
+
readonly nodes: readonly Node[];
|
|
66
|
+
readonly edges: readonly Edge[];
|
|
67
|
+
readonly tags: readonly Tag[];
|
|
68
|
+
readonly unresolved: readonly UnresolvedReference[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
CERTAINTY_LEVELS,
|
|
73
|
+
type Certainty,
|
|
74
|
+
EDGE_KINDS,
|
|
75
|
+
type Edge,
|
|
76
|
+
type EdgeKind,
|
|
77
|
+
type ExtractionResult,
|
|
78
|
+
NODE_KINDS,
|
|
79
|
+
type Node,
|
|
80
|
+
type NodeKind,
|
|
81
|
+
TAG_KINDS,
|
|
82
|
+
type Tag,
|
|
83
|
+
type TagKind,
|
|
84
|
+
UNRESOLVED_REASONS,
|
|
85
|
+
type UnresolvedReason,
|
|
86
|
+
type UnresolvedReference,
|
|
87
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Severity levels for lint issues. */
|
|
2
|
+
const LINT_SEVERITIES = ["error", "warning", "info"] as const;
|
|
3
|
+
type LintSeverity = (typeof LINT_SEVERITIES)[number];
|
|
4
|
+
|
|
5
|
+
/** A single lint issue found in the codebase. */
|
|
6
|
+
type LintIssue = {
|
|
7
|
+
readonly severity: LintSeverity;
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly line: number;
|
|
10
|
+
readonly symbol: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Aggregate result of running all lint checks. */
|
|
15
|
+
type LintResult = {
|
|
16
|
+
readonly issues: readonly LintIssue[];
|
|
17
|
+
readonly coverage: { readonly tagged: number; readonly total: number };
|
|
18
|
+
readonly unresolvedCount: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type { LintIssue, LintResult, LintSeverity };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Represents a successful result containing data of type T. */
|
|
2
|
+
type Ok<T> = { readonly ok: true; readonly data: T };
|
|
3
|
+
|
|
4
|
+
/** Represents a failed result containing an error of type E. */
|
|
5
|
+
type Err<E> = { readonly ok: false; readonly error: E };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Discriminated union representing either success or failure.
|
|
9
|
+
* Used throughout the codebase instead of exceptions for error handling.
|
|
10
|
+
*/
|
|
11
|
+
type Result<T, E> = Ok<T> | Err<E>;
|
|
12
|
+
|
|
13
|
+
/** Creates a successful Result wrapping the given data. */
|
|
14
|
+
function ok<T>(data: T): Ok<T> {
|
|
15
|
+
return { ok: true, data };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Creates a failed Result wrapping the given error. */
|
|
19
|
+
function err<E>(error: E): Err<E> {
|
|
20
|
+
return { ok: false, error };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Type guard that narrows a Result to Ok. */
|
|
24
|
+
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
|
|
25
|
+
return result.ok;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Type guard that narrows a Result to Err. */
|
|
29
|
+
function isErr<T, E>(result: Result<T, E>): result is Err<E> {
|
|
30
|
+
return !result.ok;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts the data from an Ok result or throws on Err.
|
|
35
|
+
* Only use at program boundaries — prefer pattern matching elsewhere.
|
|
36
|
+
*
|
|
37
|
+
* @param result - The result to unwrap
|
|
38
|
+
* @returns The data inside the Ok variant
|
|
39
|
+
* @throws Error if the result is Err
|
|
40
|
+
*/
|
|
41
|
+
function unwrap<T, E>(result: Result<T, E>): T {
|
|
42
|
+
if (result.ok) return result.data;
|
|
43
|
+
throw new Error(`Unwrap called on Err: ${String(result.error)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transforms the data inside an Ok result, passes Err through unchanged.
|
|
48
|
+
*
|
|
49
|
+
* @param result - The result to transform
|
|
50
|
+
* @param fn - Transformation function applied to the Ok data
|
|
51
|
+
* @returns A new Result with the transformed data or the original error
|
|
52
|
+
*/
|
|
53
|
+
function mapResult<T, U, E>(result: Result<T, E>, fn: (data: T) => U): Result<U, E> {
|
|
54
|
+
if (result.ok) return ok(fn(result.data));
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { type Err, err, isErr, isOk, mapResult, type Ok, ok, type Result, unwrap };
|