@lifeaitools/rdc-skills 0.34.0 → 0.35.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 (60) hide show
  1. package/.claude-plugin/plugin.json +284 -1
  2. package/VALIDATOR-ARCHITECTURE.md +534 -0
  3. package/commands/analyze-tests.md +11 -0
  4. package/commands/check-clean-code.md +11 -0
  5. package/commands/check-packages.md +10 -0
  6. package/commands/compare-compliance.md +14 -0
  7. package/commands/full-analysis.md +50 -0
  8. package/commands/get-refactoring-plan.md +13 -0
  9. package/commands/quick-check.md +13 -0
  10. package/commands/recover.md +149 -0
  11. package/commands/review-arch.md +12 -0
  12. package/commands/review.md +12 -113
  13. package/commands/suggest-patterns.md +11 -0
  14. package/commands/validate-solid.md +11 -0
  15. package/package.json +14 -2
  16. package/scripts/architecture-score.mjs +157 -0
  17. package/scripts/clean-code-score.mjs +177 -0
  18. package/scripts/duplication-score.mjs +66 -0
  19. package/scripts/lib/architecture-scoring.mjs +695 -0
  20. package/scripts/lib/clean-code-scoring.mjs +258 -0
  21. package/scripts/lib/duplication-scoring.mjs +238 -0
  22. package/scripts/lib/language-plugin.mjs +82 -0
  23. package/scripts/lib/package-metrics.mjs +439 -0
  24. package/scripts/lib/pattern-scoring.mjs +351 -0
  25. package/scripts/lib/plugins/treesitter.mjs +1182 -0
  26. package/scripts/lib/plugins/typescript.mjs +672 -0
  27. package/scripts/lib/refactoring-scoring.mjs +307 -0
  28. package/scripts/lib/solid-scoring.mjs +101 -0
  29. package/scripts/lib/test-smell-scoring.mjs +581 -0
  30. package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
  31. package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
  32. package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
  33. package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
  34. package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
  35. package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
  36. package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
  37. package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
  38. package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
  39. package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
  40. package/scripts/package-metrics-cli.mjs +112 -0
  41. package/scripts/pattern-score.mjs +143 -0
  42. package/scripts/refactoring-score.mjs +253 -0
  43. package/scripts/solid-score.mjs +337 -0
  44. package/skills/architecture-reviewer/SKILL.md +287 -0
  45. package/skills/clean-code-analyzer/SKILL.md +147 -0
  46. package/skills/package-design/SKILL.md +118 -0
  47. package/skills/pattern-advisor/SKILL.md +237 -0
  48. package/skills/pattern-refactoring-guide/SKILL.md +262 -0
  49. package/skills/review/SKILL.md +29 -0
  50. package/skills/solid-validator/SKILL.md +92 -0
  51. package/skills/testing-strategy/SKILL.md +132 -0
  52. package/tests/lib/architecture-scoring.test.mjs +335 -0
  53. package/tests/lib/clean-code-scoring.test.mjs +241 -0
  54. package/tests/lib/duplication-scoring.test.mjs +144 -0
  55. package/tests/lib/fixtures.mjs +58 -0
  56. package/tests/lib/package-metrics.test.mjs +241 -0
  57. package/tests/lib/pattern-scoring.test.mjs +251 -0
  58. package/tests/lib/refactoring-scoring.test.mjs +264 -0
  59. package/tests/lib/solid-scoring.test.mjs +291 -0
  60. package/tests/lib/test-smell-scoring.test.mjs +281 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Package Design metrics — Robert C. Martin's package-level cohesion/coupling
