cto-ai-cli 3.1.0 → 4.0.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/DOCS.md +352 -0
- package/README.md +192 -15
- package/dist/action/index.js +629 -83
- package/dist/api/dashboard.js +107 -23
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +108 -24
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +2925 -0
- package/dist/cli/score.js +3015 -237
- package/dist/cli/v2/index.js +133 -49
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +85 -1
- package/dist/engine/index.js +665 -42
- package/dist/engine/index.js.map +1 -1
- package/dist/gateway/index.d.ts +281 -0
- package/dist/gateway/index.js +2803 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/govern/index.d.ts +67 -3
- package/dist/govern/index.js +462 -23
- package/dist/govern/index.js.map +1 -1
- package/dist/interact/index.js +108 -24
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +130 -46
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +3 -2
package/dist/engine/index.js
CHANGED
|
@@ -1360,11 +1360,13 @@ function emptyResult(baseBranch) {
|
|
|
1360
1360
|
}
|
|
1361
1361
|
|
|
1362
1362
|
// src/engine/selector.ts
|
|
1363
|
-
import { createHash as
|
|
1363
|
+
import { createHash as createHash4 } from "crypto";
|
|
1364
1364
|
|
|
1365
1365
|
// src/govern/secrets.ts
|
|
1366
1366
|
import { readFile as readFile4 } from "fs/promises";
|
|
1367
|
-
import {
|
|
1367
|
+
import { readFileSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
1368
|
+
import { resolve as resolve6, relative as relative4, join as join5, dirname as dirname2 } from "path";
|
|
1369
|
+
import { createHash as createHash3 } from "crypto";
|
|
1368
1370
|
var BUILTIN_PATTERNS = [
|
|
1369
1371
|
// API Keys
|
|
1370
1372
|
{ type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
|
|
@@ -1389,15 +1391,66 @@ var BUILTIN_PATTERNS = [
|
|
|
1389
1391
|
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
1390
1392
|
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
1391
1393
|
// Environment variables with secrets
|
|
1392
|
-
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
|
|
1394
|
+
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
|
|
1395
|
+
// Stripe
|
|
1396
|
+
{ type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
|
|
1397
|
+
{ type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
|
|
1398
|
+
{ type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
|
|
1399
|
+
// Slack
|
|
1400
|
+
{ type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
|
|
1401
|
+
{ type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
|
|
1402
|
+
{ type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
|
|
1403
|
+
// Google
|
|
1404
|
+
{ type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
|
|
1405
|
+
{ type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
|
|
1406
|
+
// Azure
|
|
1407
|
+
{ type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
|
|
1408
|
+
// Twilio
|
|
1409
|
+
{ type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
|
|
1410
|
+
// SendGrid
|
|
1411
|
+
{ type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
|
|
1412
|
+
// JWT
|
|
1413
|
+
{ type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
|
|
1414
|
+
// Datadog
|
|
1415
|
+
{ type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
|
|
1416
|
+
{ type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
|
|
1417
|
+
// Sentry
|
|
1418
|
+
{ type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
|
|
1419
|
+
// Firebase
|
|
1420
|
+
{ type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
|
|
1421
|
+
{ type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
|
|
1422
|
+
// Supabase
|
|
1423
|
+
{ type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
|
|
1424
|
+
{ type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
|
|
1425
|
+
// Vercel
|
|
1426
|
+
{ type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
|
|
1427
|
+
// Heroku
|
|
1428
|
+
{ type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
|
|
1429
|
+
// DigitalOcean
|
|
1430
|
+
{ type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
|
|
1431
|
+
{ type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
|
|
1432
|
+
// Mailgun
|
|
1433
|
+
{ type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
|
|
1434
|
+
// PII
|
|
1435
|
+
{ type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
|
|
1436
|
+
{ type: "pii", source: "\\b(?!000|666|9\\d{2})(\\d{3})[-.]?(?!00)(\\d{2})[-.]?(?!0000)(\\d{4})\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
|
|
1393
1437
|
];
|
|
1438
|
+
var _cachedBuiltinPatterns = null;
|
|
1439
|
+
function getBuiltinPatterns() {
|
|
1440
|
+
if (!_cachedBuiltinPatterns) {
|
|
1441
|
+
_cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
|
|
1442
|
+
type: def.type,
|
|
1443
|
+
pattern: new RegExp(def.source, def.flags),
|
|
1444
|
+
severity: def.severity,
|
|
1445
|
+
description: def.description
|
|
1446
|
+
}));
|
|
1447
|
+
}
|
|
1448
|
+
return _cachedBuiltinPatterns;
|
|
1449
|
+
}
|
|
1394
1450
|
function buildPatterns(customPatterns = []) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
severity: def.severity,
|
|
1399
|
-
description: def.description
|
|
1400
|
-
}));
|
|
1451
|
+
const builtins = getBuiltinPatterns();
|
|
1452
|
+
if (customPatterns.length === 0) return builtins;
|
|
1453
|
+
const patterns = [...builtins];
|
|
1401
1454
|
for (const custom of customPatterns) {
|
|
1402
1455
|
try {
|
|
1403
1456
|
patterns.push({
|
|
@@ -1411,7 +1464,7 @@ function buildPatterns(customPatterns = []) {
|
|
|
1411
1464
|
}
|
|
1412
1465
|
return patterns;
|
|
1413
1466
|
}
|
|
1414
|
-
function scanContentForSecrets(content, filePath, customPatterns = []) {
|
|
1467
|
+
function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
|
|
1415
1468
|
const findings = [];
|
|
1416
1469
|
const lines = content.split("\n");
|
|
1417
1470
|
const allPatterns = buildPatterns(customPatterns);
|
|
@@ -1423,6 +1476,7 @@ function scanContentForSecrets(content, filePath, customPatterns = []) {
|
|
|
1423
1476
|
while ((match = secretPattern.pattern.exec(line)) !== null) {
|
|
1424
1477
|
const matchText = match[0];
|
|
1425
1478
|
if (isTemplateOrPlaceholder(matchText)) continue;
|
|
1479
|
+
if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
|
|
1426
1480
|
findings.push({
|
|
1427
1481
|
type: secretPattern.type,
|
|
1428
1482
|
file: filePath,
|
|
@@ -1470,6 +1524,36 @@ function isTemplateOrPlaceholder(value) {
|
|
|
1470
1524
|
];
|
|
1471
1525
|
return placeholders.some((p) => p.test(value));
|
|
1472
1526
|
}
|
|
1527
|
+
var PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1528
|
+
"example.com",
|
|
1529
|
+
"example.org",
|
|
1530
|
+
"example.net",
|
|
1531
|
+
"test.com",
|
|
1532
|
+
"test.org",
|
|
1533
|
+
"test.net",
|
|
1534
|
+
"localhost",
|
|
1535
|
+
"localhost.localdomain",
|
|
1536
|
+
"email.com",
|
|
1537
|
+
"mail.com",
|
|
1538
|
+
"foo.com",
|
|
1539
|
+
"bar.com",
|
|
1540
|
+
"baz.com",
|
|
1541
|
+
"acme.com",
|
|
1542
|
+
"company.com",
|
|
1543
|
+
"corp.com",
|
|
1544
|
+
"noreply.com",
|
|
1545
|
+
"no-reply.com",
|
|
1546
|
+
"users.noreply.github.com",
|
|
1547
|
+
"placeholder.com"
|
|
1548
|
+
]);
|
|
1549
|
+
function isSafeEmail(value, extraDomains) {
|
|
1550
|
+
const match = value.match(/@([a-zA-Z0-9.-]+)$/);
|
|
1551
|
+
if (!match) return false;
|
|
1552
|
+
const domain = match[1].toLowerCase();
|
|
1553
|
+
if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
|
|
1554
|
+
if (extraDomains && extraDomains.has(domain)) return true;
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1473
1557
|
function deduplicateFindings(findings) {
|
|
1474
1558
|
const seen = /* @__PURE__ */ new Set();
|
|
1475
1559
|
return findings.filter((f) => {
|
|
@@ -1483,8 +1567,8 @@ function deduplicateFindings(findings) {
|
|
|
1483
1567
|
// src/engine/pruner.ts
|
|
1484
1568
|
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1485
1569
|
import { readFile as readFile5 } from "fs/promises";
|
|
1486
|
-
import { existsSync as
|
|
1487
|
-
import { join as
|
|
1570
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1571
|
+
import { join as join6 } from "path";
|
|
1488
1572
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
1489
1573
|
async function pruneFile(file, level) {
|
|
1490
1574
|
if (level === "excluded") {
|
|
@@ -1739,9 +1823,9 @@ function addJSDoc(node, parts) {
|
|
|
1739
1823
|
function findTsConfig(filePath) {
|
|
1740
1824
|
let dir = filePath;
|
|
1741
1825
|
for (let i = 0; i < 10; i++) {
|
|
1742
|
-
dir =
|
|
1743
|
-
const candidate =
|
|
1744
|
-
if (
|
|
1826
|
+
dir = join6(dir, "..");
|
|
1827
|
+
const candidate = join6(dir, "tsconfig.json");
|
|
1828
|
+
if (existsSync4(candidate)) return candidate;
|
|
1745
1829
|
}
|
|
1746
1830
|
return void 0;
|
|
1747
1831
|
}
|
|
@@ -2020,7 +2104,7 @@ async function selectContext(input) {
|
|
|
2020
2104
|
);
|
|
2021
2105
|
const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
|
|
2022
2106
|
const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
|
|
2023
|
-
const hash =
|
|
2107
|
+
const hash = createHash4("sha256").update(hashInput).digest("hex").substring(0, 16);
|
|
2024
2108
|
return {
|
|
2025
2109
|
files: selectedFiles,
|
|
2026
2110
|
totalTokens: usedTokens,
|
|
@@ -3168,8 +3252,8 @@ function fmt2(n) {
|
|
|
3168
3252
|
}
|
|
3169
3253
|
|
|
3170
3254
|
// src/engine/predictor.ts
|
|
3171
|
-
import { resolve as resolve8, join as
|
|
3172
|
-
import { readFile as readFile7, writeFile as
|
|
3255
|
+
import { resolve as resolve8, join as join7 } from "path";
|
|
3256
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3173
3257
|
var DEFAULT_PREDICTOR_CONFIG = {
|
|
3174
3258
|
maxCoSelectionPairs: 500,
|
|
3175
3259
|
decayFactor: 0.95,
|
|
@@ -3178,9 +3262,9 @@ var DEFAULT_PREDICTOR_CONFIG = {
|
|
|
3178
3262
|
// need at least 2 observations before predicting
|
|
3179
3263
|
};
|
|
3180
3264
|
async function getModelPath(projectPath) {
|
|
3181
|
-
const ctoDir =
|
|
3265
|
+
const ctoDir = join7(resolve8(projectPath), ".cto");
|
|
3182
3266
|
await mkdir2(ctoDir, { recursive: true });
|
|
3183
|
-
return
|
|
3267
|
+
return join7(ctoDir, "predictor.json");
|
|
3184
3268
|
}
|
|
3185
3269
|
async function loadModel(projectPath) {
|
|
3186
3270
|
try {
|
|
@@ -3193,7 +3277,7 @@ async function loadModel(projectPath) {
|
|
|
3193
3277
|
}
|
|
3194
3278
|
async function saveModel(projectPath, model) {
|
|
3195
3279
|
const path = await getModelPath(projectPath);
|
|
3196
|
-
await
|
|
3280
|
+
await writeFile3(path, JSON.stringify(model, null, 2));
|
|
3197
3281
|
}
|
|
3198
3282
|
function createEmptyModel() {
|
|
3199
3283
|
return {
|
|
@@ -3430,8 +3514,8 @@ function pruneCoSelection(model, maxPairs) {
|
|
|
3430
3514
|
}
|
|
3431
3515
|
|
|
3432
3516
|
// src/engine/cross-repo.ts
|
|
3433
|
-
import { join as
|
|
3434
|
-
import { readFile as readFile8, writeFile as
|
|
3517
|
+
import { join as join8, basename as basename3 } from "path";
|
|
3518
|
+
import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
3435
3519
|
function computeFingerprint2(analysis) {
|
|
3436
3520
|
const totalFiles = analysis.totalFiles;
|
|
3437
3521
|
const sizeClass = totalFiles < 20 ? "tiny" : totalFiles < 100 ? "small" : totalFiles < 500 ? "medium" : totalFiles < 2e3 ? "large" : "huge";
|
|
@@ -3463,7 +3547,7 @@ function fingerprintHash(fp) {
|
|
|
3463
3547
|
}
|
|
3464
3548
|
function getGlobalModelPath() {
|
|
3465
3549
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
3466
|
-
return
|
|
3550
|
+
return join8(home, ".cto", "global-intelligence.json");
|
|
3467
3551
|
}
|
|
3468
3552
|
async function loadGlobalModel() {
|
|
3469
3553
|
try {
|
|
@@ -3474,9 +3558,9 @@ async function loadGlobalModel() {
|
|
|
3474
3558
|
}
|
|
3475
3559
|
}
|
|
3476
3560
|
async function saveGlobalModel(model) {
|
|
3477
|
-
const dir =
|
|
3561
|
+
const dir = join8(getGlobalModelPath(), "..");
|
|
3478
3562
|
await mkdir3(dir, { recursive: true });
|
|
3479
|
-
await
|
|
3563
|
+
await writeFile4(getGlobalModelPath(), JSON.stringify(model, null, 2));
|
|
3480
3564
|
}
|
|
3481
3565
|
function createEmptyModel2() {
|
|
3482
3566
|
return {
|
|
@@ -3695,17 +3779,17 @@ function getCrossRepoStats(model) {
|
|
|
3695
3779
|
}
|
|
3696
3780
|
|
|
3697
3781
|
// src/engine/feedback.ts
|
|
3698
|
-
import { resolve as resolve10, join as
|
|
3699
|
-
import { readFile as readFile9, writeFile as
|
|
3782
|
+
import { resolve as resolve10, join as join9 } from "path";
|
|
3783
|
+
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
3700
3784
|
async function getFeedbackPath(projectPath) {
|
|
3701
|
-
const ctoDir =
|
|
3785
|
+
const ctoDir = join9(resolve10(projectPath), ".cto");
|
|
3702
3786
|
await mkdir4(ctoDir, { recursive: true });
|
|
3703
|
-
return
|
|
3787
|
+
return join9(ctoDir, "feedback.json");
|
|
3704
3788
|
}
|
|
3705
3789
|
async function getModelPath2(projectPath) {
|
|
3706
|
-
const ctoDir =
|
|
3790
|
+
const ctoDir = join9(resolve10(projectPath), ".cto");
|
|
3707
3791
|
await mkdir4(ctoDir, { recursive: true });
|
|
3708
|
-
return
|
|
3792
|
+
return join9(ctoDir, "feedback-model.json");
|
|
3709
3793
|
}
|
|
3710
3794
|
async function loadFeedback(projectPath) {
|
|
3711
3795
|
try {
|
|
@@ -3716,7 +3800,7 @@ async function loadFeedback(projectPath) {
|
|
|
3716
3800
|
}
|
|
3717
3801
|
}
|
|
3718
3802
|
async function saveFeedback(projectPath, entries) {
|
|
3719
|
-
await
|
|
3803
|
+
await writeFile5(await getFeedbackPath(projectPath), JSON.stringify(entries, null, 2));
|
|
3720
3804
|
}
|
|
3721
3805
|
async function loadFeedbackModel(projectPath) {
|
|
3722
3806
|
try {
|
|
@@ -3727,7 +3811,7 @@ async function loadFeedbackModel(projectPath) {
|
|
|
3727
3811
|
}
|
|
3728
3812
|
}
|
|
3729
3813
|
async function saveFeedbackModel(projectPath, model) {
|
|
3730
|
-
await
|
|
3814
|
+
await writeFile5(await getModelPath2(projectPath), JSON.stringify(model, null, 2));
|
|
3731
3815
|
}
|
|
3732
3816
|
function createEmptyModel3() {
|
|
3733
3817
|
return {
|
|
@@ -4402,8 +4486,8 @@ function fmt3(n) {
|
|
|
4402
4486
|
}
|
|
4403
4487
|
|
|
4404
4488
|
// src/engine/compile-proof.ts
|
|
4405
|
-
import { resolve as resolve11, join as
|
|
4406
|
-
import { writeFile as
|
|
4489
|
+
import { resolve as resolve11, join as join10, dirname as dirname3, basename as basename4 } from "path";
|
|
4490
|
+
import { writeFile as writeFile6, mkdir as mkdir5, rm, cp } from "fs/promises";
|
|
4407
4491
|
import { execSync } from "child_process";
|
|
4408
4492
|
async function runCompileProof(analysis, task, budget = 5e4) {
|
|
4409
4493
|
const projectPath = resolve11(analysis.projectPath);
|
|
@@ -4604,21 +4688,21 @@ function extractKnownTypes(analysis, typePaths) {
|
|
|
4604
4688
|
return types;
|
|
4605
4689
|
}
|
|
4606
4690
|
async function runTscWithContext(name, projectPath, selectedPaths, tokensUsed, typePaths, consumerCode) {
|
|
4607
|
-
const tmpDir =
|
|
4691
|
+
const tmpDir = join10(projectPath, ".cto", `compile-proof-${name.toLowerCase()}`);
|
|
4608
4692
|
try {
|
|
4609
4693
|
await rm(tmpDir, { recursive: true, force: true });
|
|
4610
4694
|
await mkdir5(tmpDir, { recursive: true });
|
|
4611
4695
|
for (const filePath of selectedPaths) {
|
|
4612
|
-
const src =
|
|
4613
|
-
const dest =
|
|
4696
|
+
const src = join10(projectPath, filePath);
|
|
4697
|
+
const dest = join10(tmpDir, filePath);
|
|
4614
4698
|
try {
|
|
4615
|
-
await mkdir5(
|
|
4699
|
+
await mkdir5(dirname3(dest), { recursive: true });
|
|
4616
4700
|
await cp(src, dest);
|
|
4617
4701
|
} catch {
|
|
4618
4702
|
}
|
|
4619
4703
|
}
|
|
4620
|
-
await
|
|
4621
|
-
await
|
|
4704
|
+
await writeFile6(join10(tmpDir, "_compile_test.ts"), consumerCode);
|
|
4705
|
+
await writeFile6(join10(tmpDir, "tsconfig.json"), JSON.stringify({
|
|
4622
4706
|
compilerOptions: {
|
|
4623
4707
|
target: "ES2022",
|
|
4624
4708
|
module: "nodenext",
|
|
@@ -4923,9 +5007,541 @@ function fmt5(n) {
|
|
|
4923
5007
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
4924
5008
|
return n.toString();
|
|
4925
5009
|
}
|
|
5010
|
+
|
|
5011
|
+
// src/engine/monorepo.ts
|
|
5012
|
+
import { readFile as readFile11, readdir as readdir4 } from "fs/promises";
|
|
5013
|
+
import { join as join11, relative as relative6, basename as basename5 } from "path";
|
|
5014
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5015
|
+
async function detectMonorepoTool(rootPath) {
|
|
5016
|
+
const checks = [
|
|
5017
|
+
{ file: "nx.json", tool: "nx" },
|
|
5018
|
+
{ file: "turbo.json", tool: "turborepo" },
|
|
5019
|
+
{ file: "lerna.json", tool: "lerna" },
|
|
5020
|
+
{ file: "pnpm-workspace.yaml", tool: "pnpm-workspaces" },
|
|
5021
|
+
{
|
|
5022
|
+
file: "package.json",
|
|
5023
|
+
tool: "npm-workspaces",
|
|
5024
|
+
validate: (content) => {
|
|
5025
|
+
try {
|
|
5026
|
+
const pkg = JSON.parse(content);
|
|
5027
|
+
return Array.isArray(pkg.workspaces) || typeof pkg.workspaces?.packages !== "undefined";
|
|
5028
|
+
} catch {
|
|
5029
|
+
return false;
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
];
|
|
5034
|
+
for (const check of checks) {
|
|
5035
|
+
const filePath = join11(rootPath, check.file);
|
|
5036
|
+
if (existsSync5(filePath)) {
|
|
5037
|
+
if (!check.validate) return check.tool;
|
|
5038
|
+
try {
|
|
5039
|
+
const content = await readFile11(filePath, "utf-8");
|
|
5040
|
+
if (check.validate(content)) {
|
|
5041
|
+
if (check.tool === "npm-workspaces") {
|
|
5042
|
+
if (existsSync5(join11(rootPath, "yarn.lock"))) return "yarn-workspaces";
|
|
5043
|
+
return "npm-workspaces";
|
|
5044
|
+
}
|
|
5045
|
+
return check.tool;
|
|
5046
|
+
}
|
|
5047
|
+
} catch {
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
return "none";
|
|
5052
|
+
}
|
|
5053
|
+
async function resolveWorkspaceGlobs(rootPath, globs) {
|
|
5054
|
+
const packagePaths = [];
|
|
5055
|
+
for (const glob of globs) {
|
|
5056
|
+
const cleanGlob = glob.replace(/\/?\*\*?$/, "");
|
|
5057
|
+
const searchDir = join11(rootPath, cleanGlob);
|
|
5058
|
+
if (!existsSync5(searchDir)) continue;
|
|
5059
|
+
try {
|
|
5060
|
+
const entries = await readdir4(searchDir, { withFileTypes: true });
|
|
5061
|
+
for (const entry of entries) {
|
|
5062
|
+
if (!entry.isDirectory()) continue;
|
|
5063
|
+
const pkgJsonPath = join11(searchDir, entry.name, "package.json");
|
|
5064
|
+
if (existsSync5(pkgJsonPath)) {
|
|
5065
|
+
packagePaths.push(join11(searchDir, entry.name));
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
} catch {
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
return packagePaths;
|
|
5072
|
+
}
|
|
5073
|
+
async function discoverPackages(rootPath, tool) {
|
|
5074
|
+
switch (tool) {
|
|
5075
|
+
case "npm-workspaces":
|
|
5076
|
+
case "yarn-workspaces": {
|
|
5077
|
+
const pkgJson = JSON.parse(await readFile11(join11(rootPath, "package.json"), "utf-8"));
|
|
5078
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5079
|
+
return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5080
|
+
}
|
|
5081
|
+
case "pnpm-workspaces": {
|
|
5082
|
+
const content = await readFile11(join11(rootPath, "pnpm-workspace.yaml"), "utf-8");
|
|
5083
|
+
const packages = [];
|
|
5084
|
+
let inPackages = false;
|
|
5085
|
+
for (const line of content.split("\n")) {
|
|
5086
|
+
const trimmed = line.trim();
|
|
5087
|
+
if (trimmed === "packages:") {
|
|
5088
|
+
inPackages = true;
|
|
5089
|
+
continue;
|
|
5090
|
+
}
|
|
5091
|
+
if (inPackages && trimmed.startsWith("- ")) {
|
|
5092
|
+
packages.push(trimmed.slice(2).replace(/['"]/g, ""));
|
|
5093
|
+
} else if (inPackages && !trimmed.startsWith("-") && trimmed.length > 0) {
|
|
5094
|
+
inPackages = false;
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5098
|
+
}
|
|
5099
|
+
case "turborepo": {
|
|
5100
|
+
const pkgJson = JSON.parse(await readFile11(join11(rootPath, "package.json"), "utf-8"));
|
|
5101
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5102
|
+
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5103
|
+
if (existsSync5(join11(rootPath, "pnpm-workspace.yaml"))) {
|
|
5104
|
+
return discoverPackages(rootPath, "pnpm-workspaces");
|
|
5105
|
+
}
|
|
5106
|
+
return [];
|
|
5107
|
+
}
|
|
5108
|
+
case "nx": {
|
|
5109
|
+
const standardDirs = ["packages", "apps", "libs"];
|
|
5110
|
+
const globs = standardDirs.filter((d) => existsSync5(join11(rootPath, d)));
|
|
5111
|
+
if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
|
|
5112
|
+
try {
|
|
5113
|
+
const pkgJson = JSON.parse(await readFile11(join11(rootPath, "package.json"), "utf-8"));
|
|
5114
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
|
|
5115
|
+
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5116
|
+
} catch {
|
|
5117
|
+
}
|
|
5118
|
+
return [];
|
|
5119
|
+
}
|
|
5120
|
+
case "lerna": {
|
|
5121
|
+
const lernaJson = JSON.parse(await readFile11(join11(rootPath, "lerna.json"), "utf-8"));
|
|
5122
|
+
const packages = lernaJson.packages || ["packages/*"];
|
|
5123
|
+
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5124
|
+
}
|
|
5125
|
+
default:
|
|
5126
|
+
return [];
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
function buildCrossPackageEdges(packages, allFiles, graphEdges, rootPath) {
|
|
5130
|
+
const fileToPackage = /* @__PURE__ */ new Map();
|
|
5131
|
+
for (const pkg of packages) {
|
|
5132
|
+
const pkgRel = relative6(rootPath, pkg.path);
|
|
5133
|
+
for (const f of allFiles) {
|
|
5134
|
+
if (f.relativePath.startsWith(pkgRel + "/") || f.relativePath.startsWith(pkgRel + "\\")) {
|
|
5135
|
+
fileToPackage.set(f.relativePath, pkg.name);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
5140
|
+
for (const edge of graphEdges) {
|
|
5141
|
+
const fromPkg = fileToPackage.get(edge.from);
|
|
5142
|
+
const toPkg = fileToPackage.get(edge.to);
|
|
5143
|
+
if (fromPkg && toPkg && fromPkg !== toPkg) {
|
|
5144
|
+
const key = `${fromPkg}\u2192${toPkg}`;
|
|
5145
|
+
if (!edgeMap.has(key)) {
|
|
5146
|
+
edgeMap.set(key, { files: /* @__PURE__ */ new Set(), type: "dependency" });
|
|
5147
|
+
}
|
|
5148
|
+
edgeMap.get(key).files.add(edge.from);
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
return Array.from(edgeMap.entries()).map(([key, val]) => {
|
|
5152
|
+
const [from, to] = key.split("\u2192");
|
|
5153
|
+
return { from, to, files: val.files.size, type: val.type };
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
async function analyzeMonorepo(rootPath, analysis) {
|
|
5157
|
+
const tool = await detectMonorepoTool(rootPath);
|
|
5158
|
+
if (tool === "none") {
|
|
5159
|
+
return {
|
|
5160
|
+
detected: false,
|
|
5161
|
+
tool: "none",
|
|
5162
|
+
rootPath,
|
|
5163
|
+
packages: [],
|
|
5164
|
+
sharedPackages: [],
|
|
5165
|
+
crossPackageEdges: [],
|
|
5166
|
+
isolationScore: 100,
|
|
5167
|
+
totalTokens: analysis?.totalTokens ?? 0,
|
|
5168
|
+
packageTokenMap: {}
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
const packagePaths = await discoverPackages(rootPath, tool);
|
|
5172
|
+
const packages = [];
|
|
5173
|
+
const packageTokenMap = {};
|
|
5174
|
+
for (const pkgPath of packagePaths) {
|
|
5175
|
+
const pkgJsonPath = join11(pkgPath, "package.json");
|
|
5176
|
+
let name = basename5(pkgPath);
|
|
5177
|
+
let pkgDeps = [];
|
|
5178
|
+
try {
|
|
5179
|
+
const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
|
|
5180
|
+
name = pkgJson.name || name;
|
|
5181
|
+
const allDeps = {
|
|
5182
|
+
...pkgJson.dependencies || {},
|
|
5183
|
+
...pkgJson.devDependencies || {},
|
|
5184
|
+
...pkgJson.peerDependencies || {}
|
|
5185
|
+
};
|
|
5186
|
+
pkgDeps = Object.keys(allDeps);
|
|
5187
|
+
} catch {
|
|
5188
|
+
}
|
|
5189
|
+
const relPath = relative6(rootPath, pkgPath);
|
|
5190
|
+
let fileCount = 0;
|
|
5191
|
+
let tokenCount = 0;
|
|
5192
|
+
const entryPoints = [];
|
|
5193
|
+
if (analysis) {
|
|
5194
|
+
for (const f of analysis.files) {
|
|
5195
|
+
if (f.relativePath.startsWith(relPath + "/") || f.relativePath.startsWith(relPath + "\\")) {
|
|
5196
|
+
fileCount++;
|
|
5197
|
+
tokenCount += f.tokens;
|
|
5198
|
+
if (f.kind === "entry") entryPoints.push(f.relativePath);
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
packageTokenMap[name] = tokenCount;
|
|
5203
|
+
packages.push({
|
|
5204
|
+
name,
|
|
5205
|
+
path: pkgPath,
|
|
5206
|
+
relativePath: relPath,
|
|
5207
|
+
files: fileCount,
|
|
5208
|
+
tokens: tokenCount,
|
|
5209
|
+
dependencies: [],
|
|
5210
|
+
// Filled below after we know all package names
|
|
5211
|
+
dependents: [],
|
|
5212
|
+
isShared: false,
|
|
5213
|
+
entryPoints
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
const pkgNames = new Set(packages.map((p) => p.name));
|
|
5217
|
+
for (const pkg of packages) {
|
|
5218
|
+
const pkgJsonPath = join11(pkg.path, "package.json");
|
|
5219
|
+
try {
|
|
5220
|
+
const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
|
|
5221
|
+
const allDeps = {
|
|
5222
|
+
...pkgJson.dependencies || {},
|
|
5223
|
+
...pkgJson.devDependencies || {}
|
|
5224
|
+
};
|
|
5225
|
+
pkg.dependencies = Object.keys(allDeps).filter((d) => pkgNames.has(d));
|
|
5226
|
+
} catch {
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
for (const pkg of packages) {
|
|
5230
|
+
for (const depName of pkg.dependencies) {
|
|
5231
|
+
const dep = packages.find((p) => p.name === depName);
|
|
5232
|
+
if (dep) dep.dependents.push(pkg.name);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
for (const pkg of packages) {
|
|
5236
|
+
pkg.isShared = pkg.dependents.length >= 2;
|
|
5237
|
+
}
|
|
5238
|
+
const sharedPackages = packages.filter((p) => p.isShared);
|
|
5239
|
+
const crossPackageEdges = analysis ? buildCrossPackageEdges(packages, analysis.files, analysis.graph.edges, rootPath) : [];
|
|
5240
|
+
const maxPossibleEdges = packages.length * (packages.length - 1);
|
|
5241
|
+
const actualEdges = crossPackageEdges.length;
|
|
5242
|
+
const isolationScore = maxPossibleEdges > 0 ? Math.round(100 * (1 - actualEdges / maxPossibleEdges)) : 100;
|
|
5243
|
+
return {
|
|
5244
|
+
detected: true,
|
|
5245
|
+
tool,
|
|
5246
|
+
rootPath,
|
|
5247
|
+
packages,
|
|
5248
|
+
sharedPackages,
|
|
5249
|
+
crossPackageEdges,
|
|
5250
|
+
isolationScore,
|
|
5251
|
+
totalTokens: analysis?.totalTokens ?? packages.reduce((s, p) => s + p.tokens, 0),
|
|
5252
|
+
packageTokenMap
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
function selectPackageContext(monorepo, targetPackage) {
|
|
5256
|
+
const target = monorepo.packages.find((p) => p.name === targetPackage || p.relativePath === targetPackage);
|
|
5257
|
+
if (!target) {
|
|
5258
|
+
return {
|
|
5259
|
+
targetPackage,
|
|
5260
|
+
includedPackages: [],
|
|
5261
|
+
excludedPackages: monorepo.packages.map((p) => p.name),
|
|
5262
|
+
originalTokens: monorepo.totalTokens,
|
|
5263
|
+
optimizedTokens: 0,
|
|
5264
|
+
savedTokens: monorepo.totalTokens,
|
|
5265
|
+
savedPercent: 100
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
const includedNames = /* @__PURE__ */ new Set([target.name]);
|
|
5269
|
+
for (const depName of target.dependencies) {
|
|
5270
|
+
includedNames.add(depName);
|
|
5271
|
+
const dep = monorepo.packages.find((p) => p.name === depName);
|
|
5272
|
+
if (dep) {
|
|
5273
|
+
for (const transDep of dep.dependencies) {
|
|
5274
|
+
const transDepPkg = monorepo.packages.find((p) => p.name === transDep);
|
|
5275
|
+
if (transDepPkg?.isShared) includedNames.add(transDep);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
const includedPackages = Array.from(includedNames);
|
|
5280
|
+
const excludedPackages = monorepo.packages.filter((p) => !includedNames.has(p.name)).map((p) => p.name);
|
|
5281
|
+
const optimizedTokens = monorepo.packages.filter((p) => includedNames.has(p.name)).reduce((s, p) => s + p.tokens, 0);
|
|
5282
|
+
const savedTokens = monorepo.totalTokens - optimizedTokens;
|
|
5283
|
+
const savedPercent = monorepo.totalTokens > 0 ? Math.round(savedTokens / monorepo.totalTokens * 100) : 0;
|
|
5284
|
+
return {
|
|
5285
|
+
targetPackage: target.name,
|
|
5286
|
+
includedPackages,
|
|
5287
|
+
excludedPackages,
|
|
5288
|
+
originalTokens: monorepo.totalTokens,
|
|
5289
|
+
optimizedTokens,
|
|
5290
|
+
savedTokens,
|
|
5291
|
+
savedPercent
|
|
5292
|
+
};
|
|
5293
|
+
}
|
|
5294
|
+
function renderMonorepoAnalysis(mono) {
|
|
5295
|
+
if (!mono.detected) {
|
|
5296
|
+
return " \u2139\uFE0F No monorepo detected (single-package project).\n";
|
|
5297
|
+
}
|
|
5298
|
+
const lines = [];
|
|
5299
|
+
lines.push("");
|
|
5300
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5301
|
+
lines.push(` \u{1F4E6} Monorepo Analysis \u2014 ${mono.tool}`);
|
|
5302
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5303
|
+
lines.push("");
|
|
5304
|
+
lines.push(` Packages: ${mono.packages.length}`);
|
|
5305
|
+
lines.push(` Shared packages: ${mono.sharedPackages.length}`);
|
|
5306
|
+
lines.push(` Isolation score: ${mono.isolationScore}/100`);
|
|
5307
|
+
lines.push(` Total tokens: ${mono.totalTokens.toLocaleString()}`);
|
|
5308
|
+
lines.push("");
|
|
5309
|
+
lines.push(" Package breakdown:");
|
|
5310
|
+
lines.push("");
|
|
5311
|
+
const sorted = [...mono.packages].sort((a, b) => b.tokens - a.tokens);
|
|
5312
|
+
const maxNameLen = Math.max(...sorted.map((p) => p.name.length), 10);
|
|
5313
|
+
for (const pkg of sorted) {
|
|
5314
|
+
const name = pkg.name.padEnd(maxNameLen);
|
|
5315
|
+
const tokens = `${(pkg.tokens / 1e3).toFixed(1)}K`.padStart(8);
|
|
5316
|
+
const files = `${pkg.files} files`.padStart(10);
|
|
5317
|
+
const deps = pkg.dependencies.length > 0 ? ` \u2192 ${pkg.dependencies.join(", ")}` : "";
|
|
5318
|
+
const shared = pkg.isShared ? " [shared]" : "";
|
|
5319
|
+
lines.push(` ${name} ${tokens} ${files}${shared}${deps}`);
|
|
5320
|
+
}
|
|
5321
|
+
if (mono.crossPackageEdges.length > 0) {
|
|
5322
|
+
lines.push("");
|
|
5323
|
+
lines.push(" Cross-package dependencies:");
|
|
5324
|
+
for (const edge of mono.crossPackageEdges.slice(0, 10)) {
|
|
5325
|
+
lines.push(` ${edge.from} \u2192 ${edge.to} (${edge.files} files)`);
|
|
5326
|
+
}
|
|
5327
|
+
if (mono.crossPackageEdges.length > 10) {
|
|
5328
|
+
lines.push(` ... and ${mono.crossPackageEdges.length - 10} more`);
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
lines.push("");
|
|
5332
|
+
lines.push(" \u{1F4A1} Use --monorepo --package <name> to see context savings for a specific package.");
|
|
5333
|
+
lines.push("");
|
|
5334
|
+
return lines.join("\n");
|
|
5335
|
+
}
|
|
5336
|
+
function renderPackageContext(result) {
|
|
5337
|
+
const lines = [];
|
|
5338
|
+
lines.push("");
|
|
5339
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5340
|
+
lines.push(` \u{1F3AF} Package Context \u2014 ${result.targetPackage}`);
|
|
5341
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5342
|
+
lines.push("");
|
|
5343
|
+
lines.push(` Included packages: ${result.includedPackages.length}`);
|
|
5344
|
+
lines.push(` Excluded packages: ${result.excludedPackages.length}`);
|
|
5345
|
+
lines.push("");
|
|
5346
|
+
lines.push(` Original tokens: ${result.originalTokens.toLocaleString()}`);
|
|
5347
|
+
lines.push(` Optimized tokens: ${result.optimizedTokens.toLocaleString()}`);
|
|
5348
|
+
lines.push(` Token savings: ${result.savedTokens.toLocaleString()} (${result.savedPercent}%)`);
|
|
5349
|
+
lines.push("");
|
|
5350
|
+
if (result.includedPackages.length > 0) {
|
|
5351
|
+
lines.push(" \u2705 Included:");
|
|
5352
|
+
for (const p of result.includedPackages) {
|
|
5353
|
+
lines.push(` ${p}`);
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
if (result.excludedPackages.length > 0) {
|
|
5357
|
+
lines.push(" \u2B1C Excluded (not needed for this package):");
|
|
5358
|
+
for (const p of result.excludedPackages.slice(0, 10)) {
|
|
5359
|
+
lines.push(` ${p}`);
|
|
5360
|
+
}
|
|
5361
|
+
if (result.excludedPackages.length > 10) {
|
|
5362
|
+
lines.push(` ... and ${result.excludedPackages.length - 10} more`);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
lines.push("");
|
|
5366
|
+
return lines.join("\n");
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
// src/engine/quality-gate.ts
|
|
5370
|
+
import { readFile as readFile12, writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
|
|
5371
|
+
import { resolve as resolve13 } from "path";
|
|
5372
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5373
|
+
var DEFAULT_GATE_CONFIG = {
|
|
5374
|
+
threshold: 70,
|
|
5375
|
+
failOnSecrets: true,
|
|
5376
|
+
failOnRegression: true,
|
|
5377
|
+
regressionLimit: 5,
|
|
5378
|
+
baselinePath: ".cto/baseline.json",
|
|
5379
|
+
secretSeverities: ["critical", "high"]
|
|
5380
|
+
};
|
|
5381
|
+
async function loadBaseline(projectPath, baselinePath) {
|
|
5382
|
+
const filePath = resolve13(projectPath, baselinePath || ".cto/baseline.json");
|
|
5383
|
+
if (!existsSync6(filePath)) return null;
|
|
5384
|
+
try {
|
|
5385
|
+
const content = await readFile12(filePath, "utf-8");
|
|
5386
|
+
return JSON.parse(content);
|
|
5387
|
+
} catch {
|
|
5388
|
+
return null;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
|
|
5392
|
+
const dir = resolve13(projectPath, ".cto");
|
|
5393
|
+
if (!existsSync6(dir)) await mkdir6(dir, { recursive: true });
|
|
5394
|
+
const baseline = {
|
|
5395
|
+
score: score.overall,
|
|
5396
|
+
grade: score.grade,
|
|
5397
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5398
|
+
commit,
|
|
5399
|
+
branch,
|
|
5400
|
+
dimensions: {
|
|
5401
|
+
efficiency: score.dimensions.efficiency.score,
|
|
5402
|
+
coverage: score.dimensions.coverage.score,
|
|
5403
|
+
riskControl: score.dimensions.riskControl.score,
|
|
5404
|
+
structure: score.dimensions.structure.score,
|
|
5405
|
+
governance: score.dimensions.governance.score
|
|
5406
|
+
}
|
|
5407
|
+
};
|
|
5408
|
+
const filePath = resolve13(projectPath, baselinePath || ".cto/baseline.json");
|
|
5409
|
+
await writeFile7(filePath, JSON.stringify(baseline, null, 2));
|
|
5410
|
+
}
|
|
5411
|
+
async function runQualityGate(score, analysis, secretFindings, config = {}) {
|
|
5412
|
+
const cfg = { ...DEFAULT_GATE_CONFIG, ...config };
|
|
5413
|
+
const checks = [];
|
|
5414
|
+
const baseline = await loadBaseline(analysis.projectPath, cfg.baselinePath);
|
|
5415
|
+
const previousScore = baseline?.score ?? null;
|
|
5416
|
+
const delta = previousScore !== null ? score.overall - previousScore : null;
|
|
5417
|
+
const thresholdPassed = score.overall >= cfg.threshold;
|
|
5418
|
+
checks.push({
|
|
5419
|
+
name: "Score threshold",
|
|
5420
|
+
passed: thresholdPassed,
|
|
5421
|
+
detail: thresholdPassed ? `Score ${score.overall} \u2265 threshold ${cfg.threshold}` : `Score ${score.overall} < threshold ${cfg.threshold}`,
|
|
5422
|
+
severity: thresholdPassed ? "info" : "error"
|
|
5423
|
+
});
|
|
5424
|
+
const dangerousSecrets = secretFindings.filter(
|
|
5425
|
+
(f) => cfg.secretSeverities.includes(f.severity)
|
|
5426
|
+
);
|
|
5427
|
+
const secretsPassed = !cfg.failOnSecrets || dangerousSecrets.length === 0;
|
|
5428
|
+
checks.push({
|
|
5429
|
+
name: "No secrets detected",
|
|
5430
|
+
passed: secretsPassed,
|
|
5431
|
+
detail: secretsPassed ? "No critical/high severity secrets found" : `${dangerousSecrets.length} secret(s) with ${cfg.secretSeverities.join("/")} severity`,
|
|
5432
|
+
severity: secretsPassed ? "info" : "error"
|
|
5433
|
+
});
|
|
5434
|
+
let regressionPassed = true;
|
|
5435
|
+
if (cfg.failOnRegression && delta !== null) {
|
|
5436
|
+
regressionPassed = delta >= -cfg.regressionLimit;
|
|
5437
|
+
}
|
|
5438
|
+
checks.push({
|
|
5439
|
+
name: "No score regression",
|
|
5440
|
+
passed: regressionPassed,
|
|
5441
|
+
detail: delta !== null ? regressionPassed ? `Score changed by ${delta >= 0 ? "+" : ""}${delta} (limit: -${cfg.regressionLimit})` : `Score dropped by ${Math.abs(delta)} points (limit: -${cfg.regressionLimit})` : "No baseline found (first run)",
|
|
5442
|
+
severity: regressionPassed ? delta !== null && delta < 0 ? "warning" : "info" : "error"
|
|
5443
|
+
});
|
|
5444
|
+
const weakDimensions = Object.entries(score.dimensions).filter(([_, d]) => d.score < 50).map(([name]) => name);
|
|
5445
|
+
const dimensionsPassed = weakDimensions.length === 0;
|
|
5446
|
+
checks.push({
|
|
5447
|
+
name: "Dimension health",
|
|
5448
|
+
passed: dimensionsPassed,
|
|
5449
|
+
detail: dimensionsPassed ? "All dimensions above 50%" : `Weak dimensions: ${weakDimensions.join(", ")}`,
|
|
5450
|
+
severity: dimensionsPassed ? "info" : "warning"
|
|
5451
|
+
});
|
|
5452
|
+
const passed = checks.filter((c) => c.severity === "error").every((c) => c.passed);
|
|
5453
|
+
const prComment = generatePRComment(score, analysis, checks, baseline, delta);
|
|
5454
|
+
const summary = generateSummary(score, checks, passed);
|
|
5455
|
+
return {
|
|
5456
|
+
passed,
|
|
5457
|
+
score: score.overall,
|
|
5458
|
+
grade: score.grade,
|
|
5459
|
+
previousScore,
|
|
5460
|
+
delta,
|
|
5461
|
+
checks,
|
|
5462
|
+
baseline,
|
|
5463
|
+
prComment,
|
|
5464
|
+
summary
|
|
5465
|
+
};
|
|
5466
|
+
}
|
|
5467
|
+
function generatePRComment(score, analysis, checks, baseline, delta) {
|
|
5468
|
+
const gradeEmoji = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
|
|
5469
|
+
const allPassed = checks.filter((c) => c.severity === "error").every((c) => c.passed);
|
|
5470
|
+
const statusIcon = allPassed ? "\u2705" : "\u274C";
|
|
5471
|
+
const deltaStr = delta !== null ? ` (${delta >= 0 ? "+" : ""}${delta})` : "";
|
|
5472
|
+
const lines = [
|
|
5473
|
+
`## ${statusIcon} CTO Quality Gate ${allPassed ? "Passed" : "Failed"}`,
|
|
5474
|
+
"",
|
|
5475
|
+
`### ${gradeEmoji} Context Score: ${score.overall}/100 (${score.grade})${deltaStr}`,
|
|
5476
|
+
"",
|
|
5477
|
+
`> **${analysis.projectName}** \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`,
|
|
5478
|
+
"",
|
|
5479
|
+
"### Checks",
|
|
5480
|
+
"",
|
|
5481
|
+
"| Check | Status | Detail |",
|
|
5482
|
+
"|-------|--------|--------|"
|
|
5483
|
+
];
|
|
5484
|
+
for (const check of checks) {
|
|
5485
|
+
const icon = check.passed ? "\u2705" : check.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
5486
|
+
lines.push(`| ${check.name} | ${icon} | ${check.detail} |`);
|
|
5487
|
+
}
|
|
5488
|
+
lines.push("");
|
|
5489
|
+
lines.push("### Dimensions");
|
|
5490
|
+
lines.push("");
|
|
5491
|
+
lines.push("| Dimension | Score | vs Baseline |");
|
|
5492
|
+
lines.push("|-----------|-------|-------------|");
|
|
5493
|
+
for (const [name, dim] of Object.entries(score.dimensions)) {
|
|
5494
|
+
const prev = baseline?.dimensions[name];
|
|
5495
|
+
const diff = prev !== void 0 ? dim.score - prev : null;
|
|
5496
|
+
const diffStr = diff !== null ? `${diff >= 0 ? "+" : ""}${diff}` : "\u2014";
|
|
5497
|
+
const bar = renderBar2(dim.score);
|
|
5498
|
+
lines.push(`| ${name} | ${bar} ${dim.score}% | ${diffStr} |`);
|
|
5499
|
+
}
|
|
5500
|
+
lines.push("");
|
|
5501
|
+
lines.push("### Savings");
|
|
5502
|
+
lines.push("");
|
|
5503
|
+
lines.push(`| Metric | Value |`);
|
|
5504
|
+
lines.push(`|--------|-------|`);
|
|
5505
|
+
lines.push(`| Tokens saved | ${score.comparison.savedTokens.toLocaleString()} (${score.comparison.savedPercent}%) |`);
|
|
5506
|
+
lines.push(`| Monthly savings | $${score.comparison.monthlySavingsUSD.toFixed(2)} |`);
|
|
5507
|
+
if (score.insights.length > 0) {
|
|
5508
|
+
lines.push("");
|
|
5509
|
+
lines.push("### Insights");
|
|
5510
|
+
lines.push("");
|
|
5511
|
+
for (const insight of score.insights.slice(0, 5)) {
|
|
5512
|
+
const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
5513
|
+
lines.push(`- ${icon} **${insight.title}** \u2014 ${insight.detail}`);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
lines.push("");
|
|
5517
|
+
lines.push("---");
|
|
5518
|
+
lines.push(`<sub>Generated by [CTO Quality Gate](https://npmjs.com/package/cto-ai-cli) \xB7 ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}</sub>`);
|
|
5519
|
+
return lines.join("\n");
|
|
5520
|
+
}
|
|
5521
|
+
function renderBar2(score) {
|
|
5522
|
+
const filled = Math.round(score / 10);
|
|
5523
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
5524
|
+
}
|
|
5525
|
+
function generateSummary(score, checks, passed) {
|
|
5526
|
+
const status = passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
5527
|
+
const failedChecks = checks.filter((c) => !c.passed && c.severity === "error");
|
|
5528
|
+
const warnings = checks.filter((c) => !c.passed && c.severity === "warning");
|
|
5529
|
+
let summary = `Quality Gate ${status} \u2014 Score: ${score.overall}/100 (${score.grade})`;
|
|
5530
|
+
if (failedChecks.length > 0) {
|
|
5531
|
+
summary += `
|
|
5532
|
+
Failed: ${failedChecks.map((c) => c.name).join(", ")}`;
|
|
5533
|
+
}
|
|
5534
|
+
if (warnings.length > 0) {
|
|
5535
|
+
summary += `
|
|
5536
|
+
Warnings: ${warnings.map((c) => c.name).join(", ")}`;
|
|
5537
|
+
}
|
|
5538
|
+
return summary;
|
|
5539
|
+
}
|
|
4926
5540
|
export {
|
|
5541
|
+
DEFAULT_GATE_CONFIG,
|
|
4927
5542
|
MODEL_REGISTRY2 as MODEL_REGISTRY,
|
|
4928
5543
|
ProjectWatcher,
|
|
5544
|
+
analyzeMonorepo,
|
|
4929
5545
|
analyzeProject,
|
|
4930
5546
|
analyzeSemantics,
|
|
4931
5547
|
bfsBidirectional,
|
|
@@ -4939,6 +5555,7 @@ export {
|
|
|
4939
5555
|
countTokensChars4,
|
|
4940
5556
|
countTokensTiktoken,
|
|
4941
5557
|
createProject,
|
|
5558
|
+
detectMonorepoTool,
|
|
4942
5559
|
detectStack,
|
|
4943
5560
|
estimateFileTokens,
|
|
4944
5561
|
estimateTokens,
|
|
@@ -4957,6 +5574,7 @@ export {
|
|
|
4957
5574
|
getPruneLevelForRisk,
|
|
4958
5575
|
initProjectConfig,
|
|
4959
5576
|
invalidateCache,
|
|
5577
|
+
loadBaseline,
|
|
4960
5578
|
loadConfig,
|
|
4961
5579
|
loadFeedbackModel,
|
|
4962
5580
|
loadGlobalModel,
|
|
@@ -4977,17 +5595,22 @@ export {
|
|
|
4977
5595
|
renderCompileProof,
|
|
4978
5596
|
renderContextScore,
|
|
4979
5597
|
renderFeedbackReport,
|
|
5598
|
+
renderMonorepoAnalysis,
|
|
4980
5599
|
renderMultiModelResult,
|
|
5600
|
+
renderPackageContext,
|
|
4981
5601
|
renderQualityBenchmark,
|
|
4982
5602
|
renderSemanticAnalysis,
|
|
4983
5603
|
runBenchmark,
|
|
4984
5604
|
runCompilabilityBenchmark,
|
|
4985
5605
|
runCompileProof,
|
|
4986
5606
|
runQualityBenchmark,
|
|
5607
|
+
runQualityGate,
|
|
5608
|
+
saveBaseline,
|
|
4987
5609
|
saveConfig,
|
|
4988
5610
|
scoreAllFiles,
|
|
4989
5611
|
scoreFile,
|
|
4990
5612
|
selectContext,
|
|
5613
|
+
selectPackageContext,
|
|
4991
5614
|
semanticBoosts,
|
|
4992
5615
|
unwatchAll,
|
|
4993
5616
|
unwatchProject,
|