@williamzujkowski/live-coding-music-mcp 3.0.0 → 4.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/README.md +212 -780
- package/dist/AudioAnalyzer.js +1294 -91
- package/dist/PatternStore.js +276 -23
- package/dist/StrudelController.js +388 -56
- package/dist/server/resources.js +27 -20
- package/dist/server/server.js +391 -83
- package/dist/server/tools/ai.js +129 -152
- package/dist/server/tools/analysis.js +170 -91
- package/dist/server/tools/capture.js +128 -55
- package/dist/server/tools/compose.js +28 -42
- package/dist/server/tools/diagnostics.js +23 -53
- package/dist/server/tools/editor.js +63 -72
- package/dist/server/tools/generate.js +45 -167
- package/dist/server/tools/history.js +35 -66
- package/dist/server/tools/playback.js +7 -22
- package/dist/server/tools/session.js +41 -47
- package/dist/server/tools/storage.js +173 -50
- package/dist/server/tools/transform.js +136 -199
- package/dist/server/tools/types.js +209 -14
- package/dist/services/AudioCaptureService.js +302 -33
- package/dist/services/AudioExportService.js +318 -0
- package/dist/services/BrowserOnlyFunctions.js +149 -0
- package/dist/services/EventDensityProbe.js +324 -0
- package/dist/services/GeminiService.js +221 -36
- package/dist/services/IsolatedEngineRunner.js +308 -0
- package/dist/services/IsolatedStrudelEngine.js +135 -0
- package/dist/services/LocalPatternEngine.js +16 -0
- package/dist/services/MIDIExportService.js +765 -71
- package/dist/services/MIDIImportService.js +474 -0
- package/dist/services/MusicTheory.js +85 -13
- package/dist/services/PatternGenerator.js +168 -85
- package/dist/services/PatternSandbox.js +359 -0
- package/dist/services/SessionManager.js +338 -65
- package/dist/services/StrudelEngine.js +218 -12
- package/dist/services/StrudelEngineHelpers.js +24 -8
- package/dist/services/StyleRegistry.js +204 -0
- package/dist/services/Transferable.js +114 -0
- package/dist/services/ai/AiTransport.js +56 -0
- package/dist/services/ai/AudioMeasurements.js +155 -0
- package/dist/services/ai/CliTransport.js +256 -0
- package/dist/services/engineChild.js +83 -0
- package/dist/services/engineChildPath.js +28 -0
- package/dist/utils/CategorisedError.js +59 -0
- package/dist/utils/ErrorRecovery.js +130 -110
- package/dist/utils/InputValidator.js +55 -7
- package/dist/utils/PatternValidator.js +136 -17
- package/dist/utils/SafePath.js +112 -0
- package/dist/utils/ServerConfig.js +248 -0
- package/dist/utils/TableLookup.js +41 -0
- package/dist/utils/Tempo.js +143 -0
- package/package.json +17 -5
- package/dist/services/__mocks__/StrudelEngine.js +0 -157
- package/dist/types/index.js +0 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tempo units, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Strudel's tempo calls count CYCLES, not beats: `setcpm(n)` is n cycles
|
|
5
|
+
* per minute, `setcps(y)` is y cycles per second. Every pattern this
|
|
6
|
+
* project generates or imports is one bar of 4/4 per cycle, so a beat is
|
|
7
|
+
* a quarter of a cycle and the conversion is a factor of four.
|
|
8
|
+
*
|
|
9
|
+
* That factor was wrong in five places before anyone measured it (#395,
|
|
10
|
+
* #397): three writers with three different divisors, and two readers
|
|
11
|
+
* with two different regexes. It lives here now so the next call site
|
|
12
|
+
* inherits the decision instead of re-deriving it.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Beats in one bar: the meter. Everything here is 4/4, and MIDI import
|
|
16
|
+
* lays other meters onto 4/4 bars rather than honouring them (#336).
|
|
17
|
+
*/
|
|
18
|
+
export const BEATS_PER_BAR = 4;
|
|
19
|
+
/**
|
|
20
|
+
* Bars in one cycle: this project's convention, not Strudel's rule.
|
|
21
|
+
*
|
|
22
|
+
* Strudel lets a cycle be any length. Everything generated or imported
|
|
23
|
+
* here puts one bar in it — the generator's drum line is `s("bd*4, ...")`,
|
|
24
|
+
* the hand-written templates spell out eight eighth-notes with the snare
|
|
25
|
+
* on the backbeat, and `renderBars` wraps each bar so one cycle plays
|
|
26
|
+
* one bar.
|
|
27
|
+
*
|
|
28
|
+
* This is the assumption that was wrong three different ways before
|
|
29
|
+
* anyone measured it: one writer assumed one beat per cycle, another two
|
|
30
|
+
* bars, a third got it right. Named separately from the meter because
|
|
31
|
+
* they are separate claims that happen to multiply to four.
|
|
32
|
+
*/
|
|
33
|
+
export const BARS_PER_CYCLE = 1;
|
|
34
|
+
/**
|
|
35
|
+
* Beats in one cycle — the tempo conversion factor.
|
|
36
|
+
*
|
|
37
|
+
* The tests assert the structure this rests on, so changing the bar
|
|
38
|
+
* fails loudly rather than silently rescaling every tempo.
|
|
39
|
+
*/
|
|
40
|
+
export const BEATS_PER_CYCLE = BEATS_PER_BAR * BARS_PER_CYCLE;
|
|
41
|
+
/**
|
|
42
|
+
* A tempo call and its arguments.
|
|
43
|
+
*
|
|
44
|
+
* `setcpm` and `setcps` only. The old `ai.ts` parser also accepted
|
|
45
|
+
* `setbpm`, and a cross-model reviewer called dropping it a silent
|
|
46
|
+
* regression — but `@strudel/core` binds `setCps`/`setcps` and
|
|
47
|
+
* `setCpm`/`setcpm` and nothing else; `setbpm` appears only as a
|
|
48
|
+
* keyword token in `@strudel/mini`'s grammar table. A pattern calling
|
|
49
|
+
* it does not run, so reading a tempo out of it would be inventing one.
|
|
50
|
+
* `StrudelEngineHelpers` had already excluded it on purpose; this makes
|
|
51
|
+
* that one decision instead of two. (`mergeLayerIntoPattern` still
|
|
52
|
+
* matches `setbpm` when it hunts for a tempo prefix — harmless, since
|
|
53
|
+
* it only decides where to splice, but it is the last reference left.)
|
|
54
|
+
*
|
|
55
|
+
* `(?<![\w$.])` so `resetcpm(120)` and `x.setcps(2)` are not tempo
|
|
56
|
+
* calls. Only digits, dots and `/` inside: this parses pattern text
|
|
57
|
+
* that arrived as a tool argument, and it never evaluates it.
|
|
58
|
+
*/
|
|
59
|
+
const TEMPO_CALL = /(?<![\w$.])set(cpm|cps)\s*\(\s*(\d+(?:\.\d+)?(?:\s*\/\s*\d+(?:\.\d+)?)*)\s*\)/i;
|
|
60
|
+
/** Line and block comments, so a commented-out call is not read as one. */
|
|
61
|
+
const COMMENTS = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
|
|
62
|
+
/**
|
|
63
|
+
* The BPM a pattern's tempo call declares, or undefined when it has none.
|
|
64
|
+
*
|
|
65
|
+
* For a divided call this is the NUMERATOR — the number the author wrote
|
|
66
|
+
* — because that is the convention the shipped examples and the
|
|
67
|
+
* generator both follow: `setcpm(130/4)` means "130 BPM in 4/4", and the
|
|
68
|
+
* divisor is the beats-per-cycle conversion, not part of the tempo. Same
|
|
69
|
+
* for `setcps(174/60/4)`.
|
|
70
|
+
*
|
|
71
|
+
* The undivided forms are not symmetric, and deliberately so:
|
|
72
|
+
*
|
|
73
|
+
* `setcpm(120)` -> 120. A bare cycles-per-minute number is what
|
|
74
|
+
* hand-written patterns mean by their tempo, and
|
|
75
|
+
* #341 already settled this.
|
|
76
|
+
* `setcps(2)` -> 480. Nobody writing cycles per SECOND means BPM
|
|
77
|
+
* by it — `setcps(2)` is two cycles a second, and
|
|
78
|
+
* returning 2 as a tempo would be nonsense.
|
|
79
|
+
*
|
|
80
|
+
* Never eval — see `TEMPO_CALL`.
|
|
81
|
+
*/
|
|
82
|
+
export function declaredBpm(code) {
|
|
83
|
+
const parts = tempoParts(code);
|
|
84
|
+
if (parts === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
// A divided call states its author's number first.
|
|
87
|
+
if (parts.values.length > 1)
|
|
88
|
+
return parts.values[0];
|
|
89
|
+
// An undivided one has to be read in its own unit. `cpm` hands back a
|
|
90
|
+
// number a person would recognise as their tempo; `cps` does not, so
|
|
91
|
+
// it converts.
|
|
92
|
+
return parts.unit === 'cps' ? convert(parts) : parts.values[0];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The BPM a pattern's tempo call implies, or undefined when it has none.
|
|
96
|
+
*
|
|
97
|
+
* Works the arithmetic through and converts, so it answers the question
|
|
98
|
+
* `declaredBpm` does not: for a correctly written pattern the two agree,
|
|
99
|
+
* and for the four-times-too-fast calls #395 fixed this one is four
|
|
100
|
+
* times larger. That gap is the whole bug, and a test that only reads
|
|
101
|
+
* the numerator cannot see it.
|
|
102
|
+
*
|
|
103
|
+
* **The tempo call only.** `.slow(2)` and `.fast(2)` rescale time and
|
|
104
|
+
* are not accounted for here — the shipped corpus pairs `setcpm(174/2)`
|
|
105
|
+
* with `.slow(2)` and plays at 174, which this reports as 348. Use it
|
|
106
|
+
* on patterns whose timing lives entirely in the tempo call, which is
|
|
107
|
+
* everything this project generates. `declaredBpm` is the right answer
|
|
108
|
+
* for a pattern someone else wrote.
|
|
109
|
+
*/
|
|
110
|
+
export function impliedBpm(code) {
|
|
111
|
+
const parts = tempoParts(code);
|
|
112
|
+
if (parts === null)
|
|
113
|
+
return undefined;
|
|
114
|
+
return convert(parts);
|
|
115
|
+
}
|
|
116
|
+
/** Work a parsed call through its arithmetic and into beats per minute. */
|
|
117
|
+
function convert(parts) {
|
|
118
|
+
const value = parts.values.reduce((a, b) => a / b);
|
|
119
|
+
const cyclesPerMinute = parts.unit === 'cps' ? value * 60 : value;
|
|
120
|
+
return cyclesPerMinute * BEATS_PER_CYCLE;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Split a tempo call into its unit and its operands.
|
|
124
|
+
*
|
|
125
|
+
* Comments are stripped first. A parser that reads the first `setcpm` in
|
|
126
|
+
* the text will read a commented-out one, and "tempo taken from the
|
|
127
|
+
* wrong place" is the entire subject of #395 and #397 — this file does
|
|
128
|
+
* not get to repeat it.
|
|
129
|
+
*
|
|
130
|
+
* A zero anywhere rejects the whole call, divisor or numerator alike. A
|
|
131
|
+
* zero divisor is nonsense; a zero numerator is a stopped transport,
|
|
132
|
+
* which is not a tempo any caller here can use, and handing back 0 would
|
|
133
|
+
* only push the problem into `validateBPM` further down.
|
|
134
|
+
*/
|
|
135
|
+
function tempoParts(code) {
|
|
136
|
+
const match = TEMPO_CALL.exec(code.replace(COMMENTS, ' '));
|
|
137
|
+
if (match === null)
|
|
138
|
+
return null;
|
|
139
|
+
const values = match[2].split('/').map(part => Number.parseFloat(part.trim()));
|
|
140
|
+
if (values.some(n => !Number.isFinite(n) || n === 0))
|
|
141
|
+
return null;
|
|
142
|
+
return { unit: match[1].toLowerCase(), values };
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamzujkowski/live-coding-music-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "MCP server (unofficial, fan project) for AI-assisted live-coding music via strudel.cc. Lets Claude drive pattern generation, audio analysis, and MIDI export through the Strudel browser REPL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,21 +17,27 @@
|
|
|
17
17
|
"build": "tsc && npx tsx scripts/generate-tool-docs.ts",
|
|
18
18
|
"dev": "tsx watch src/index.ts",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "npm run test:fast && npm run test:browser",
|
|
21
21
|
"test:nocov": "jest",
|
|
22
22
|
"test:watch": "jest --watch",
|
|
23
23
|
"test:integration": "node tests/strudel-integration.js",
|
|
24
|
-
"test:browser": "HEADLESS=true jest --testPathPattern=browser",
|
|
24
|
+
"test:browser": "HEADLESS=true jest --runInBand --forceExit --coverage=false --testPathPattern=__tests__/browser/",
|
|
25
25
|
"lint": "eslint src --ext .ts",
|
|
26
26
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
27
27
|
"docker:build": "docker build -t live-coding-music-mcp .",
|
|
28
28
|
"docker:run": "docker-compose up",
|
|
29
|
-
"prepublishOnly": "npm run build && npm run test:nocov -- --testPathIgnorePatterns=browser",
|
|
29
|
+
"prepublishOnly": "npm run build && npm run test:nocov -- --testPathIgnorePatterns=__tests__/browser/",
|
|
30
30
|
"prepare": "npm run build",
|
|
31
31
|
"validate": "echo '{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}' | node dist/index.js",
|
|
32
32
|
"benchmark": "tsx src/__tests__/benchmarks/latency.benchmark.ts",
|
|
33
33
|
"benchmark:gate": "tsx src/__tests__/benchmarks/latency.benchmark.ts --gate",
|
|
34
|
-
"clean": "rm -rf dist coverage"
|
|
34
|
+
"clean": "rm -rf dist coverage",
|
|
35
|
+
"test:sandbox": "tsx scripts/verify-sandbox.ts",
|
|
36
|
+
"test:envelopes": "npm run build && tsx scripts/verify-envelopes.ts",
|
|
37
|
+
"test:export-audio": "tsx scripts/verify-export-audio.ts",
|
|
38
|
+
"test:ai-transport": "tsx scripts/verify-ai-transport.ts",
|
|
39
|
+
"test:fast": "jest --coverage --testPathIgnorePatterns=__tests__/browser/",
|
|
40
|
+
"calibrate:tempo": "tsx scripts/calibrate-tempo.ts"
|
|
35
41
|
},
|
|
36
42
|
"dependencies": {
|
|
37
43
|
"@google/generative-ai": "^0.24.1",
|
|
@@ -41,6 +47,7 @@
|
|
|
41
47
|
"@strudel/tonal": "1.2.5",
|
|
42
48
|
"@strudel/transpiler": "1.2.5",
|
|
43
49
|
"@tonejs/midi": "^2.0.28",
|
|
50
|
+
"acorn": "8.16.0",
|
|
44
51
|
"google-auth-library": "^10.5.0",
|
|
45
52
|
"playwright": "^1.52.0"
|
|
46
53
|
},
|
|
@@ -82,5 +89,10 @@
|
|
|
82
89
|
"engines": {
|
|
83
90
|
"node": ">=22.0.0",
|
|
84
91
|
"npm": ">=10.0.0"
|
|
92
|
+
},
|
|
93
|
+
"overrides": {
|
|
94
|
+
"js-yaml@^3": "^3.15.1",
|
|
95
|
+
"js-yaml@^4": "^4.3.1",
|
|
96
|
+
"@babel/core": "^7.29.6"
|
|
85
97
|
}
|
|
86
98
|
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock StrudelEngine for Jest testing
|
|
3
|
-
* Provides stub implementations for tests that don't need actual Strudel execution
|
|
4
|
-
*/
|
|
5
|
-
export class StrudelEngine {
|
|
6
|
-
transpile(code) {
|
|
7
|
-
if (!code || code.trim().length === 0) {
|
|
8
|
-
return {
|
|
9
|
-
success: false,
|
|
10
|
-
error: 'Empty pattern',
|
|
11
|
-
errorLocation: { line: 1, column: 1, offset: 0 },
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
// Simple validation - check for basic syntax issues
|
|
15
|
-
if (code.includes('(') && !code.includes(')')) {
|
|
16
|
-
return {
|
|
17
|
-
success: false,
|
|
18
|
-
error: 'Unexpected end of input',
|
|
19
|
-
errorLocation: { line: 1, column: code.length, offset: code.length },
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
success: true,
|
|
24
|
-
transpiledCode: `return /* mock transpiled */ ${code};`,
|
|
25
|
-
locations: [],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
validate(code) {
|
|
29
|
-
const errors = [];
|
|
30
|
-
const warnings = [];
|
|
31
|
-
const suggestions = [];
|
|
32
|
-
if (!code || code.trim().length === 0) {
|
|
33
|
-
return {
|
|
34
|
-
valid: false,
|
|
35
|
-
errors: ['Pattern is empty'],
|
|
36
|
-
warnings: [],
|
|
37
|
-
suggestions: ['Add a pattern like: s("bd hh sd hh")'],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const transpileResult = this.transpile(code);
|
|
41
|
-
if (!transpileResult.success) {
|
|
42
|
-
return {
|
|
43
|
-
valid: false,
|
|
44
|
-
errors: [transpileResult.error || 'Syntax error'],
|
|
45
|
-
warnings,
|
|
46
|
-
suggestions: ['Check pattern syntax'],
|
|
47
|
-
errorLocation: transpileResult.errorLocation
|
|
48
|
-
? { line: transpileResult.errorLocation.line, column: transpileResult.errorLocation.column }
|
|
49
|
-
: undefined,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
// Check for common issues
|
|
53
|
-
const gainMatches = code.match(/\.gain\s*\(\s*(\d+(?:\.\d+)?)\s*\)/g);
|
|
54
|
-
if (gainMatches) {
|
|
55
|
-
for (const match of gainMatches) {
|
|
56
|
-
const value = parseFloat(match.match(/(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
57
|
-
if (value > 2) {
|
|
58
|
-
warnings.push(`High gain value (${value}) may cause distortion`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (!/setcpm|setbpm|cpm|bpm/.test(code)) {
|
|
63
|
-
suggestions.push('Consider setting tempo with setcpm(120)');
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
valid: errors.length === 0,
|
|
67
|
-
errors,
|
|
68
|
-
warnings,
|
|
69
|
-
suggestions,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
queryEvents(code, start, end) {
|
|
73
|
-
const transpileResult = this.transpile(code);
|
|
74
|
-
if (!transpileResult.success) {
|
|
75
|
-
throw new Error(`Transpilation failed: ${transpileResult.error}`);
|
|
76
|
-
}
|
|
77
|
-
// Return mock events based on pattern content
|
|
78
|
-
const events = [];
|
|
79
|
-
const cycles = end - start;
|
|
80
|
-
// Simple mock - return 4 events per cycle for patterns containing s()
|
|
81
|
-
if (code.includes('s(')) {
|
|
82
|
-
const matches = code.match(/s\("([^"]+)"\)/);
|
|
83
|
-
if (matches) {
|
|
84
|
-
const sounds = matches[1].split(' ');
|
|
85
|
-
const eventsPerCycle = sounds.length;
|
|
86
|
-
const duration = 1 / eventsPerCycle;
|
|
87
|
-
for (let cycle = 0; cycle < cycles; cycle++) {
|
|
88
|
-
for (let i = 0; i < sounds.length; i++) {
|
|
89
|
-
events.push({
|
|
90
|
-
value: { s: sounds[i] },
|
|
91
|
-
start: start + cycle + i * duration,
|
|
92
|
-
end: start + cycle + (i + 1) * duration,
|
|
93
|
-
isWhole: true,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return events;
|
|
100
|
-
}
|
|
101
|
-
compile(code) {
|
|
102
|
-
const transpileResult = this.transpile(code);
|
|
103
|
-
if (!transpileResult.success) {
|
|
104
|
-
throw new Error(`Transpilation failed: ${transpileResult.error}`);
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
queryArc: (start, end) => this.queryEvents(code, start, end),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
analyzePattern(code) {
|
|
111
|
-
const metadata = {
|
|
112
|
-
eventsPerCycle: 0,
|
|
113
|
-
uniqueValues: [],
|
|
114
|
-
usesSound: false,
|
|
115
|
-
usesNote: false,
|
|
116
|
-
isStack: false,
|
|
117
|
-
functionsUsed: [],
|
|
118
|
-
complexity: 0,
|
|
119
|
-
};
|
|
120
|
-
// Extract BPM from setcpm
|
|
121
|
-
const bpmMatch = code.match(/setcpm\s*\(\s*(\d+(?:\.\d+)?)\s*\)/);
|
|
122
|
-
if (bpmMatch) {
|
|
123
|
-
metadata.bpm = parseFloat(bpmMatch[1]);
|
|
124
|
-
}
|
|
125
|
-
metadata.usesSound = /\bs\s*\(/.test(code) || /\bsound\s*\(/.test(code);
|
|
126
|
-
metadata.usesNote = /\bnote\s*\(/.test(code);
|
|
127
|
-
metadata.isStack = /\bstack\s*\(/.test(code);
|
|
128
|
-
// Extract function names
|
|
129
|
-
const funcMatches = code.match(/\b([a-z][a-zA-Z0-9_]*)\s*\(/g);
|
|
130
|
-
if (funcMatches) {
|
|
131
|
-
const funcNames = funcMatches.map(m => m.replace(/\s*\($/, ''));
|
|
132
|
-
metadata.functionsUsed = [...new Set(funcNames)];
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const events = this.queryEvents(code, 0, 1);
|
|
136
|
-
metadata.eventsPerCycle = events.length;
|
|
137
|
-
const values = events.map(e => {
|
|
138
|
-
if (typeof e.value === 'object' && e.value !== null) {
|
|
139
|
-
return e.value.s || e.value.note || e.value.n || JSON.stringify(e.value);
|
|
140
|
-
}
|
|
141
|
-
return String(e.value);
|
|
142
|
-
});
|
|
143
|
-
metadata.uniqueValues = [...new Set(values)];
|
|
144
|
-
metadata.complexity = Math.min((events.length / 16) * 0.3 +
|
|
145
|
-
(metadata.uniqueValues.length / 8) * 0.2 +
|
|
146
|
-
(metadata.functionsUsed.length / 10) * 0.3 +
|
|
147
|
-
(metadata.isStack ? 0.1 : 0) +
|
|
148
|
-
(code.length / 500) * 0.1, 1);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
metadata.complexity = Math.min((metadata.functionsUsed.length / 10) * 0.5 +
|
|
152
|
-
(code.length / 500) * 0.3 +
|
|
153
|
-
(metadata.isStack ? 0.2 : 0), 1);
|
|
154
|
-
}
|
|
155
|
-
return metadata;
|
|
156
|
-
}
|
|
157
|
-
}
|
package/dist/types/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|