@aiready/context-analyzer 0.9.26 → 0.9.28
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/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-test.log +21 -20
- package/dist/chunk-FYI56A5M.mjs +1892 -0
- package/dist/chunk-I77HFFZU.mjs +1876 -0
- package/dist/chunk-KYSZF5N6.mjs +1876 -0
- package/dist/chunk-M64RHH4D.mjs +1896 -0
- package/dist/chunk-OP4G6GLN.mjs +1876 -0
- package/dist/chunk-P3T3H27S.mjs +1895 -0
- package/dist/chunk-VBWXHKGD.mjs +1895 -0
- package/dist/cli.js +197 -36
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +202 -32
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +168 -34
- package/src/index.ts +1 -1
- package/src/scoring.ts +28 -1
package/src/analyzer.ts
CHANGED
|
@@ -371,9 +371,9 @@ export function calculatePathEntropy(files: string[]): number {
|
|
|
371
371
|
|
|
372
372
|
const total = counts.reduce((s, v) => s + v, 0);
|
|
373
373
|
let entropy = 0;
|
|
374
|
-
for (const
|
|
375
|
-
const
|
|
376
|
-
entropy -=
|
|
374
|
+
for (const count of counts) {
|
|
375
|
+
const prob = count / total;
|
|
376
|
+
entropy -= prob * Math.log2(prob);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const maxEntropy = Math.log2(counts.length);
|
|
@@ -826,8 +826,8 @@ export function calculateStructuralCohesionFromCoUsage(
|
|
|
826
826
|
|
|
827
827
|
// Calculate entropy
|
|
828
828
|
let entropy = 0;
|
|
829
|
-
for (const
|
|
830
|
-
entropy -=
|
|
829
|
+
for (const prob of probs) {
|
|
830
|
+
entropy -= prob * Math.log2(prob);
|
|
831
831
|
}
|
|
832
832
|
|
|
833
833
|
const maxEntropy = Math.log2(probs.length);
|
|
@@ -892,10 +892,10 @@ function calculateDomainCohesion(exports: ExportInfo[]): number {
|
|
|
892
892
|
const total = domains.length;
|
|
893
893
|
let entropy = 0;
|
|
894
894
|
|
|
895
|
-
for (const
|
|
896
|
-
const
|
|
897
|
-
if (
|
|
898
|
-
entropy -=
|
|
895
|
+
for (const domainCount of domainCounts.values()) {
|
|
896
|
+
const prob = domainCount / total;
|
|
897
|
+
if (prob > 0) {
|
|
898
|
+
entropy -= prob * Math.log2(prob);
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|
|
@@ -946,6 +946,11 @@ export function classifyFile(
|
|
|
946
946
|
return 'lambda-handler';
|
|
947
947
|
}
|
|
948
948
|
|
|
949
|
+
// 4b. Check for data access layer (DAL) files
|
|
950
|
+
if (isDataAccessFile(node)) {
|
|
951
|
+
return 'cohesive-module';
|
|
952
|
+
}
|
|
953
|
+
|
|
949
954
|
// 5. Check for email templates (they reference multiple domains but serve one purpose)
|
|
950
955
|
if (isEmailTemplate(node)) {
|
|
951
956
|
return 'email-template';
|
|
@@ -975,6 +980,14 @@ export function classifyFile(
|
|
|
975
980
|
if (isUtilityFile(node)) {
|
|
976
981
|
return 'utility-module';
|
|
977
982
|
}
|
|
983
|
+
|
|
984
|
+
// Explicit path-based utility heuristic: files under /utils/ or /helpers/
|
|
985
|
+
// should be classified as utility-module regardless of domain count.
|
|
986
|
+
// This ensures common helper modules (e.g., src/utils/dynamodb-utils.ts)
|
|
987
|
+
// are treated as utility modules in tests and analysis.
|
|
988
|
+
if (file.toLowerCase().includes('/utils/') || file.toLowerCase().includes('/helpers/')) {
|
|
989
|
+
return 'utility-module';
|
|
990
|
+
}
|
|
978
991
|
|
|
979
992
|
// 10. Check for cohesive module (single domain + reasonable cohesion)
|
|
980
993
|
const uniqueDomains = domains.filter(d => d !== 'unknown');
|
|
@@ -984,6 +997,12 @@ export function classifyFile(
|
|
|
984
997
|
if (hasSingleDomain) {
|
|
985
998
|
return 'cohesive-module';
|
|
986
999
|
}
|
|
1000
|
+
|
|
1001
|
+
// 10b. Check for shared entity noun despite multi-domain scoring
|
|
1002
|
+
// e.g. getUserReceipts + createPendingReceipt both refer to 'receipt'
|
|
1003
|
+
if (allExportsShareEntityNoun(exports)) {
|
|
1004
|
+
return 'cohesive-module';
|
|
1005
|
+
}
|
|
987
1006
|
|
|
988
1007
|
// 11. Check for mixed concerns (multiple domains + low cohesion)
|
|
989
1008
|
const hasMultipleDomains = uniqueDomains.length > 1;
|
|
@@ -1170,6 +1189,105 @@ function isUtilityFile(node: DependencyNode): boolean {
|
|
|
1170
1189
|
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1171
1190
|
}
|
|
1172
1191
|
|
|
1192
|
+
/**
|
|
1193
|
+
* Split a camelCase or PascalCase identifier into lowercase tokens.
|
|
1194
|
+
* e.g. getUserReceipts -> ['get', 'user', 'receipts']
|
|
1195
|
+
*/
|
|
1196
|
+
function splitCamelCase(name: string): string[] {
|
|
1197
|
+
return name
|
|
1198
|
+
.replace(/([A-Z])/g, ' $1')
|
|
1199
|
+
.trim()
|
|
1200
|
+
.toLowerCase()
|
|
1201
|
+
.split(/[\s_-]+/)
|
|
1202
|
+
.filter(Boolean);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Common English verbs and adjectives to ignore when extracting entity nouns */
|
|
1206
|
+
const SKIP_WORDS = new Set([
|
|
1207
|
+
'get', 'set', 'create', 'update', 'delete', 'fetch', 'save', 'load',
|
|
1208
|
+
'parse', 'format', 'validate', 'convert', 'transform', 'build',
|
|
1209
|
+
'generate', 'render', 'send', 'receive', 'find', 'list', 'add',
|
|
1210
|
+
'remove', 'insert', 'upsert', 'put', 'read', 'write', 'check',
|
|
1211
|
+
'handle', 'process', 'compute', 'calculate', 'init', 'reset', 'clear',
|
|
1212
|
+
'pending', 'active', 'current', 'new', 'old', 'all', 'by', 'with',
|
|
1213
|
+
'from', 'to', 'and', 'or', 'is', 'has', 'in', 'on', 'of', 'the',
|
|
1214
|
+
]);
|
|
1215
|
+
|
|
1216
|
+
/** Singularize a word simply (strip trailing 's') */
|
|
1217
|
+
function simpleSingularize(word: string): string {
|
|
1218
|
+
if (word.endsWith('ies') && word.length > 3) return word.slice(0, -3) + 'y';
|
|
1219
|
+
if (word.endsWith('ses') && word.length > 4) return word.slice(0, -2);
|
|
1220
|
+
if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
|
|
1221
|
+
return word;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Extract meaningful entity nouns from a camelCase/PascalCase function name.
|
|
1226
|
+
* Strips common verbs/adjectives and singularizes remainder.
|
|
1227
|
+
*/
|
|
1228
|
+
function extractEntityNouns(name: string): string[] {
|
|
1229
|
+
return splitCamelCase(name)
|
|
1230
|
+
.filter(token => !SKIP_WORDS.has(token) && token.length > 2)
|
|
1231
|
+
.map(simpleSingularize);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Check whether all exports in a file share at least one common entity noun.
|
|
1236
|
+
* This catches DAL patterns like getUserReceipts + createPendingReceipt → both 'receipt'.
|
|
1237
|
+
*/
|
|
1238
|
+
function allExportsShareEntityNoun(exports: ExportInfo[]): boolean {
|
|
1239
|
+
if (exports.length < 2 || exports.length > 30) return false;
|
|
1240
|
+
|
|
1241
|
+
const nounSets = exports.map(e => new Set(extractEntityNouns(e.name)));
|
|
1242
|
+
if (nounSets.some(s => s.size === 0)) return false;
|
|
1243
|
+
|
|
1244
|
+
// Find nouns that appear in ALL exports
|
|
1245
|
+
const [first, ...rest] = nounSets;
|
|
1246
|
+
const commonNouns = Array.from(first).filter(noun =>
|
|
1247
|
+
rest.every(s => s.has(noun))
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
return commonNouns.length > 0;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Detect if a file is a Data Access Layer (DAL) / repository module.
|
|
1255
|
+
*
|
|
1256
|
+
* Characteristics:
|
|
1257
|
+
* - Named with db, dynamo, database, repository, dao, postgres, mongo patterns
|
|
1258
|
+
* - Or located in /repositories/, /dao/, /data/ directories
|
|
1259
|
+
* - Exports all relate to one data store or entity
|
|
1260
|
+
*/
|
|
1261
|
+
function isDataAccessFile(node: DependencyNode): boolean {
|
|
1262
|
+
const { file, exports } = node;
|
|
1263
|
+
const fileName = file.split('/').pop()?.toLowerCase();
|
|
1264
|
+
|
|
1265
|
+
const dalPatterns = [
|
|
1266
|
+
'dynamo', 'database', 'repository', 'repo', 'dao',
|
|
1267
|
+
'firestore', 'postgres', 'mysql', 'mongo', 'redis',
|
|
1268
|
+
'sqlite', 'supabase', 'prisma',
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
const isDalName = dalPatterns.some(p => fileName?.includes(p));
|
|
1272
|
+
|
|
1273
|
+
const isDalPath = file.toLowerCase().includes('/repositories/') ||
|
|
1274
|
+
file.toLowerCase().includes('/dao/') ||
|
|
1275
|
+
file.toLowerCase().includes('/data/');
|
|
1276
|
+
|
|
1277
|
+
// File with few exports (≤10) that all share a common entity noun
|
|
1278
|
+
const hasDalExportPattern = exports.length >= 1 &&
|
|
1279
|
+
exports.length <= 10 &&
|
|
1280
|
+
allExportsShareEntityNoun(exports);
|
|
1281
|
+
|
|
1282
|
+
// Exclude obvious utility paths from DAL detection (e.g., src/utils/)
|
|
1283
|
+
const isUtilityPathLocal = file.toLowerCase().includes('/utils/') || file.toLowerCase().includes('/helpers/');
|
|
1284
|
+
|
|
1285
|
+
// Only treat as DAL when the file is in a DAL path, or when the name/pattern
|
|
1286
|
+
// indicates a data access module AND exports follow a DAL-like pattern.
|
|
1287
|
+
// Do not classify utility paths as DAL even if the name contains DAL keywords.
|
|
1288
|
+
return isDalPath || (isDalName && hasDalExportPattern && !isUtilityPathLocal);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1173
1291
|
/**
|
|
1174
1292
|
* Detect if a file is a Lambda/API handler
|
|
1175
1293
|
*
|
|
@@ -1193,10 +1311,11 @@ function isLambdaHandler(node: DependencyNode): boolean {
|
|
|
1193
1311
|
fileName?.includes(pattern)
|
|
1194
1312
|
);
|
|
1195
1313
|
|
|
1196
|
-
// Check if file is in a handlers/lambdas/functions directory
|
|
1314
|
+
// Check if file is in a handlers/lambdas/functions/lambda directory
|
|
1197
1315
|
// Exclude /api/ unless it has handler-specific naming
|
|
1198
1316
|
const isHandlerPath = file.toLowerCase().includes('/handlers/') ||
|
|
1199
1317
|
file.toLowerCase().includes('/lambdas/') ||
|
|
1318
|
+
file.toLowerCase().includes('/lambda/') ||
|
|
1200
1319
|
file.toLowerCase().includes('/functions/');
|
|
1201
1320
|
|
|
1202
1321
|
// Check for typical lambda handler exports (handler, main, etc.)
|
|
@@ -1492,56 +1611,49 @@ export function adjustCohesionForClassification(
|
|
|
1492
1611
|
// Type definitions centralize types - high cohesion by nature
|
|
1493
1612
|
return 1;
|
|
1494
1613
|
case 'utility-module': {
|
|
1495
|
-
// Utility modules serve a functional purpose despite multi-domain
|
|
1496
|
-
//
|
|
1614
|
+
// Utility modules serve a functional purpose despite multi-domain.
|
|
1615
|
+
// Use a floor of 0.75 so related utilities never appear as low-cohesion.
|
|
1497
1616
|
if (node) {
|
|
1498
1617
|
const exportNames = node.exports.map(e => e.name.toLowerCase());
|
|
1499
1618
|
const hasRelatedNames = hasRelatedExportNames(exportNames);
|
|
1500
1619
|
if (hasRelatedNames) {
|
|
1501
|
-
|
|
1502
|
-
return Math.min(1, baseCohesion + 0.45);
|
|
1620
|
+
return Math.max(0.80, Math.min(1, baseCohesion + 0.45));
|
|
1503
1621
|
}
|
|
1504
1622
|
}
|
|
1505
|
-
|
|
1506
|
-
return Math.min(1, baseCohesion + 0.35);
|
|
1623
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1507
1624
|
}
|
|
1508
1625
|
case 'service-file': {
|
|
1509
|
-
// Services orchestrate dependencies
|
|
1510
|
-
//
|
|
1626
|
+
// Services orchestrate dependencies by design.
|
|
1627
|
+
// Floor at 0.72 so service files are never flagged as low-cohesion.
|
|
1511
1628
|
if (node?.exports.some(e => e.type === 'class')) {
|
|
1512
|
-
return Math.min(1, baseCohesion + 0.40);
|
|
1629
|
+
return Math.max(0.78, Math.min(1, baseCohesion + 0.40));
|
|
1513
1630
|
}
|
|
1514
|
-
|
|
1515
|
-
return Math.min(1, baseCohesion + 0.30);
|
|
1631
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.30));
|
|
1516
1632
|
}
|
|
1517
1633
|
case 'lambda-handler': {
|
|
1518
|
-
// Lambda handlers have single business purpose
|
|
1519
|
-
// They coordinate multiple services but are entry points
|
|
1634
|
+
// Lambda handlers have single business purpose; floor at 0.75.
|
|
1520
1635
|
if (node) {
|
|
1521
|
-
// Single entry point = cohesive purpose
|
|
1522
1636
|
const hasSingleEntry = node.exports.length === 1 ||
|
|
1523
1637
|
node.exports.some(e => e.name.toLowerCase() === 'handler');
|
|
1524
1638
|
if (hasSingleEntry) {
|
|
1525
|
-
return Math.min(1, baseCohesion + 0.45);
|
|
1639
|
+
return Math.max(0.80, Math.min(1, baseCohesion + 0.45));
|
|
1526
1640
|
}
|
|
1527
1641
|
}
|
|
1528
|
-
return Math.min(1, baseCohesion + 0.35);
|
|
1642
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1529
1643
|
}
|
|
1530
1644
|
case 'email-template': {
|
|
1531
|
-
// Email templates
|
|
1532
|
-
// This is structural cohesion (template purpose)
|
|
1645
|
+
// Email templates are structurally cohesive (single rendering purpose); floor at 0.72.
|
|
1533
1646
|
if (node) {
|
|
1534
|
-
// Check for render/generate functions (template purpose)
|
|
1535
1647
|
const hasTemplateFunc = node.exports.some(e =>
|
|
1536
1648
|
e.name.toLowerCase().includes('render') ||
|
|
1537
1649
|
e.name.toLowerCase().includes('generate') ||
|
|
1538
1650
|
e.name.toLowerCase().includes('template')
|
|
1539
1651
|
);
|
|
1540
1652
|
if (hasTemplateFunc) {
|
|
1541
|
-
return Math.min(1, baseCohesion + 0.40);
|
|
1653
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.40));
|
|
1542
1654
|
}
|
|
1543
1655
|
}
|
|
1544
|
-
return Math.min(1, baseCohesion + 0.30);
|
|
1656
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.30));
|
|
1545
1657
|
}
|
|
1546
1658
|
case 'parser-file': {
|
|
1547
1659
|
// Parsers transform data - single transformation purpose
|
|
@@ -1553,10 +1665,10 @@ export function adjustCohesionForClassification(
|
|
|
1553
1665
|
e.name.toLowerCase().startsWith('convert')
|
|
1554
1666
|
);
|
|
1555
1667
|
if (hasParseFunc) {
|
|
1556
|
-
return Math.min(1, baseCohesion + 0.40);
|
|
1668
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.40));
|
|
1557
1669
|
}
|
|
1558
1670
|
}
|
|
1559
|
-
return Math.min(1, baseCohesion + 0.30);
|
|
1671
|
+
return Math.max(0.70, Math.min(1, baseCohesion + 0.30));
|
|
1560
1672
|
}
|
|
1561
1673
|
case 'nextjs-page':
|
|
1562
1674
|
// Next.js pages have multiple exports by design (metadata, jsonLd, page component)
|
|
@@ -1622,7 +1734,29 @@ function hasRelatedExportNames(exportNames: string[]): boolean {
|
|
|
1622
1734
|
const uniquePrefixes = new Set(prefixes);
|
|
1623
1735
|
if (uniquePrefixes.size === 1) return true;
|
|
1624
1736
|
}
|
|
1625
|
-
|
|
1737
|
+
|
|
1738
|
+
// Check for shared entity noun across all exports using camelCase token splitting
|
|
1739
|
+
// e.g. getUserReceipts + createPendingReceipt both contain 'receipt'
|
|
1740
|
+
const nounSets = exportNames.map(name => {
|
|
1741
|
+
const tokens = name
|
|
1742
|
+
.replace(/([A-Z])/g, ' $1')
|
|
1743
|
+
.trim()
|
|
1744
|
+
.toLowerCase()
|
|
1745
|
+
.split(/[\s_-]+/)
|
|
1746
|
+
.filter(Boolean);
|
|
1747
|
+
const skip = new Set(['get','set','create','update','delete','fetch','save','load',
|
|
1748
|
+
'parse','format','validate','convert','transform','build','generate','render',
|
|
1749
|
+
'send','receive','find','list','add','remove','insert','upsert','put','read',
|
|
1750
|
+
'write','check','handle','process','pending','active','current','new','old','all']);
|
|
1751
|
+
const singularize = (w: string) => w.endsWith('s') && w.length > 3 ? w.slice(0,-1) : w;
|
|
1752
|
+
return new Set(tokens.filter(t => !skip.has(t) && t.length > 2).map(singularize));
|
|
1753
|
+
});
|
|
1754
|
+
if (nounSets.length >= 2 && nounSets.every(s => s.size > 0)) {
|
|
1755
|
+
const [first, ...rest] = nounSets;
|
|
1756
|
+
const commonNouns = Array.from(first).filter(n => rest.every(s => s.has(n)));
|
|
1757
|
+
if (commonNouns.length > 0) return true;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1626
1760
|
return false;
|
|
1627
1761
|
}
|
|
1628
1762
|
|
package/src/index.ts
CHANGED
|
@@ -253,7 +253,7 @@ export async function analyzeContext(
|
|
|
253
253
|
|
|
254
254
|
const cohesionScore =
|
|
255
255
|
focus === 'cohesion' || focus === 'all'
|
|
256
|
-
? calculateCohesion(node.exports, file)
|
|
256
|
+
? calculateCohesion(node.exports, file, { coUsageMatrix: graph.coUsageMatrix })
|
|
257
257
|
: 1;
|
|
258
258
|
|
|
259
259
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
package/src/scoring.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateMonthlyCost,
|
|
3
|
+
calculateProductivityImpact,
|
|
4
|
+
DEFAULT_COST_CONFIG,
|
|
5
|
+
type CostConfig
|
|
6
|
+
} from '@aiready/core';
|
|
1
7
|
import type { ToolScoringOutput } from '@aiready/core';
|
|
2
8
|
import type { ContextSummary } from './types';
|
|
3
9
|
|
|
@@ -9,9 +15,14 @@ import type { ContextSummary } from './types';
|
|
|
9
15
|
* - Import depth (dependency chain length)
|
|
10
16
|
* - Fragmentation score (code organization)
|
|
11
17
|
* - Critical/major issues
|
|
18
|
+
*
|
|
19
|
+
* Includes business value metrics:
|
|
20
|
+
* - Estimated monthly cost of context waste
|
|
21
|
+
* - Estimated developer hours to fix
|
|
12
22
|
*/
|
|
13
23
|
export function calculateContextScore(
|
|
14
|
-
summary: ContextSummary
|
|
24
|
+
summary: ContextSummary,
|
|
25
|
+
costConfig?: Partial<CostConfig>
|
|
15
26
|
): ToolScoringOutput {
|
|
16
27
|
const {
|
|
17
28
|
avgContextBudget,
|
|
@@ -144,6 +155,19 @@ export function calculateContextScore(
|
|
|
144
155
|
});
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
// Calculate business value metrics
|
|
159
|
+
const cfg = { ...DEFAULT_COST_CONFIG, ...costConfig };
|
|
160
|
+
// Total context budget across all files
|
|
161
|
+
const totalContextBudget = avgContextBudget * summary.totalFiles;
|
|
162
|
+
const estimatedMonthlyCost = calculateMonthlyCost(totalContextBudget, cfg);
|
|
163
|
+
|
|
164
|
+
// Convert issues to format for productivity calculation
|
|
165
|
+
const issues = [
|
|
166
|
+
...Array(criticalIssues).fill({ severity: 'critical' as const }),
|
|
167
|
+
...Array(majorIssues).fill({ severity: 'major' as const }),
|
|
168
|
+
];
|
|
169
|
+
const productivityImpact = calculateProductivityImpact(issues);
|
|
170
|
+
|
|
147
171
|
return {
|
|
148
172
|
toolName: 'context-analyzer',
|
|
149
173
|
score,
|
|
@@ -155,6 +179,9 @@ export function calculateContextScore(
|
|
|
155
179
|
avgFragmentation: Math.round(avgFragmentation * 100) / 100,
|
|
156
180
|
criticalIssues,
|
|
157
181
|
majorIssues,
|
|
182
|
+
// Business value metrics
|
|
183
|
+
estimatedMonthlyCost,
|
|
184
|
+
estimatedDeveloperHours: productivityImpact.totalHours,
|
|
158
185
|
},
|
|
159
186
|
factors,
|
|
160
187
|
recommendations,
|