@brainwav/diagram 1.0.8 → 1.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/.diagram/contracts/machine-command-coverage.json +73 -0
- package/.diagram/migration/finalization-policy.json +20 -0
- package/LICENSE +202 -21
- package/README.md +132 -339
- package/package.json +46 -13
- package/scripts/refresh-diagram-context.sh +274 -182
- package/src/analyzers/default-analyzer.js +11 -0
- package/src/analyzers/index.js +34 -0
- package/src/artifacts/agent-context.js +105 -0
- package/src/artifacts/artifact-budget.js +224 -0
- package/src/artifacts/brief.js +153 -0
- package/src/artifacts/evidence-manifest.js +206 -0
- package/src/artifacts/evidence-summary.js +29 -0
- package/src/commands/analyze.js +125 -0
- package/src/commands/changed.js +185 -0
- package/src/commands/context.js +110 -0
- package/src/commands/diff.js +142 -0
- package/src/commands/doctor.js +335 -0
- package/src/commands/explain.js +273 -0
- package/src/commands/generate-all.js +170 -0
- package/src/commands/generate-animated.js +50 -0
- package/src/commands/generate-video.js +65 -0
- package/src/commands/generate.js +522 -0
- package/src/commands/init.js +123 -0
- package/src/commands/output.js +76 -0
- package/src/commands/scan.js +624 -0
- package/src/commands/shared.js +396 -0
- package/src/commands/validate.js +328 -0
- package/src/commands/video-shared.js +105 -0
- package/src/commands/workflow-pr.js +26 -0
- package/src/confidence/pipeline.js +186 -0
- package/src/config/diagramrc.js +79 -0
- package/src/context/build-context-pack.js +291 -0
- package/src/context/normalize-diagram-manifest.js +282 -0
- package/src/core/analysis-generation-analyze-components.js +102 -0
- package/src/core/analysis-generation-analyze-dependencies.js +33 -0
- package/src/core/analysis-generation-analyze-files.js +48 -0
- package/src/core/analysis-generation-analyze-options.js +73 -0
- package/src/core/analysis-generation-analyze.js +63 -0
- package/src/core/analysis-generation-constants.js +53 -0
- package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
- package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
- package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
- package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
- package/src/core/analysis-generation-diagrams-core.js +12 -0
- package/src/core/analysis-generation-diagrams-empty.js +68 -0
- package/src/core/analysis-generation-diagrams-erd.js +59 -0
- package/src/core/analysis-generation-diagrams-limit.js +27 -0
- package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
- package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
- package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
- package/src/core/analysis-generation-diagrams-role-data.js +182 -0
- package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
- package/src/core/analysis-generation-diagrams-role-security.js +129 -0
- package/src/core/analysis-generation-diagrams-role.js +25 -0
- package/src/core/analysis-generation-diagrams.js +182 -0
- package/src/core/analysis-generation-role-tags-constants.js +55 -0
- package/src/core/analysis-generation-role-tags-imports.js +32 -0
- package/src/core/analysis-generation-role-tags-infer.js +49 -0
- package/src/core/analysis-generation-role-tags-match.js +19 -0
- package/src/core/analysis-generation-role-tags.js +7 -0
- package/src/core/analysis-generation-utils-core.js +308 -0
- package/src/core/analysis-generation-utils-graph.js +321 -0
- package/src/core/analysis-generation-utils-resolution.js +76 -0
- package/src/core/analysis-generation-utils.js +9 -0
- package/src/core/analysis-generation.js +44 -0
- package/src/diagram.js +178 -1761
- package/src/formatters/console.js +198 -0
- package/src/formatters/index.js +41 -0
- package/src/formatters/json.js +113 -0
- package/src/formatters/junit.js +123 -0
- package/src/graph.js +159 -0
- package/src/incremental/cache.js +210 -0
- package/src/ir/architecture-ir.js +48 -0
- package/src/migration/evidence.js +262 -0
- package/src/migration/finalization-policy.js +35 -0
- package/src/renderers/report-html.js +265 -0
- package/src/rules/factory.js +108 -0
- package/src/rules/types/base.js +54 -0
- package/src/rules/types/import-rule.js +286 -0
- package/src/rules.js +380 -0
- package/src/schema/erd-confidence.js +56 -0
- package/src/schema/erd-extractor.js +504 -0
- package/src/schema/erd-model.js +176 -0
- package/src/schema/rules-schema.js +170 -0
- package/src/utils/suggestions.js +67 -0
- package/src/video.js +4 -5
- package/src/workflow/git-helpers.js +576 -0
- package/src/workflow/pr-command.js +694 -0
- package/src/workflow/pr-impact.js +848 -0
- package/src/workflow/sort-utils.js +16 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
function sanitizeToken(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.trim()
|
|
4
|
+
.replace(/[`"'\\]/g, '')
|
|
5
|
+
.replace(/[^A-Za-z0-9_]/g, '_');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function canonicalEntityName(value) {
|
|
9
|
+
const token = sanitizeToken(value);
|
|
10
|
+
return token ? token.toUpperCase() : '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function canonicalAttributeName(value) {
|
|
14
|
+
return sanitizeToken(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalType(value) {
|
|
18
|
+
const token = sanitizeToken(value);
|
|
19
|
+
return token || 'unknown';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toKeyFlags(flags) {
|
|
23
|
+
const order = ['PK', 'FK', 'UK'];
|
|
24
|
+
const seen = new Set((Array.isArray(flags) ? flags : []).map((flag) => String(flag).toUpperCase()));
|
|
25
|
+
return order.filter((flag) => seen.has(flag));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mergeAttributes(existingAttributes, incomingAttributes) {
|
|
29
|
+
const byName = new Map();
|
|
30
|
+
|
|
31
|
+
for (const attribute of [...existingAttributes, ...incomingAttributes]) {
|
|
32
|
+
const name = canonicalAttributeName(attribute?.name);
|
|
33
|
+
if (!name) continue;
|
|
34
|
+
const prev = byName.get(name);
|
|
35
|
+
const next = {
|
|
36
|
+
name,
|
|
37
|
+
type: canonicalType(attribute?.type),
|
|
38
|
+
nullable: Boolean(attribute?.nullable),
|
|
39
|
+
keyFlags: toKeyFlags(attribute?.keyFlags),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (!prev) {
|
|
43
|
+
byName.set(name, next);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mergedFlags = toKeyFlags([...(prev.keyFlags || []), ...(next.keyFlags || [])]);
|
|
48
|
+
byName.set(name, {
|
|
49
|
+
name,
|
|
50
|
+
type: prev.type !== 'unknown' ? prev.type : next.type,
|
|
51
|
+
nullable: prev.nullable || next.nullable,
|
|
52
|
+
keyFlags: mergedFlags,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const keyPriority = ['PK', 'FK', 'UK'];
|
|
57
|
+
const score = (attribute) => {
|
|
58
|
+
if ((attribute.keyFlags || []).length === 0) return keyPriority.length;
|
|
59
|
+
const positions = attribute.keyFlags
|
|
60
|
+
.map((flag) => keyPriority.indexOf(flag))
|
|
61
|
+
.filter((index) => index >= 0);
|
|
62
|
+
return positions.length > 0 ? Math.min(...positions) : keyPriority.length;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return [...byName.values()].sort((a, b) => {
|
|
66
|
+
const scoreDiff = score(a) - score(b);
|
|
67
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
68
|
+
return a.name.localeCompare(b.name);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeErdModel(input) {
|
|
73
|
+
const entitiesInput = Array.isArray(input?.entities) ? input.entities : [];
|
|
74
|
+
const relationshipsInput = Array.isArray(input?.relationships) ? input.relationships : [];
|
|
75
|
+
const diagnostics = Array.isArray(input?.diagnostics) ? input.diagnostics : [];
|
|
76
|
+
const sourceFiles = Array.isArray(input?.sourceFiles) ? input.sourceFiles : [];
|
|
77
|
+
const sourcePrecedence = Array.isArray(input?.sourcePrecedence) ? input.sourcePrecedence : [];
|
|
78
|
+
|
|
79
|
+
const entitiesByName = new Map();
|
|
80
|
+
for (const entity of entitiesInput) {
|
|
81
|
+
const name = canonicalEntityName(entity?.name);
|
|
82
|
+
if (!name) continue;
|
|
83
|
+
const source = entity?.source === 'inferred' ? 'inferred' : 'explicit';
|
|
84
|
+
const existing = entitiesByName.get(name);
|
|
85
|
+
const attributes = mergeAttributes(existing?.attributes || [], entity?.attributes || []);
|
|
86
|
+
const merged = {
|
|
87
|
+
name,
|
|
88
|
+
source: existing?.source === 'explicit' || source === 'explicit' ? 'explicit' : 'inferred',
|
|
89
|
+
attributes,
|
|
90
|
+
};
|
|
91
|
+
entitiesByName.set(name, merged);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const entities = [...entitiesByName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
+
const entitySet = new Set(entities.map((entity) => entity.name));
|
|
96
|
+
const relationshipMap = new Map();
|
|
97
|
+
|
|
98
|
+
for (const relationship of relationshipsInput) {
|
|
99
|
+
const fromEntity = canonicalEntityName(relationship?.fromEntity);
|
|
100
|
+
const toEntity = canonicalEntityName(relationship?.toEntity);
|
|
101
|
+
if (!fromEntity || !toEntity) continue;
|
|
102
|
+
if (!entitySet.has(fromEntity) || !entitySet.has(toEntity)) continue;
|
|
103
|
+
|
|
104
|
+
const provenance = relationship?.provenance === 'inferred' ? 'inferred' : 'explicit';
|
|
105
|
+
const cardinality = String(relationship?.cardinality || '||--o{');
|
|
106
|
+
const relationshipKey = `${fromEntity}|${toEntity}|${cardinality}`;
|
|
107
|
+
const existing = relationshipMap.get(relationshipKey);
|
|
108
|
+
if (!existing || existing.provenance === 'inferred') {
|
|
109
|
+
relationshipMap.set(relationshipKey, {
|
|
110
|
+
fromEntity,
|
|
111
|
+
toEntity,
|
|
112
|
+
cardinality,
|
|
113
|
+
provenance,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const relationships = [...relationshipMap.values()].sort((a, b) => {
|
|
119
|
+
if (a.fromEntity !== b.fromEntity) return a.fromEntity.localeCompare(b.fromEntity);
|
|
120
|
+
if (a.toEntity !== b.toEntity) return a.toEntity.localeCompare(b.toEntity);
|
|
121
|
+
if (a.provenance !== b.provenance) return a.provenance.localeCompare(b.provenance);
|
|
122
|
+
return a.cardinality.localeCompare(b.cardinality);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
entities,
|
|
127
|
+
relationships,
|
|
128
|
+
diagnostics,
|
|
129
|
+
sourceFiles: [...new Set(sourceFiles)].sort(),
|
|
130
|
+
sourcePrecedence: [...new Set(sourcePrecedence)],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderErdMermaid(model, options = {}) {
|
|
135
|
+
const entities = Array.isArray(model?.entities) ? model.entities : [];
|
|
136
|
+
const relationships = Array.isArray(model?.relationships) ? model.relationships : [];
|
|
137
|
+
const lines = ['erDiagram'];
|
|
138
|
+
|
|
139
|
+
if (options.lowConfidenceMarker) {
|
|
140
|
+
const percent = typeof options.inferenceShare === 'number'
|
|
141
|
+
? Math.round(options.inferenceShare * 100)
|
|
142
|
+
: null;
|
|
143
|
+
const marker = percent === null
|
|
144
|
+
? 'low-confidence: inferred relationships are above preferred threshold'
|
|
145
|
+
: `low-confidence: inferred relationships are ${percent}% of all relationships`;
|
|
146
|
+
lines.push(` %% ${marker}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entity of entities) {
|
|
150
|
+
lines.push(` ${entity.name} {`);
|
|
151
|
+
for (const attribute of entity.attributes || []) {
|
|
152
|
+
const flags = (attribute.keyFlags || []).join(' ');
|
|
153
|
+
const suffix = flags ? ` ${flags}` : '';
|
|
154
|
+
lines.push(` ${canonicalType(attribute.type)} ${canonicalAttributeName(attribute.name)}${suffix}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(' }');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (relationships.length > 0) {
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const relationship of relationships) {
|
|
164
|
+
lines.push(
|
|
165
|
+
` ${relationship.fromEntity} ${relationship.cardinality} ${relationship.toEntity} : ${relationship.provenance}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
canonicalEntityName,
|
|
174
|
+
normalizeErdModel,
|
|
175
|
+
renderErdMermaid,
|
|
176
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for architecture rules validation
|
|
5
|
+
* Used to validate .architecture.yml files
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Single rule schema
|
|
9
|
+
const ruleSchema = z.object({
|
|
10
|
+
name: z.string()
|
|
11
|
+
.min(1, 'Rule name is required')
|
|
12
|
+
.max(100, 'Rule name must be less than 100 characters'),
|
|
13
|
+
|
|
14
|
+
description: z.string()
|
|
15
|
+
.max(500, 'Description must be less than 500 characters')
|
|
16
|
+
.optional(),
|
|
17
|
+
|
|
18
|
+
baseline: z.number()
|
|
19
|
+
.int('Baseline must be an integer')
|
|
20
|
+
.min(0, 'Baseline must be non-negative')
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Accepted violation count. New violations beyond this will fail.'),
|
|
23
|
+
|
|
24
|
+
layer: z.union([
|
|
25
|
+
z.string().min(1, 'Layer pattern is required'),
|
|
26
|
+
z.array(z.string().min(1, 'Layer pattern cannot be empty'))
|
|
27
|
+
.min(1, 'At least one layer pattern is required')
|
|
28
|
+
]),
|
|
29
|
+
|
|
30
|
+
// Import constraint rules
|
|
31
|
+
must_not_import_from: z.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('List of patterns that files in this layer must not import'),
|
|
34
|
+
|
|
35
|
+
may_import_from: z.array(z.string())
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Whitelist of allowed import patterns'),
|
|
38
|
+
|
|
39
|
+
must_import_from: z.array(z.string())
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('List of patterns that files in this layer must import'),
|
|
42
|
+
|
|
43
|
+
// Inward-only constraint: files in this layer cannot be imported by other inward_only layers
|
|
44
|
+
inward_only: z.boolean()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('If true, other inward_only layers cannot import from this layer'),
|
|
47
|
+
|
|
48
|
+
}).refine(
|
|
49
|
+
(data) => {
|
|
50
|
+
// At least one constraint must be specified
|
|
51
|
+
// Must check for non-empty arrays (empty array is truthy but invalid)
|
|
52
|
+
const hasMustNotImport = Array.isArray(data.must_not_import_from) && data.must_not_import_from.length > 0;
|
|
53
|
+
const hasMayImport = Array.isArray(data.may_import_from) && data.may_import_from.length > 0;
|
|
54
|
+
const hasMustImport = Array.isArray(data.must_import_from) && data.must_import_from.length > 0;
|
|
55
|
+
const hasInwardOnly = data.inward_only === true;
|
|
56
|
+
return hasMustNotImport || hasMayImport || hasMustImport || hasInwardOnly;
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
message: 'Rule must specify at least one constraint (must_not_import_from, may_import_from, must_import_from, or inward_only)',
|
|
60
|
+
path: ['constraints']
|
|
61
|
+
}
|
|
62
|
+
).refine(
|
|
63
|
+
(data) => {
|
|
64
|
+
// Validate that layer patterns don't contain '..' or absolute paths
|
|
65
|
+
const patterns = Array.isArray(data.layer) ? data.layer : [data.layer];
|
|
66
|
+
return patterns.every(p => !p.includes('..') && !p.startsWith('/'));
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
message: 'Layer patterns cannot contain ".." or absolute paths',
|
|
70
|
+
path: ['layer']
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Full configuration schema
|
|
75
|
+
const configSchema = z.object({
|
|
76
|
+
version: z.string()
|
|
77
|
+
.regex(/^[0-9]+\.[0-9]+$/, 'Version must be in format "X.Y"')
|
|
78
|
+
.default('1.0'),
|
|
79
|
+
|
|
80
|
+
extends: z.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Path to base configuration file to extend'),
|
|
83
|
+
|
|
84
|
+
rules: z.array(ruleSchema)
|
|
85
|
+
.min(1, 'At least one rule is required'),
|
|
86
|
+
}).strict(); // Reject unknown properties for safety
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate configuration against schema
|
|
90
|
+
* @param {Object} config - Parsed configuration
|
|
91
|
+
* @returns {Object} Validation result
|
|
92
|
+
*/
|
|
93
|
+
function validateConfig(config) {
|
|
94
|
+
const result = configSchema.safeParse(config);
|
|
95
|
+
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
const errors = result.error.issues.map(err => ({
|
|
98
|
+
path: err.path.join('.'),
|
|
99
|
+
message: err.message,
|
|
100
|
+
value: String(err.received).slice(0, 100) // Truncate large values
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
valid: false,
|
|
105
|
+
errors
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
valid: true,
|
|
111
|
+
data: result.data
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate a single rule
|
|
117
|
+
* @param {Object} rule - Rule configuration
|
|
118
|
+
* @returns {Object} Validation result
|
|
119
|
+
*/
|
|
120
|
+
function validateRule(rule) {
|
|
121
|
+
const result = ruleSchema.safeParse(rule);
|
|
122
|
+
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
errors: result.error.issues.map(err => ({
|
|
127
|
+
path: err.path.join('.'),
|
|
128
|
+
message: err.message
|
|
129
|
+
}))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
valid: true,
|
|
135
|
+
data: result.data
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get default configuration
|
|
141
|
+
* @returns {Object}
|
|
142
|
+
*/
|
|
143
|
+
function getDefaultConfig() {
|
|
144
|
+
return {
|
|
145
|
+
version: '1.0',
|
|
146
|
+
rules: [
|
|
147
|
+
{
|
|
148
|
+
name: 'Domain isolation',
|
|
149
|
+
description: 'Domain logic should not depend on UI',
|
|
150
|
+
layer: 'src/domain',
|
|
151
|
+
must_not_import_from: ['src/ui', 'src/components']
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'API contract',
|
|
155
|
+
description: 'API routes only use domain and shared',
|
|
156
|
+
layer: 'src/api',
|
|
157
|
+
may_import_from: ['src/domain', 'src/shared', 'src/types'],
|
|
158
|
+
must_not_import_from: ['src/ui']
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
configSchema,
|
|
166
|
+
ruleSchema,
|
|
167
|
+
validateConfig,
|
|
168
|
+
validateRule,
|
|
169
|
+
getDefaultConfig
|
|
170
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute the Levenshtein edit distance between two strings.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} a - First string to compare.
|
|
7
|
+
* @param {string} b - Second string to compare.
|
|
8
|
+
* @returns {number} The minimum number of single-character insertions, deletions or substitutions required to transform `a` into `b`.
|
|
9
|
+
*/
|
|
10
|
+
function levenshtein(a, b) {
|
|
11
|
+
const rows = a.length + 1;
|
|
12
|
+
const cols = b.length + 1;
|
|
13
|
+
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < rows; i += 1) matrix[i][0] = i;
|
|
16
|
+
for (let j = 0; j < cols; j += 1) matrix[0][j] = j;
|
|
17
|
+
|
|
18
|
+
for (let i = 1; i < rows; i += 1) {
|
|
19
|
+
for (let j = 1; j < cols; j += 1) {
|
|
20
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
21
|
+
matrix[i][j] = Math.min(
|
|
22
|
+
matrix[i - 1][j] + 1,
|
|
23
|
+
matrix[i][j - 1] + 1,
|
|
24
|
+
matrix[i - 1][j - 1] + cost
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return matrix[a.length][b.length];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Selects the closest matching option to the given input from an array of candidates.
|
|
34
|
+
*
|
|
35
|
+
* Compares `input` (stringified and lowercased) against each element of `options` (stringified and lowercased)
|
|
36
|
+
* using Levenshtein edit distance and returns the candidate with the lowest distance only if that distance
|
|
37
|
+
* is within an acceptance threshold proportional to the input length; otherwise returns `null`.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} input - Value to match; it will be converted to a string for comparison.
|
|
40
|
+
* @param {Array<*>} options - Array of candidate values (each will be converted to a string).
|
|
41
|
+
* @returns {*} The best matching option from `options` if its edit distance is acceptable, `null` otherwise.
|
|
42
|
+
*/
|
|
43
|
+
function findClosestMatch(input, options) {
|
|
44
|
+
if (!input || !Array.isArray(options) || options.length === 0) return null;
|
|
45
|
+
|
|
46
|
+
const normalizedInput = String(input).toLowerCase();
|
|
47
|
+
const scored = options
|
|
48
|
+
.map((option) => ({ option, score: levenshtein(normalizedInput, String(option).toLowerCase()) }))
|
|
49
|
+
.sort((a, b) => a.score - b.score);
|
|
50
|
+
|
|
51
|
+
const best = scored[0];
|
|
52
|
+
return best.score <= Math.max(2, Math.floor(normalizedInput.length / 3)) ? best.option : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a suggestion message for display.
|
|
57
|
+
* @param {string} suggestion - The suggested alternative to present to the user.
|
|
58
|
+
* @returns {string} A grey-coloured string in the form " Did you mean: <suggestion>".
|
|
59
|
+
*/
|
|
60
|
+
function formatSuggestion(suggestion) {
|
|
61
|
+
return chalk.gray(` Did you mean: ${suggestion}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
findClosestMatch,
|
|
66
|
+
formatSuggestion,
|
|
67
|
+
};
|
package/src/video.js
CHANGED
|
@@ -2,7 +2,6 @@ const { chromium } = require('playwright');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
const { execFileSync } = require('child_process');
|
|
7
6
|
const { pathToFileURL } = require('url');
|
|
8
7
|
const chalk = require('chalk');
|
|
@@ -309,8 +308,8 @@ ${escapedCode}
|
|
|
309
308
|
browser = await chromium.launch({ timeout: 60000 });
|
|
310
309
|
const page = await browser.newPage();
|
|
311
310
|
|
|
312
|
-
const
|
|
313
|
-
tempFile = path.join(
|
|
311
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diagram-svg-'), { mode: 0o700 });
|
|
312
|
+
tempFile = path.join(tempDir, 'diagram.html');
|
|
314
313
|
fs.writeFileSync(tempFile, htmlContent);
|
|
315
314
|
|
|
316
315
|
const fileUrl = pathToFileURL(tempFile).href;
|
|
@@ -362,7 +361,7 @@ ${escapedCode}
|
|
|
362
361
|
browser = null;
|
|
363
362
|
|
|
364
363
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
365
|
-
fs.
|
|
364
|
+
try { fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); } catch (e) {}
|
|
366
365
|
}
|
|
367
366
|
|
|
368
367
|
if (svgContent) {
|
|
@@ -379,7 +378,7 @@ ${escapedCode}
|
|
|
379
378
|
try { await browser.close(); } catch (e) {}
|
|
380
379
|
}
|
|
381
380
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
382
|
-
try { fs.
|
|
381
|
+
try { fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); } catch (e) {}
|
|
383
382
|
}
|
|
384
383
|
throw error;
|
|
385
384
|
}
|