@living-architecture/riviere-builder 0.2.1

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.
@@ -0,0 +1,35 @@
1
+ import type { Component, ComponentId } from '@living-architecture/riviere-schema';
2
+ import { ComponentNotFoundError } from './errors';
3
+ import type { NearMatchOptions, NearMatchQuery, NearMatchResult } from './types';
4
+ /**
5
+ * Finds components similar to a query using fuzzy matching.
6
+ *
7
+ * Used for error recovery to suggest alternatives when exact matches fail.
8
+ *
9
+ * @param components - Array of components to search
10
+ * @param query - Search criteria with name and optional type/domain filters
11
+ * @param options - Optional threshold and limit settings
12
+ * @returns Array of matching components with similarity scores
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const matches = findNearMatches(components, { name: 'Create Ordr' })
17
+ * // [{ component: {...}, score: 0.9, mismatch: undefined }]
18
+ * ```
19
+ */
20
+ export declare function findNearMatches(components: Component[], query: NearMatchQuery, options?: NearMatchOptions): NearMatchResult[];
21
+ /**
22
+ * Creates a typed error with suggestions for a missing source component.
23
+ *
24
+ * @param components - Array of existing components to search for suggestions
25
+ * @param id - The ComponentId that was not found
26
+ * @returns ComponentNotFoundError with suggestions array for programmatic access
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const error = createSourceNotFoundError(components, ComponentId.parse('orders:checkout:api:create-ordr'))
31
+ * // ComponentNotFoundError with suggestions: ['orders:checkout:api:create-order']
32
+ * ```
33
+ */
34
+ export declare function createSourceNotFoundError(components: Component[], id: ComponentId): ComponentNotFoundError;
35
+ //# sourceMappingURL=component-suggestion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-suggestion.d.ts","sourceRoot":"","sources":["../src/component-suggestion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAA;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEjD,OAAO,KAAK,EAAqB,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAoBnG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,SAAS,EAAE,EACvB,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,EAAE,CAmBnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,WAAW,GAAG,sBAAsB,CAI1G"}
@@ -0,0 +1,66 @@
1
+ import { ComponentNotFoundError } from './errors';
2
+ import { similarityScore } from './string-similarity';
3
+ function detectMismatch(query, component) {
4
+ const nameMatches = query.name.toLowerCase() === component.name.toLowerCase();
5
+ if (!nameMatches) {
6
+ return undefined;
7
+ }
8
+ if (query.type !== undefined && query.type !== component.type) {
9
+ return { field: 'type', expected: query.type, actual: component.type };
10
+ }
11
+ if (query.domain !== undefined && query.domain !== component.domain) {
12
+ return { field: 'domain', expected: query.domain, actual: component.domain };
13
+ }
14
+ return undefined;
15
+ }
16
+ /**
17
+ * Finds components similar to a query using fuzzy matching.
18
+ *
19
+ * Used for error recovery to suggest alternatives when exact matches fail.
20
+ *
21
+ * @param components - Array of components to search
22
+ * @param query - Search criteria with name and optional type/domain filters
23
+ * @param options - Optional threshold and limit settings
24
+ * @returns Array of matching components with similarity scores
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const matches = findNearMatches(components, { name: 'Create Ordr' })
29
+ * // [{ component: {...}, score: 0.9, mismatch: undefined }]
30
+ * ```
31
+ */
32
+ export function findNearMatches(components, query, options) {
33
+ if (query.name === '') {
34
+ return [];
35
+ }
36
+ const threshold = options?.threshold ?? 0.6;
37
+ const limit = options?.limit ?? 10;
38
+ const results = components
39
+ .map((component) => {
40
+ const score = similarityScore(query.name, component.name);
41
+ const mismatch = detectMismatch(query, component);
42
+ return { component, score, mismatch };
43
+ })
44
+ .filter((result) => result.score >= threshold || result.mismatch !== undefined)
45
+ .sort((a, b) => b.score - a.score)
46
+ .slice(0, limit);
47
+ return results;
48
+ }
49
+ /**
50
+ * Creates a typed error with suggestions for a missing source component.
51
+ *
52
+ * @param components - Array of existing components to search for suggestions
53
+ * @param id - The ComponentId that was not found
54
+ * @returns ComponentNotFoundError with suggestions array for programmatic access
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const error = createSourceNotFoundError(components, ComponentId.parse('orders:checkout:api:create-ordr'))
59
+ * // ComponentNotFoundError with suggestions: ['orders:checkout:api:create-order']
60
+ * ```
61
+ */
62
+ export function createSourceNotFoundError(components, id) {
63
+ const matches = findNearMatches(components, { name: id.name() }, { limit: 3 });
64
+ const suggestions = matches.map((s) => s.component.id);
65
+ return new ComponentNotFoundError(id.toString(), suggestions);
66
+ }
@@ -0,0 +1,28 @@
1
+ export declare class DuplicateDomainError extends Error {
2
+ readonly domainName: string;
3
+ constructor(domainName: string);
4
+ }
5
+ export declare class DomainNotFoundError extends Error {
6
+ readonly domainName: string;
7
+ constructor(domainName: string);
8
+ }
9
+ export declare class CustomTypeNotFoundError extends Error {
10
+ readonly customTypeName: string;
11
+ readonly definedTypes: string[];
12
+ constructor(customTypeName: string, definedTypes: string[]);
13
+ }
14
+ export declare class DuplicateComponentError extends Error {
15
+ readonly componentId: string;
16
+ constructor(componentId: string);
17
+ }
18
+ export declare class ComponentNotFoundError extends Error {
19
+ readonly componentId: string;
20
+ readonly suggestions: string[];
21
+ constructor(componentId: string, suggestions?: string[]);
22
+ }
23
+ export declare class InvalidEnrichmentTargetError extends Error {
24
+ readonly componentId: string;
25
+ readonly componentType: string;
26
+ constructor(componentId: string, componentType: string);
27
+ }
28
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;gBAEf,UAAU,EAAE,MAAM;CAK/B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;gBAEf,UAAU,EAAE,MAAM;CAK/B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;gBAEnB,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE;CAU3D;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;gBAEhB,WAAW,EAAE,MAAM;CAKhC;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,CAAA;gBAElB,WAAW,EAAE,MAAM,EAAE,WAAW,GAAE,MAAM,EAAO;CAS5D;AAED,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;gBAElB,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;CAMvD"}
package/dist/errors.js ADDED
@@ -0,0 +1,59 @@
1
+ export class DuplicateDomainError extends Error {
2
+ domainName;
3
+ constructor(domainName) {
4
+ super(`Domain '${domainName}' already exists`);
5
+ this.name = 'DuplicateDomainError';
6
+ this.domainName = domainName;
7
+ }
8
+ }
9
+ export class DomainNotFoundError extends Error {
10
+ domainName;
11
+ constructor(domainName) {
12
+ super(`Domain '${domainName}' does not exist`);
13
+ this.name = 'DomainNotFoundError';
14
+ this.domainName = domainName;
15
+ }
16
+ }
17
+ export class CustomTypeNotFoundError extends Error {
18
+ customTypeName;
19
+ definedTypes;
20
+ constructor(customTypeName, definedTypes) {
21
+ const suffix = definedTypes.length === 0
22
+ ? 'No custom types have been defined.'
23
+ : `Defined types: ${definedTypes.join(', ')}`;
24
+ super(`Custom type '${customTypeName}' not defined. ${suffix}`);
25
+ this.name = 'CustomTypeNotFoundError';
26
+ this.customTypeName = customTypeName;
27
+ this.definedTypes = definedTypes;
28
+ }
29
+ }
30
+ export class DuplicateComponentError extends Error {
31
+ componentId;
32
+ constructor(componentId) {
33
+ super(`Component with ID '${componentId}' already exists`);
34
+ this.name = 'DuplicateComponentError';
35
+ this.componentId = componentId;
36
+ }
37
+ }
38
+ export class ComponentNotFoundError extends Error {
39
+ componentId;
40
+ suggestions;
41
+ constructor(componentId, suggestions = []) {
42
+ const baseMessage = `Source component '${componentId}' not found`;
43
+ const message = suggestions.length > 0 ? `${baseMessage}. Did you mean: ${suggestions.join(', ')}?` : baseMessage;
44
+ super(message);
45
+ this.name = 'ComponentNotFoundError';
46
+ this.componentId = componentId;
47
+ this.suggestions = suggestions;
48
+ }
49
+ }
50
+ export class InvalidEnrichmentTargetError extends Error {
51
+ componentId;
52
+ componentType;
53
+ constructor(componentId, componentType) {
54
+ super(`Only DomainOp components can be enriched. '${componentId}' is type '${componentType}'`);
55
+ this.name = 'InvalidEnrichmentTargetError';
56
+ this.componentId = componentId;
57
+ this.componentType = componentType;
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ export * from './builder';
2
+ export { ComponentId, type ComponentIdParts } from '@living-architecture/riviere-schema';
3
+ export * from './errors';
4
+ export { findNearMatches } from './component-suggestion';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACzF,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './builder';
2
+ export { ComponentId } from '@living-architecture/riviere-schema';
3
+ export * from './errors';
4
+ export { findNearMatches } from './component-suggestion';
@@ -0,0 +1,90 @@
1
+ import type { Component, CustomTypeDefinition, DomainMetadata, ExternalLink, Link, RiviereGraph, SourceInfo } from '@living-architecture/riviere-schema';
2
+ import { type ValidationResult } from '@living-architecture/riviere-query';
3
+ import type { BuilderStats, BuilderWarning } from './types';
4
+ interface InspectionGraph {
5
+ version: string;
6
+ metadata: {
7
+ name?: string;
8
+ description?: string;
9
+ generated?: string;
10
+ sources: SourceInfo[];
11
+ domains: Record<string, DomainMetadata>;
12
+ customTypes: Record<string, CustomTypeDefinition>;
13
+ };
14
+ components: Component[];
15
+ links: Link[];
16
+ externalLinks: ExternalLink[];
17
+ }
18
+ /**
19
+ * Finds components with no incoming or outgoing links.
20
+ *
21
+ * @param graph - The graph to inspect
22
+ * @returns Array of orphaned component IDs
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const orphans = findOrphans(graph)
27
+ * // ['orders:checkout:api:unused-endpoint']
28
+ * ```
29
+ */
30
+ export declare function findOrphans(graph: InspectionGraph): string[];
31
+ /**
32
+ * Calculates statistics about the graph.
33
+ *
34
+ * @param graph - The graph to analyze
35
+ * @returns Object with component counts, link counts, and domain count
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const stats = calculateStats(graph)
40
+ * // { componentCount: 10, linkCount: 8, domainCount: 2, ... }
41
+ * ```
42
+ */
43
+ export declare function calculateStats(graph: InspectionGraph): BuilderStats;
44
+ /**
45
+ * Finds non-fatal issues in the graph.
46
+ *
47
+ * Detects orphaned components and unused domains.
48
+ *
49
+ * @param graph - The graph to inspect
50
+ * @returns Array of warning objects
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const warnings = findWarnings(graph)
55
+ * // [{ code: 'ORPHAN_COMPONENT', message: '...', componentId: '...' }]
56
+ * ```
57
+ */
58
+ export declare function findWarnings(graph: InspectionGraph): BuilderWarning[];
59
+ /**
60
+ * Converts builder internal graph to schema-compliant RiviereGraph.
61
+ *
62
+ * Removes undefined optional fields and ensures proper structure.
63
+ *
64
+ * @param graph - The internal builder graph
65
+ * @returns Schema-compliant RiviereGraph
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const output = toRiviereGraph(builderGraph)
70
+ * JSON.stringify(output) // Valid Rivière JSON
71
+ * ```
72
+ */
73
+ export declare function toRiviereGraph(graph: InspectionGraph): RiviereGraph;
74
+ /**
75
+ * Validates the graph against the Rivière schema.
76
+ *
77
+ * @param graph - The graph to validate
78
+ * @returns Validation result with valid flag and any errors
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const result = validateGraph(graph)
83
+ * if (!result.valid) {
84
+ * console.error(result.errors)
85
+ * }
86
+ * ```
87
+ */
88
+ export declare function validateGraph(graph: InspectionGraph): ValidationResult;
89
+ export {};
90
+ //# sourceMappingURL=inspection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspection.d.ts","sourceRoot":"","sources":["../src/inspection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,UAAU,EACX,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AACxF,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,EAAE,UAAU,EAAE,CAAA;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;QACvC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;KAClD,CAAA;IACD,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,KAAK,EAAE,IAAI,EAAE,CAAA;IACb,aAAa,EAAE,YAAY,EAAE,CAAA;CAC9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,EAAE,CAa5D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,YAAY,CAiBnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,eAAe,GAAG,cAAc,EAAE,CAuBrE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,YAAY,CAiBnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAEtE"}
@@ -0,0 +1,137 @@
1
+ import { RiviereQuery } from '@living-architecture/riviere-query';
2
+ /**
3
+ * Finds components with no incoming or outgoing links.
4
+ *
5
+ * @param graph - The graph to inspect
6
+ * @returns Array of orphaned component IDs
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const orphans = findOrphans(graph)
11
+ * // ['orders:checkout:api:unused-endpoint']
12
+ * ```
13
+ */
14
+ export function findOrphans(graph) {
15
+ const connectedIds = new Set();
16
+ for (const link of graph.links) {
17
+ connectedIds.add(link.source);
18
+ connectedIds.add(link.target);
19
+ }
20
+ for (const externalLink of graph.externalLinks) {
21
+ connectedIds.add(externalLink.source);
22
+ }
23
+ return graph.components.filter((c) => !connectedIds.has(c.id)).map((c) => c.id);
24
+ }
25
+ /**
26
+ * Calculates statistics about the graph.
27
+ *
28
+ * @param graph - The graph to analyze
29
+ * @returns Object with component counts, link counts, and domain count
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const stats = calculateStats(graph)
34
+ * // { componentCount: 10, linkCount: 8, domainCount: 2, ... }
35
+ * ```
36
+ */
37
+ export function calculateStats(graph) {
38
+ const components = graph.components;
39
+ return {
40
+ componentCount: components.length,
41
+ componentsByType: {
42
+ UI: components.filter((c) => c.type === 'UI').length,
43
+ API: components.filter((c) => c.type === 'API').length,
44
+ UseCase: components.filter((c) => c.type === 'UseCase').length,
45
+ DomainOp: components.filter((c) => c.type === 'DomainOp').length,
46
+ Event: components.filter((c) => c.type === 'Event').length,
47
+ EventHandler: components.filter((c) => c.type === 'EventHandler').length,
48
+ Custom: components.filter((c) => c.type === 'Custom').length,
49
+ },
50
+ linkCount: graph.links.length,
51
+ externalLinkCount: graph.externalLinks.length,
52
+ domainCount: Object.keys(graph.metadata.domains).length,
53
+ };
54
+ }
55
+ /**
56
+ * Finds non-fatal issues in the graph.
57
+ *
58
+ * Detects orphaned components and unused domains.
59
+ *
60
+ * @param graph - The graph to inspect
61
+ * @returns Array of warning objects
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const warnings = findWarnings(graph)
66
+ * // [{ code: 'ORPHAN_COMPONENT', message: '...', componentId: '...' }]
67
+ * ```
68
+ */
69
+ export function findWarnings(graph) {
70
+ const warnings = [];
71
+ for (const id of findOrphans(graph)) {
72
+ warnings.push({
73
+ code: 'ORPHAN_COMPONENT',
74
+ message: `Component '${id}' has no incoming or outgoing links`,
75
+ componentId: id,
76
+ });
77
+ }
78
+ const usedDomains = new Set(graph.components.map((c) => c.domain));
79
+ for (const domain of Object.keys(graph.metadata.domains)) {
80
+ if (!usedDomains.has(domain)) {
81
+ warnings.push({
82
+ code: 'UNUSED_DOMAIN',
83
+ message: `Domain '${domain}' is declared but has no components`,
84
+ domainName: domain,
85
+ });
86
+ }
87
+ }
88
+ return warnings;
89
+ }
90
+ /**
91
+ * Converts builder internal graph to schema-compliant RiviereGraph.
92
+ *
93
+ * Removes undefined optional fields and ensures proper structure.
94
+ *
95
+ * @param graph - The internal builder graph
96
+ * @returns Schema-compliant RiviereGraph
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const output = toRiviereGraph(builderGraph)
101
+ * JSON.stringify(output) // Valid Rivière JSON
102
+ * ```
103
+ */
104
+ export function toRiviereGraph(graph) {
105
+ const hasCustomTypes = Object.keys(graph.metadata.customTypes).length > 0;
106
+ const hasExternalLinks = graph.externalLinks.length > 0;
107
+ return {
108
+ version: graph.version,
109
+ metadata: {
110
+ ...(graph.metadata.name !== undefined && { name: graph.metadata.name }),
111
+ ...(graph.metadata.description !== undefined && { description: graph.metadata.description }),
112
+ sources: graph.metadata.sources,
113
+ domains: graph.metadata.domains,
114
+ ...(hasCustomTypes && { customTypes: graph.metadata.customTypes }),
115
+ },
116
+ components: graph.components,
117
+ links: graph.links,
118
+ ...(hasExternalLinks && { externalLinks: graph.externalLinks }),
119
+ };
120
+ }
121
+ /**
122
+ * Validates the graph against the Rivière schema.
123
+ *
124
+ * @param graph - The graph to validate
125
+ * @returns Validation result with valid flag and any errors
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const result = validateGraph(graph)
130
+ * if (!result.valid) {
131
+ * console.error(result.errors)
132
+ * }
133
+ * ```
134
+ */
135
+ export function validateGraph(graph) {
136
+ return new RiviereQuery(toRiviereGraph(graph)).validate();
137
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Calculates the Levenshtein edit distance between two strings.
3
+ *
4
+ * @param a - First string
5
+ * @param b - Second string
6
+ * @returns Number of single-character edits needed to transform a into b
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * levenshteinDistance('kitten', 'sitting') // 3
11
+ * levenshteinDistance('hello', 'hello') // 0
12
+ * ```
13
+ */
14
+ export declare function levenshteinDistance(a: string, b: string): number;
15
+ /**
16
+ * Calculates normalized similarity score between two strings.
17
+ *
18
+ * Returns 1.0 for identical strings, 0.0 for completely different strings.
19
+ * Case-insensitive comparison.
20
+ *
21
+ * @param a - First string
22
+ * @param b - Second string
23
+ * @returns Similarity score from 0.0 to 1.0
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * similarityScore('Order', 'order') // 1.0
28
+ * similarityScore('Order', 'Ordr') // 0.8
29
+ * ```
30
+ */
31
+ export declare function similarityScore(a: string, b: string): number;
32
+ //# sourceMappingURL=string-similarity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-similarity.d.ts","sourceRoot":"","sources":["../src/string-similarity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAqBhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAW5D"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Calculates the Levenshtein edit distance between two strings.
3
+ *
4
+ * @param a - First string
5
+ * @param b - Second string
6
+ * @returns Number of single-character edits needed to transform a into b
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * levenshteinDistance('kitten', 'sitting') // 3
11
+ * levenshteinDistance('hello', 'hello') // 0
12
+ * ```
13
+ */
14
+ export function levenshteinDistance(a, b) {
15
+ if (a.length === 0)
16
+ return b.length;
17
+ if (b.length === 0)
18
+ return a.length;
19
+ const previousRow = Array.from({ length: b.length + 1 }, (_, j) => j);
20
+ const finalRow = [...a].reduce((currentRow, aChar, i) => {
21
+ const nextRow = [i + 1];
22
+ return [...b].reduce((row, bChar, j) => {
23
+ const cost = aChar === bChar ? 0 : 1;
24
+ const deletion = currentRow[j + 1];
25
+ const insertion = row[j];
26
+ const substitution = currentRow[j];
27
+ /* v8 ignore next -- @preserve */
28
+ const value = Math.min((insertion ?? 0) + 1, (deletion ?? 0) + 1, (substitution ?? 0) + cost);
29
+ return [...row, value];
30
+ }, nextRow);
31
+ }, previousRow);
32
+ /* v8 ignore next -- @preserve */
33
+ return finalRow[b.length] ?? 0;
34
+ }
35
+ /**
36
+ * Calculates normalized similarity score between two strings.
37
+ *
38
+ * Returns 1.0 for identical strings, 0.0 for completely different strings.
39
+ * Case-insensitive comparison.
40
+ *
41
+ * @param a - First string
42
+ * @param b - Second string
43
+ * @returns Similarity score from 0.0 to 1.0
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * similarityScore('Order', 'order') // 1.0
48
+ * similarityScore('Order', 'Ordr') // 0.8
49
+ * ```
50
+ */
51
+ export function similarityScore(a, b) {
52
+ const aLower = a.toLowerCase();
53
+ const bLower = b.toLowerCase();
54
+ if (aLower.length === 0 && bLower.length === 0)
55
+ return 1.0;
56
+ if (aLower.length === 0 || bLower.length === 0)
57
+ return 0.0;
58
+ const distance = levenshteinDistance(aLower, bLower);
59
+ const maxLength = Math.max(aLower.length, bLower.length);
60
+ return 1 - distance / maxLength;
61
+ }