3
+ * suite, operating on PACKAGES (a directory with its own `package.json`, or
4
+ * a declared module boundary passed in explicitly), not files/classes.
5
+ *
6
+ * `solid-validator`'s five scorers (SRP/OCP/LSP/ISP/DIP, driven by
7
+ * `scripts/lib/language-plugin.mjs` + `scripts/lib/plugins/typescript.mjs`)
8
+ * already cover the file/class level. This file is one level up: the
9
+ * package-dependency GRAPH across a set of sibling packages.
10
+ *
11
+ * Deliberately independent of the ts-morph plugin (both being edited
12
+ * concurrently by another agent tonight — no import from either file here).
13
+ * Import extraction is a lightweight regex/text scan over plain source via
14
+ * `node:fs`, not an AST. That is a real, disclosed limitation — see the
15
+ * per-function notes below for exactly what it can and cannot see.
16
+ *
17
+ * ---- Reference check: OnSightTeam/architecture-toolkit (MIT) ----
18
+ *
19
+ * Per operator instruction, checked this implementation's formulas against
20
+ * github.com/OnSightTeam/architecture-toolkit's
21
+ * `src/agents/package-design/tools/{stability-metrics-calculator,
22
+ * package-coupling-analyzer}.ts` (fetched via `gh api` / raw.githubusercontent,
23
+ * MIT per its package.json `license` field and README `## License` section).
24
+ *
25
+ * Confirmed independently, not copied — both are short enough that there is
26
+ * nothing to adapt, only to check against:
27
+ * - D = |A + I − 1| — stability-metrics-calculator.ts:29
28
+ * (`Math.abs(abstractness + stability - 1)`, where their local variable
29
+ * named `stability` is computed as efferent/(efferent+afferent), i.e.
30
+ * Martin's INSTABILITY, not stability — same formula as `distanceFromMainSequence`
31
+ * below, matches Martin's own definition).
32
+ * - Zone-of-Pain / Zone-of-Uselessness split at instability<0.5 && A<0.5 —
33
+ * stability-metrics-calculator.ts:94-96 and package-coupling-analyzer.ts:127
34
+ * — matches the `zone()` classification below.
35
+ *
36
+ * NOT reused — both are genuinely broken, confirmed by reading, not assumed:
37
+ * - Their `countAfferentCoupling` (stability-metrics-calculator.ts:60-75)
38
+ * `continue`s on every file whose path contains the target package name,
39
+ * then re-tests the identical condition on what's left — that branch is
40
+ * dead code. Ca is always 0 there, so I is always 1 whenever Ce>0. Not a
41
+ * real afferent count.
42
+ * - Their ADP cycle walk's `extractDependencies` (package-coupling-analyzer.ts:154-176)
43
+ * keys the whole dependency map off `this.getPackageName('')`, which
44
+ * always resolves to the literal string `'root'` (see its
45
+ * `getPackageName`, ...:149-152, on an empty path) — so cross-file cycle
46
+ * detection never actually walks a real multi-package graph; it only
47
+ * ever inspects the current file's own single-hop relative imports in
48
+ * isolation.
49
+ * - Their abstractness scan counts EVERY `class`/`interface` declaration in
50
+ * a file, exported or not, and has no notion of `type` aliases at all.
51
+ *
52
+ * `buildImportGraph` below resolves relative and bare-specifier imports to
53
+ * actual sibling package directories across the WHOLE package tree (not one
54
+ * file read in isolation), so Ca/Ce and `cycles` here are real multi-file,
55
+ * multi-package graph facts, dogfooded against `rdc-harness/packages/*`
56
+ * (21 packages, cross-checked by hand against
57
+ * `grep -rn "from '\.\./\.\./"` — see the skill doc / task report for the
58
+ * full positive-control table).
59
+ */
60
+
61
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
62
+ import path from 'node:path';
63
+
64
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.next']);
65
+ const SOURCE_EXT = new Set(['.mjs', '.js', '.cjs', '.ts', '.tsx', '.jsx']);
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // File walking
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** @returns {string[]} absolute paths of every source file under dir, recursively */
72
+ function walkSourceFiles(dir) {
73
+ const out = [];
74
+ let entries;
75
+ try {
76
+ entries = readdirSync(dir, { withFileTypes: true });
77
+ } catch {
78
+ return out;
79
+ }
80
+ for (const entry of entries) {
81
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
82
+ const full = path.join(dir, entry.name);
83
+ if (entry.isDirectory()) {
84
+ if (SKIP_DIRS.has(entry.name)) continue;
85
+ out.push(...walkSourceFiles(full));
86
+ } else if (entry.isFile() && SOURCE_EXT.has(path.extname(entry.name))) {
87
+ out.push(full);
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Import/export extraction — plain regex, no AST.
95
+ //
96
+ // Deliberately misses: type-only imports written with unusual whitespace
97
+ // gymnastics, re-exports hidden behind a computed/template specifier,
98
+ // anything generated at runtime (`require(someVar)`). Good enough for the
99
+ // overwhelming majority of ESM/CJS import statements, which is the honest
100
+ // ceiling of a regex approach — this is disclosed, not hidden.
101
+ // ---------------------------------------------------------------------------
102
+
103
+ const IMPORT_FROM_RE = /\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s+['"]([^'"]+)['"]/g;
104
+ const IMPORT_BARE_RE = /\bimport\s+['"]([^'"]+)['"]/g;
105
+ const EXPORT_FROM_RE = /\bexport\s+(?:type\s+)?(?:\*|\{[^}]*\})\s*from\s+['"]([^'"]+)['"]/g;
106
+ const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
107
+ const DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
108
+
109
+ function stripComments(text) {
110
+ // Block comments, then line comments. Simple on purpose: this only needs
111
+ // to avoid matching an import specifier that appears inside a comment
112
+ // (e.g. this file's own doc-comment above, which quotes real import
113
+ // syntax) — it is not a tokenizer.
114
+ return text
115
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
116
+ .replace(/(^|[^:"'])\/\/.*$/gm, '$1');
117
+ }
118
+
119
+ /**
120
+ * @param {string} sourceText
121
+ * @returns {string[]} raw module specifiers this file imports/requires
122
+ */
123
+ export function extractImportSpecifiers(sourceText) {
124
+ const clean = stripComments(sourceText);
125
+ const specs = new Set();
126
+ for (const re of [IMPORT_FROM_RE, IMPORT_BARE_RE, EXPORT_FROM_RE, REQUIRE_RE, DYNAMIC_IMPORT_RE]) {
127
+ re.lastIndex = 0;
128
+ let m;
129
+ while ((m = re.exec(clean))) specs.add(m[1]);
130
+ }
131
+ return [...specs];
132
+ }
133
+
134
+ /**
135
+ * True for a file whose declaration-site exports should NOT count toward
136
+ * package abstractness. Test files routinely embed source-as-STRING fixtures
137
+ * (`` `export interface Page { ... }` `` as a template-literal test input,
138
+ * not a real declaration of the package under test) that a regex scanner
139
+ * cannot distinguish from real code without a full lexer — confirmed live:
140
+ * `rdc-harness/packages/e2e/test/breadth-lifecycle.test.mjs` embeds exactly
141
+ * that fixture and was originally mis-measured as A=0.105 "measured" for a
142
+ * plain-.mjs package with zero real type declarations. Test files also
143
+ * aren't the package's public contract in Martin's sense regardless of the
144
+ * string-literal risk, so excluding them from THIS scan (not from the
145
+ * Ca/Ce import graph, which legitimately counts test-time coupling too) is
146
+ * correct on both grounds, not just a patch for the false positive.
147
+ */
148
+ function isTestFile(filePath) {
149
+ const norm = filePath.replace(/\\/g, '/');
150
+ if (/\/(test|tests|__tests__)\//.test(norm)) return true;
151
+ return /\.(test|spec)\.[a-z]+$/.test(norm);
152
+ }
153
+
154
+ // Declaration-site exports only (`export interface Foo`, `export const Foo = ...`).
155
+ // `export { a, b }` re-export lists are intentionally NOT counted here — they
156
+ // name existing declarations rather than introducing new ones, and counting
157
+ // both would double-count the same declaration under two different exports.
158
+ const EXPORT_DECL_RE =
159
+ /\bexport\s+(?:default\s+)?(?:declare\s+)?(abstract\s+class|interface|type|class|function\s*\*?|const|let|var|enum)\s+([A-Za-z0-9_$]+)/g;
160
+
161
+ /**
162
+ * @param {string} sourceText
163
+ * @returns {{ abstract: number, concrete: number }}
164
+ *
165
+ * NOTE: this does NOT decide measurability. A `.ts` file with zero
166
+ * interfaces/types (all classes/functions/consts) is a real, honest A=0 —
167
+ * "fully concrete" is a legitimate measurement, not an absence of one.
168
+ * Measurability is decided in `buildImportGraph` from the file EXTENSION
169
+ * (does this package contain any `.ts`/`.tsx` source at all), never from
170
+ * whether abstract-shaped keywords happen to appear in it. Gating on
171
+ * keyword presence was tried first and was wrong: it reported a genuine
172
+ * all-concrete TypeScript package (fixture `stable/src/index.ts`, three
173
+ * exported classes/consts, zero interfaces) as "no-type-syntax"/null
174
+ * instead of the correct A=0 — caught by the synthetic Zone-of-Pain/
175
+ * Zone-of-Uselessness fixture run during dogfooding, see the task report.
176
+ */
177
+ export function exportedDeclarationCounts(sourceText) {
178
+ const clean = stripComments(sourceText);
179
+ let abstract = 0;
180
+ let concrete = 0;
181
+ EXPORT_DECL_RE.lastIndex = 0;
182
+ let m;
183
+ while ((m = EXPORT_DECL_RE.exec(clean))) {
184
+ const kind = m[1].trim().replace(/\s*\*$/, '');
185
+ if (kind === 'interface' || kind === 'type') abstract++;
186
+ else concrete++;
187
+ }
188
+ return { abstract, concrete };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Package identity
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * @param {string} dir absolute path to a package directory
197
+ * @returns {{ dir: string, name: string, hasPackageJson: boolean }}
198
+ */
199
+ function packageIdentity(dir) {
200
+ const pkgJsonPath = path.join(dir, 'package.json');
201
+ if (existsSync(pkgJsonPath)) {
202
+ try {
203
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
204
+ if (pkg.name) return { dir, name: pkg.name, hasPackageJson: true };
205
+ } catch {
206
+ // fall through to dirname
207
+ }
208
+ }
209
+ return { dir, name: path.basename(dir), hasPackageJson: existsSync(pkgJsonPath) };
210
+ }
211
+
212
+ function normalize(p) {
213
+ return path.resolve(p).replace(/\\/g, '/').toLowerCase();
214
+ }
215
+
216
+ /**
217
+ * Resolve a module specifier found in `fromFile` to a sibling package, if it
218
+ * points at one.
219
+ *
220
+ * @returns {string|null} the target package's declared name, or null if the
221
+ * specifier doesn't resolve to any of `packages` (npm dependency, node
222
+ * builtin, or an intra-package relative import).
223
+ */
224
+ function resolveToPackage(specifier, fromFile, packages, nameToIndex, dirsSortedByLenDesc) {
225
+ if (specifier.startsWith('.')) {
226
+ const resolved = normalize(path.resolve(path.dirname(fromFile), specifier));
227
+ for (const pkg of dirsSortedByLenDesc) {
228
+ const pdir = normalize(pkg.dir);
229
+ if (resolved === pdir || resolved.startsWith(pdir + '/')) return pkg.name;
230
+ }
231
+ return null;
232
+ }
233
+ // Bare specifier: exact package-name match (`@scope/name`) or, as a
234
+ // fallback, a bare dirname match (`name`) — some monorepos import by
235
+ // dirname via a workspace alias rather than the declared package.json name.
236
+ if (nameToIndex.has(specifier)) return packages[nameToIndex.get(specifier)].name;
237
+ const byDirname = packages.find((p) => path.basename(p.dir) === specifier);
238
+ return byDirname ? byDirname.name : null;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Import graph across the whole sibling set
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * @param {string[]} packageDirs absolute paths
247
+ * @returns {{
248
+ * packages: {dir:string,name:string,hasPackageJson:boolean}[],
249
+ * ceEdges: Map<string, Set<string>>, // pkgName -> set of pkgNames it imports from
250
+ * caEdges: Map<string, Set<string>>, // pkgName -> set of pkgNames that import it
251
+ * abstractness: Map<string, {abstract:number, concrete:number, hasTsFile:boolean}>,
252
+ * }}
253
+ */
254
+ export function buildImportGraph(packageDirs) {
255
+ const packages = packageDirs.map(packageIdentity);
256
+ const dirsSortedByLenDesc = [...packages].sort((a, b) => b.dir.length - a.dir.length);
257
+ const nameToIndex = new Map(packages.map((p, i) => [p.name, i]));
258
+
259
+ const ceEdges = new Map(packages.map((p) => [p.name, new Set()]));
260
+ const caEdges = new Map(packages.map((p) => [p.name, new Set()]));
261
+ const abstractness = new Map(
262
+ packages.map((p) => [p.name, { abstract: 0, concrete: 0, hasTsFile: false }]),
263
+ );
264
+
265
+ for (const pkg of packages) {
266
+ const files = walkSourceFiles(pkg.dir);
267
+ for (const file of files) {
268
+ let text;
269
+ try {
270
+ text = readFileSync(file, 'utf8');
271
+ } catch {
272
+ continue;
273
+ }
274
+
275
+ const specs = extractImportSpecifiers(text);
276
+ for (const spec of specs) {
277
+ const targetName = resolveToPackage(spec, file, packages, nameToIndex, dirsSortedByLenDesc);
278
+ if (!targetName || targetName === pkg.name) continue;
279
+ ceEdges.get(pkg.name).add(targetName);
280
+ caEdges.get(targetName).add(pkg.name);
281
+ }
282
+
283
+ if (isTestFile(file)) continue; // Ca/Ce above already counted this file's real edges
284
+
285
+ const agg = abstractness.get(pkg.name);
286
+ // Measurability is decided by EXTENSION — a `.ts`/`.tsx` file is real
287
+ // TypeScript regardless of whether it happens to declare any
288
+ // interfaces/types. See exportedDeclarationCounts' doc comment for why
289
+ // keyword-sniffing was tried and rejected.
290
+ if (/\.tsx?$/.test(file)) agg.hasTsFile = true;
291
+
292
+ const decl = exportedDeclarationCounts(text);
293
+ agg.abstract += decl.abstract;
294
+ agg.concrete += decl.concrete;
295
+ }
296
+ }
297
+
298
+ return { packages, ceEdges, caEdges, abstractness };
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // ADP — Acyclic Dependencies Principle: real cycle detection over the graph
303
+ // ---------------------------------------------------------------------------
304
+
305
+ /**
306
+ * Find every simple cycle that passes through `startName`, as an actual
307
+ * package-name path (not just "a cycle exists").
308
+ *
309
+ * @param {Map<string, Set<string>>} ceEdges
310
+ * @param {string} startName
311
+ * @returns {string[][]} each entry is a cycle path, e.g. ['a','b','c','a']
312
+ */
313
+ export function findCycles(ceEdges, startName) {
314
+ const cycles = [];
315
+ const stack = [];
316
+ const onStack = new Set();
317
+ const seenCyclesKey = new Set();
318
+
319
+ function dfs(node) {
320
+ stack.push(node);
321
+ onStack.add(node);
322
+ for (const next of ceEdges.get(node) ?? []) {
323
+ if (next === startName) {
324
+ const path = [...stack, startName];
325
+ const key = path.join('>');
326
+ if (!seenCyclesKey.has(key)) {
327
+ seenCyclesKey.add(key);
328
+ cycles.push(path);
329
+ }
330
+ } else if (!onStack.has(next)) {
331
+ dfs(next);
332
+ }
333
+ // if `next` is on the stack but isn't startName, that's a cycle NOT
334
+ // involving startName — out of scope for "cycles involving this
335
+ // package", left for a whole-graph sweep if ever needed.
336
+ }
337
+ stack.pop();
338
+ onStack.delete(node);
339
+ }
340
+
341
+ dfs(startName);
342
+ return cycles;
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Public API
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /**
350
+ * @typedef {object} PackageMetricsResult
351
+ * @property {string} name
352
+ * @property {number} ca - count of OTHER packages that import from this one
353
+ * @property {number} ce - count of OTHER packages this one imports from
354
+ * @property {number|null} instability - Ce/(Ca+Ce); null if Ca+Ce===0 (isolated, no coupling data)
355
+ * @property {number|null} abstractness - exported interface+type / exported total; null if unmeasurable
356
+ * @property {'measured'|'no-type-syntax'|'no-exported-declarations'} abstractnessBasis
357
+ * @property {number|null} distanceFromMainSequence - |A+I-1|; null if either input is null
358
+ * @property {string[][]} cycles - real cycle paths through this package (ADP violations)
359
+ * @property {'main-sequence'|'zone-of-pain'|'zone-of-uselessness'|'off-main-sequence'|'unmeasurable'} zone
360
+ */
361
+
362
+ /**
363
+ * @param {{ packageDir: string, allPackageDirs: string[] }} args
364
+ * @returns {PackageMetricsResult}
365
+ */
366
+ export function packageMetrics({ packageDir, allPackageDirs }) {
367
+ const dirs = allPackageDirs.includes(packageDir) ? allPackageDirs : [...allPackageDirs, packageDir];
368
+ const graph = graphCache(dirs);
369
+ return metricsFor(graph, packageIdentity(packageDir).name);
370
+ }
371
+
372
+ // Cache the graph per unique dir-set within a process — the CLI computes
373
+ // metrics for every sibling package in one run and would otherwise rebuild
374
+ // the identical graph N times.
375
+ const _graphCacheStore = new Map();
376
+ function graphCache(dirs) {
377
+ const key = [...dirs].map((d) => normalize(d)).sort().join('|');
378
+ if (!_graphCacheStore.has(key)) _graphCacheStore.set(key, buildImportGraph(dirs));
379
+ return _graphCacheStore.get(key);
380
+ }
381
+
382
+ function metricsFor(graph, pkgName) {
383
+ const ce = graph.ceEdges.get(pkgName)?.size ?? 0;
384
+ const ca = graph.caEdges.get(pkgName)?.size ?? 0;
385
+ const total = ca + ce;
386
+ const instability = total === 0 ? null : ce / total;
387
+
388
+ const decl = graph.abstractness.get(pkgName) ?? { abstract: 0, concrete: 0, hasTsFile: false };
389
+ let abstractness = null;
390
+ let abstractnessBasis = 'no-type-syntax';
391
+ if (decl.hasTsFile) {
392
+ const declTotal = decl.abstract + decl.concrete;
393
+ if (declTotal === 0) {
394
+ abstractnessBasis = 'no-exported-declarations';
395
+ } else {
396
+ abstractness = decl.abstract / declTotal;
397
+ abstractnessBasis = 'measured';
398
+ }
399
+ }
400
+
401
+ const distanceFromMainSequence =
402
+ instability === null || abstractness === null ? null : Math.abs(abstractness + instability - 1);
403
+
404
+ const cycles = findCycles(graph.ceEdges, pkgName);
405
+
406
+ let zone;
407
+ if (distanceFromMainSequence === null) {
408
+ zone = 'unmeasurable';
409
+ } else if (distanceFromMainSequence <= 0.5) {
410
+ zone = 'main-sequence';
411
+ } else if (instability < 0.5 && abstractness < 0.5) {
412
+ zone = 'zone-of-pain';
413
+ } else if (instability > 0.5 && abstractness > 0.5) {
414
+ zone = 'zone-of-uselessness';
415
+ } else {
416
+ zone = 'off-main-sequence';
417
+ }
418
+
419
+ return {
420
+ name: pkgName,
421
+ ca,
422
+ ce,
423
+ instability,
424
+ abstractness,
425
+ abstractnessBasis,
426
+ distanceFromMainSequence,
427
+ cycles,
428
+ zone,
429
+ };
430
+ }
431
+
432
+ /**
433
+ * @param {string[]} packageDirs absolute paths to every sibling package
434
+ * @returns {PackageMetricsResult[]}
435
+ */
436
+ export function packageMetricsAll(packageDirs) {
437
+ const graph = graphCache(packageDirs);
438
+ return graph.packages.map((p) => metricsFor(graph, p.name));
439
+ }