@kaddo/cli 3.46.0 → 3.48.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/dist/index.js +1011 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -239,6 +239,552 @@ async function runInit() {
|
|
|
239
239
|
outro2("Kaddo initialized.");
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// src/core/scan-signals.ts
|
|
243
|
+
function emptySignals() {
|
|
244
|
+
return {
|
|
245
|
+
auth: [],
|
|
246
|
+
payments: [],
|
|
247
|
+
webhooks: [],
|
|
248
|
+
storage: [],
|
|
249
|
+
background_jobs: [],
|
|
250
|
+
email: [],
|
|
251
|
+
database: [],
|
|
252
|
+
migrations: [],
|
|
253
|
+
api_routes: [],
|
|
254
|
+
tests: [],
|
|
255
|
+
security: [],
|
|
256
|
+
infrastructure: [],
|
|
257
|
+
external_integrations: [],
|
|
258
|
+
environment: []
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function sig(type, label, confidence, evidence, extra) {
|
|
262
|
+
return { type, label, confidence, evidence, ...extra };
|
|
263
|
+
}
|
|
264
|
+
function readDeps(dir) {
|
|
265
|
+
const pkgPath = join(dir, "package.json");
|
|
266
|
+
if (!exists(pkgPath)) return {};
|
|
267
|
+
try {
|
|
268
|
+
const pkg = JSON.parse(readFile(pkgPath));
|
|
269
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
270
|
+
} catch {
|
|
271
|
+
return {};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function findFilesMatching(dir, codeDirs, pattern, maxDepth = 3) {
|
|
275
|
+
const found = [];
|
|
276
|
+
const walk = (base, rel, depth) => {
|
|
277
|
+
if (depth > maxDepth) return;
|
|
278
|
+
try {
|
|
279
|
+
const entries = readDir(join(base, rel));
|
|
280
|
+
for (const e of entries) {
|
|
281
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build" || e === ".next") continue;
|
|
282
|
+
const fullRel = rel ? `${rel}/${e}` : e;
|
|
283
|
+
if (isDir(join(base, fullRel))) {
|
|
284
|
+
walk(base, fullRel, depth + 1);
|
|
285
|
+
} else if (pattern.test(e)) {
|
|
286
|
+
found.push(fullRel);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
for (const cd of codeDirs) {
|
|
293
|
+
walk(dir, cd, 0);
|
|
294
|
+
}
|
|
295
|
+
return found;
|
|
296
|
+
}
|
|
297
|
+
function findPathsContaining(dir, codeDirs, keywords, maxDepth = 3) {
|
|
298
|
+
const found = [];
|
|
299
|
+
const walk = (base, rel, depth) => {
|
|
300
|
+
if (depth > maxDepth) return;
|
|
301
|
+
try {
|
|
302
|
+
const entries = readDir(join(base, rel));
|
|
303
|
+
for (const e of entries) {
|
|
304
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build" || e === ".next") continue;
|
|
305
|
+
const fullRel = rel ? `${rel}/${e}` : e;
|
|
306
|
+
const lower = e.toLowerCase();
|
|
307
|
+
if (keywords.some((k) => lower.includes(k))) {
|
|
308
|
+
found.push(fullRel);
|
|
309
|
+
}
|
|
310
|
+
if (isDir(join(base, fullRel))) {
|
|
311
|
+
walk(base, fullRel, depth + 1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
for (const cd of codeDirs) {
|
|
318
|
+
walk(dir, cd, 0);
|
|
319
|
+
}
|
|
320
|
+
return found;
|
|
321
|
+
}
|
|
322
|
+
function detectAuth(dir, deps, codeDirs, signals) {
|
|
323
|
+
const authDeps = [
|
|
324
|
+
["next-auth", "NextAuth"],
|
|
325
|
+
["@auth/core", "Auth.js"],
|
|
326
|
+
["@supabase/supabase-js", "Supabase Auth"],
|
|
327
|
+
["@supabase/auth-helpers-nextjs", "Supabase Auth"],
|
|
328
|
+
["@supabase/ssr", "Supabase Auth"],
|
|
329
|
+
["@clerk/nextjs", "Clerk"],
|
|
330
|
+
["@clerk/clerk-sdk-node", "Clerk"],
|
|
331
|
+
["firebase", "Firebase Auth"],
|
|
332
|
+
["firebase-admin", "Firebase Auth"],
|
|
333
|
+
["passport", "Passport.js"],
|
|
334
|
+
["jsonwebtoken", "JWT"],
|
|
335
|
+
["jose", "JWT (jose)"],
|
|
336
|
+
["bcrypt", "Password hashing (bcrypt)"],
|
|
337
|
+
["bcryptjs", "Password hashing (bcryptjs)"],
|
|
338
|
+
["@aws-amplify/auth", "AWS Amplify Auth"]
|
|
339
|
+
];
|
|
340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
341
|
+
for (const [dep, label] of authDeps) {
|
|
342
|
+
if (dep in deps && !seen.has(label)) {
|
|
343
|
+
seen.add(label);
|
|
344
|
+
signals.auth.push(sig("auth", label, "high", ["package.json"]));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const authPaths = findPathsContaining(dir, codeDirs, ["auth", "login", "register", "signin", "signup", "session"], 3);
|
|
348
|
+
if (authPaths.length > 0 && signals.auth.length === 0) {
|
|
349
|
+
signals.auth.push(sig("auth", "Auth-related paths detected", "medium", authPaths.slice(0, 5)));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function detectPayments(dir, deps, codeDirs, signals) {
|
|
353
|
+
const payDeps = [
|
|
354
|
+
["stripe", "Stripe"],
|
|
355
|
+
["@stripe/stripe-js", "Stripe"],
|
|
356
|
+
["mercadopago", "Mercado Pago"],
|
|
357
|
+
["@paypal/checkout-server-sdk", "PayPal"],
|
|
358
|
+
["paypal-rest-sdk", "PayPal"],
|
|
359
|
+
["wompi", "Wompi"],
|
|
360
|
+
["@adyen/api-library", "Adyen"],
|
|
361
|
+
["braintree", "Braintree"],
|
|
362
|
+
["square", "Square"]
|
|
363
|
+
];
|
|
364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const [dep, label] of payDeps) {
|
|
366
|
+
if (dep in deps && !seen.has(label)) {
|
|
367
|
+
seen.add(label);
|
|
368
|
+
signals.payments.push(sig("payments", label, "high", ["package.json"]));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const payPaths = findPathsContaining(dir, codeDirs, ["payment", "checkout", "billing", "invoice", "subscription", "pricing"], 3);
|
|
372
|
+
if (payPaths.length > 0 && signals.payments.length === 0) {
|
|
373
|
+
signals.payments.push(sig("payments", "Payment-related paths detected", "medium", payPaths.slice(0, 5)));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function detectWebhooks(dir, codeDirs, signals) {
|
|
377
|
+
const whPaths = findPathsContaining(dir, codeDirs, ["webhook", "callback"], 3);
|
|
378
|
+
if (whPaths.length > 0) {
|
|
379
|
+
signals.webhooks.push(sig("webhooks", "Webhook routes detected", "high", whPaths.slice(0, 5), {
|
|
380
|
+
recommended_review: "Verify idempotency and signature validation."
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function detectStorage(dir, deps, codeDirs, signals) {
|
|
385
|
+
const storageDeps = [
|
|
386
|
+
["@aws-sdk/client-s3", "AWS S3"],
|
|
387
|
+
["aws-sdk", "AWS SDK (includes S3)"],
|
|
388
|
+
["@supabase/storage-js", "Supabase Storage"],
|
|
389
|
+
["cloudinary", "Cloudinary"],
|
|
390
|
+
["multer", "File upload (Multer)"],
|
|
391
|
+
["formidable", "File upload (Formidable)"],
|
|
392
|
+
["@google-cloud/storage", "Google Cloud Storage"],
|
|
393
|
+
["@azure/storage-blob", "Azure Blob Storage"]
|
|
394
|
+
];
|
|
395
|
+
const seen = /* @__PURE__ */ new Set();
|
|
396
|
+
for (const [dep, label] of storageDeps) {
|
|
397
|
+
if (dep in deps && !seen.has(label)) {
|
|
398
|
+
seen.add(label);
|
|
399
|
+
signals.storage.push(sig("storage", label, "high", ["package.json"]));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const storagePaths = findPathsContaining(dir, codeDirs, ["upload", "storage", "bucket"], 3);
|
|
403
|
+
if (storagePaths.length > 0 && signals.storage.length === 0) {
|
|
404
|
+
signals.storage.push(sig("storage", "Storage-related paths detected", "medium", storagePaths.slice(0, 5)));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function detectBackgroundJobs(dir, deps, codeDirs, signals) {
|
|
408
|
+
const jobDeps = [
|
|
409
|
+
["bullmq", "BullMQ"],
|
|
410
|
+
["bull", "Bull"],
|
|
411
|
+
["agenda", "Agenda"],
|
|
412
|
+
["@aws-sdk/client-sqs", "AWS SQS"],
|
|
413
|
+
["amqplib", "RabbitMQ (AMQP)"],
|
|
414
|
+
["bee-queue", "Bee-Queue"],
|
|
415
|
+
["node-cron", "Node-cron"],
|
|
416
|
+
["cron", "Cron"]
|
|
417
|
+
];
|
|
418
|
+
for (const [dep, label] of jobDeps) {
|
|
419
|
+
if (dep in deps) {
|
|
420
|
+
signals.background_jobs.push(sig("background_jobs", label, "high", ["package.json"]));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (isDir(join(dir, "supabase", "functions"))) {
|
|
424
|
+
signals.background_jobs.push(sig("background_jobs", "Supabase Edge Functions", "high", ["supabase/functions/"]));
|
|
425
|
+
}
|
|
426
|
+
const cronMigrations = findFilesMatching(dir, ["supabase/migrations"], /pg_cron|cron/i, 2);
|
|
427
|
+
if (cronMigrations.length > 0) {
|
|
428
|
+
signals.background_jobs.push(sig("background_jobs", "pg_cron scheduled jobs", "high", cronMigrations.slice(0, 3)));
|
|
429
|
+
}
|
|
430
|
+
if (exists(join(dir, "vercel.json"))) {
|
|
431
|
+
try {
|
|
432
|
+
const vc = JSON.parse(readFile(join(dir, "vercel.json")));
|
|
433
|
+
if (vc.crons && Array.isArray(vc.crons) && vc.crons.length > 0) {
|
|
434
|
+
signals.background_jobs.push(sig("background_jobs", "Vercel Cron Jobs", "high", ["vercel.json"]));
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const jobPaths = findPathsContaining(dir, codeDirs, ["cron", "queue", "worker", "job"], 3);
|
|
440
|
+
if (jobPaths.length > 0 && signals.background_jobs.length === 0) {
|
|
441
|
+
signals.background_jobs.push(sig("background_jobs", "Job-related paths detected", "medium", jobPaths.slice(0, 5)));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function detectEmail(dir, deps, signals) {
|
|
445
|
+
const emailDeps = [
|
|
446
|
+
["resend", "Resend"],
|
|
447
|
+
["@sendgrid/mail", "SendGrid"],
|
|
448
|
+
["@sendinblue/client", "Brevo (Sendinblue)"],
|
|
449
|
+
["@getbrevo/brevo", "Brevo"],
|
|
450
|
+
["nodemailer", "Nodemailer"],
|
|
451
|
+
["@aws-sdk/client-ses", "AWS SES"],
|
|
452
|
+
["postmark", "Postmark"],
|
|
453
|
+
["mailgun-js", "Mailgun"],
|
|
454
|
+
["@mailchimp/mailchimp_transactional", "Mailchimp Transactional"]
|
|
455
|
+
];
|
|
456
|
+
for (const [dep, label] of emailDeps) {
|
|
457
|
+
if (dep in deps) {
|
|
458
|
+
signals.email.push(sig("email", label, "high", ["package.json"]));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function detectDatabase(dir, deps, signals) {
|
|
463
|
+
const dbDeps = [
|
|
464
|
+
["prisma", "Prisma"],
|
|
465
|
+
["@prisma/client", "Prisma"],
|
|
466
|
+
["drizzle-orm", "Drizzle ORM"],
|
|
467
|
+
["typeorm", "TypeORM"],
|
|
468
|
+
["sequelize", "Sequelize"],
|
|
469
|
+
["knex", "Knex.js"],
|
|
470
|
+
["@supabase/supabase-js", "Supabase (Postgres)"],
|
|
471
|
+
["pg", "PostgreSQL (pg)"],
|
|
472
|
+
["mysql2", "MySQL"],
|
|
473
|
+
["mongodb", "MongoDB"],
|
|
474
|
+
["mongoose", "Mongoose (MongoDB)"],
|
|
475
|
+
["better-sqlite3", "SQLite"],
|
|
476
|
+
["@neondatabase/serverless", "Neon Postgres"],
|
|
477
|
+
["@planetscale/database", "PlanetScale"],
|
|
478
|
+
["@libsql/client", "Turso (LibSQL)"]
|
|
479
|
+
];
|
|
480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
481
|
+
for (const [dep, label] of dbDeps) {
|
|
482
|
+
if (dep in deps && !seen.has(label)) {
|
|
483
|
+
seen.add(label);
|
|
484
|
+
signals.database.push(sig("database", label, "high", ["package.json"]));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function detectMigrations(dir, migrationDirs, signals) {
|
|
489
|
+
for (const md of migrationDirs) {
|
|
490
|
+
const fullPath = join(dir, md);
|
|
491
|
+
if (!isDir(fullPath)) continue;
|
|
492
|
+
let count = 0;
|
|
493
|
+
try {
|
|
494
|
+
count = readDir(fullPath).filter((f) => !f.startsWith(".")).length;
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
signals.migrations.push(sig("migrations", `${md} (${count} file${count === 1 ? "" : "s"})`, "high", [md]));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function detectApiRoutes(dir, codeDirs, signals) {
|
|
501
|
+
for (const cd of codeDirs) {
|
|
502
|
+
const apiDir = join(dir, cd, "app", "api");
|
|
503
|
+
if (isDir(apiDir)) {
|
|
504
|
+
signals.api_routes.push(sig("api_routes", "Next.js API routes", "high", [`${cd}/app/api`]));
|
|
505
|
+
}
|
|
506
|
+
const pagesApi = join(dir, cd, "pages", "api");
|
|
507
|
+
if (isDir(pagesApi)) {
|
|
508
|
+
signals.api_routes.push(sig("api_routes", "Next.js Pages API routes", "high", [`${cd}/pages/api`]));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (isDir(join(dir, "app", "api"))) {
|
|
512
|
+
signals.api_routes.push(sig("api_routes", "Next.js API routes", "high", ["app/api"]));
|
|
513
|
+
}
|
|
514
|
+
if (isDir(join(dir, "pages", "api"))) {
|
|
515
|
+
signals.api_routes.push(sig("api_routes", "Next.js Pages API routes", "high", ["pages/api"]));
|
|
516
|
+
}
|
|
517
|
+
if (isDir(join(dir, "routes"))) {
|
|
518
|
+
signals.api_routes.push(sig("api_routes", "Routes directory", "medium", ["routes/"]));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function detectTests(dir, testDirs, deps, signals) {
|
|
522
|
+
const testFrameworks = [
|
|
523
|
+
["vitest", "Vitest"],
|
|
524
|
+
["jest", "Jest"],
|
|
525
|
+
["@playwright/test", "Playwright"],
|
|
526
|
+
["cypress", "Cypress"],
|
|
527
|
+
["@testing-library/react", "Testing Library"],
|
|
528
|
+
["mocha", "Mocha"],
|
|
529
|
+
["ava", "AVA"]
|
|
530
|
+
];
|
|
531
|
+
for (const [dep, label] of testFrameworks) {
|
|
532
|
+
if (dep in deps) {
|
|
533
|
+
signals.tests.push(sig("tests", label, "high", ["package.json"]));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (testDirs.length > 0) {
|
|
537
|
+
signals.tests.push(sig("tests", `Test directories: ${testDirs.join(", ")}`, "high", testDirs));
|
|
538
|
+
}
|
|
539
|
+
if (testDirs.length === 0 && signals.tests.length === 0) {
|
|
540
|
+
signals.tests.push(sig("tests", "No test directory detected", "high", [], {
|
|
541
|
+
recommended_review: "Confirm how this project is validated before production changes."
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function detectSecurity(dir, deps, codeDirs, envVarNames, signals) {
|
|
546
|
+
const secDeps = [
|
|
547
|
+
["helmet", "Helmet (security headers)"],
|
|
548
|
+
["cors", "CORS middleware"],
|
|
549
|
+
["csurf", "CSRF protection"],
|
|
550
|
+
["express-rate-limit", "Rate limiting"],
|
|
551
|
+
["@upstash/ratelimit", "Upstash Rate Limiting"]
|
|
552
|
+
];
|
|
553
|
+
for (const [dep, label] of secDeps) {
|
|
554
|
+
if (dep in deps) {
|
|
555
|
+
signals.security.push(sig("security", label, "high", ["package.json"]));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const middlewarePaths = findPathsContaining(dir, codeDirs, ["middleware"], 2);
|
|
559
|
+
if (middlewarePaths.length > 0) {
|
|
560
|
+
signals.security.push(sig("security", "Middleware detected", "medium", middlewarePaths.slice(0, 5)));
|
|
561
|
+
}
|
|
562
|
+
if (isFile(join(dir, "middleware.ts")) || isFile(join(dir, "middleware.js")) || isFile(join(dir, "src", "middleware.ts")) || isFile(join(dir, "src", "middleware.js"))) {
|
|
563
|
+
const evidence = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"].filter((f) => isFile(join(dir, f)));
|
|
564
|
+
if (evidence.length > 0) {
|
|
565
|
+
signals.security.push(sig("security", "Next.js middleware", "high", evidence));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const rlsMigrations = findFilesMatching(dir, ["supabase/migrations"], /rls|policies|policy/i, 2);
|
|
569
|
+
if (rlsMigrations.length > 0) {
|
|
570
|
+
signals.security.push(sig("security", "RLS / row-level security policies", "high", rlsMigrations.slice(0, 3)));
|
|
571
|
+
}
|
|
572
|
+
const secretVars = envVarNames.filter((v) => /secret|token|key|password|credential/i.test(v));
|
|
573
|
+
if (secretVars.length > 0) {
|
|
574
|
+
signals.security.push(sig("security", "Secret-bearing environment variables", "medium", secretVars.slice(0, 10), {
|
|
575
|
+
recommended_review: "Confirm secret rotation and endpoint protection strategy."
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function detectInfrastructure(dir, infraFiles, signals) {
|
|
580
|
+
for (const f of infraFiles) {
|
|
581
|
+
const label = inferInfraLabel(f);
|
|
582
|
+
signals.infrastructure.push(sig("infrastructure", label, "high", [f]));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function inferInfraLabel(f) {
|
|
586
|
+
const lower = f.toLowerCase();
|
|
587
|
+
if (lower.includes("docker-compose")) return "Docker Compose";
|
|
588
|
+
if (lower === "dockerfile") return "Dockerfile";
|
|
589
|
+
if (lower.includes("serverless")) return "Serverless Framework";
|
|
590
|
+
if (lower.includes("amplify")) return "AWS Amplify";
|
|
591
|
+
if (lower.includes("terraform")) return "Terraform";
|
|
592
|
+
if (lower.includes("k8s") || lower.includes("kubernetes")) return "Kubernetes";
|
|
593
|
+
if (lower.includes("helm")) return "Helm";
|
|
594
|
+
if (lower.includes(".github/workflows")) return "GitHub Actions";
|
|
595
|
+
if (lower.includes("vercel")) return "Vercel";
|
|
596
|
+
if (lower.includes("netlify")) return "Netlify";
|
|
597
|
+
return f;
|
|
598
|
+
}
|
|
599
|
+
function detectExternalIntegrations(deps, signals) {
|
|
600
|
+
const integrations = [
|
|
601
|
+
["@supabase/supabase-js", "Supabase"],
|
|
602
|
+
["@aws-sdk/client-s3", "AWS"],
|
|
603
|
+
["aws-sdk", "AWS"],
|
|
604
|
+
["@google-cloud/storage", "Google Cloud"],
|
|
605
|
+
["@azure/storage-blob", "Azure"],
|
|
606
|
+
["twilio", "Twilio"],
|
|
607
|
+
["@slack/web-api", "Slack"],
|
|
608
|
+
["@slack/bolt", "Slack"],
|
|
609
|
+
["@notionhq/client", "Notion"],
|
|
610
|
+
["@octokit/rest", "GitHub API"],
|
|
611
|
+
["@sentry/node", "Sentry"],
|
|
612
|
+
["@sentry/nextjs", "Sentry"],
|
|
613
|
+
["@datadog/datadog-api-client", "Datadog"],
|
|
614
|
+
["newrelic", "New Relic"],
|
|
615
|
+
["@segment/analytics-node", "Segment"],
|
|
616
|
+
["posthog-node", "PostHog"],
|
|
617
|
+
["mixpanel", "Mixpanel"],
|
|
618
|
+
["@amplitude/node", "Amplitude"],
|
|
619
|
+
["firebase-admin", "Firebase"],
|
|
620
|
+
["@google/generative-ai", "Google AI"],
|
|
621
|
+
["openai", "OpenAI"],
|
|
622
|
+
["@anthropic-ai/sdk", "Anthropic"],
|
|
623
|
+
["@vercel/analytics", "Vercel Analytics"],
|
|
624
|
+
["@vercel/kv", "Vercel KV"],
|
|
625
|
+
["@upstash/redis", "Upstash Redis"],
|
|
626
|
+
["ioredis", "Redis"]
|
|
627
|
+
];
|
|
628
|
+
const seen = /* @__PURE__ */ new Set();
|
|
629
|
+
for (const [dep, label] of integrations) {
|
|
630
|
+
if (dep in deps && !seen.has(label)) {
|
|
631
|
+
seen.add(label);
|
|
632
|
+
signals.external_integrations.push(sig("external_integrations", label, "high", ["package.json"]));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function detectEnvironment(dir) {
|
|
637
|
+
const envFiles = [".env.example", ".env.local.example", ".env.template", ".env.sample"];
|
|
638
|
+
const varNames = /* @__PURE__ */ new Set();
|
|
639
|
+
for (const ef of envFiles) {
|
|
640
|
+
const p2 = join(dir, ef);
|
|
641
|
+
if (!exists(p2)) continue;
|
|
642
|
+
try {
|
|
643
|
+
const lines = readFile(p2).split(/\r?\n/);
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
const m = line.match(/^([A-Z][A-Z0-9_]+)\s*=/);
|
|
646
|
+
if (m) varNames.add(m[1]);
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const envPath = join(dir, ".env");
|
|
652
|
+
if (exists(envPath)) {
|
|
653
|
+
try {
|
|
654
|
+
const lines = readFile(envPath).split(/\r?\n/);
|
|
655
|
+
for (const line of lines) {
|
|
656
|
+
const m = line.match(/^([A-Z][A-Z0-9_]+)\s*=/);
|
|
657
|
+
if (m) varNames.add(m[1]);
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return [...varNames].sort();
|
|
663
|
+
}
|
|
664
|
+
function buildScanSignals(dir, codeDirs, migrationDirs, testDirs, infraFiles) {
|
|
665
|
+
const signals = emptySignals();
|
|
666
|
+
const deps = readDeps(dir);
|
|
667
|
+
const envVarNames = detectEnvironment(dir);
|
|
668
|
+
detectAuth(dir, deps, codeDirs, signals);
|
|
669
|
+
detectPayments(dir, deps, codeDirs, signals);
|
|
670
|
+
detectWebhooks(dir, codeDirs, signals);
|
|
671
|
+
detectStorage(dir, deps, codeDirs, signals);
|
|
672
|
+
detectBackgroundJobs(dir, deps, codeDirs, signals);
|
|
673
|
+
detectEmail(dir, deps, signals);
|
|
674
|
+
detectDatabase(dir, deps, signals);
|
|
675
|
+
detectMigrations(dir, migrationDirs, signals);
|
|
676
|
+
detectApiRoutes(dir, codeDirs, signals);
|
|
677
|
+
detectTests(dir, testDirs, deps, signals);
|
|
678
|
+
detectSecurity(dir, deps, codeDirs, envVarNames, signals);
|
|
679
|
+
detectInfrastructure(dir, infraFiles, signals);
|
|
680
|
+
detectExternalIntegrations(deps, signals);
|
|
681
|
+
if (envVarNames.length > 0) {
|
|
682
|
+
signals.environment.push(sig("environment", `${envVarNames.length} environment variable(s) detected`, "high", envVarNames.slice(0, 20)));
|
|
683
|
+
}
|
|
684
|
+
return signals;
|
|
685
|
+
}
|
|
686
|
+
function signalCount(signals) {
|
|
687
|
+
return Object.values(signals).reduce((sum, arr) => sum + arr.length, 0);
|
|
688
|
+
}
|
|
689
|
+
function hasWarnings(signals) {
|
|
690
|
+
return signals.tests.some((s) => s.label.includes("No test directory"));
|
|
691
|
+
}
|
|
692
|
+
function renderSignalsCompact(signals) {
|
|
693
|
+
const lines = [];
|
|
694
|
+
const CATEGORIES = [
|
|
695
|
+
["auth", "Auth"],
|
|
696
|
+
["payments", "Payments"],
|
|
697
|
+
["webhooks", "Webhooks"],
|
|
698
|
+
["storage", "Storage"],
|
|
699
|
+
["background_jobs", "Background jobs"],
|
|
700
|
+
["email", "Email"],
|
|
701
|
+
["database", "Database"],
|
|
702
|
+
["migrations", "Migrations"],
|
|
703
|
+
["api_routes", "API routes"],
|
|
704
|
+
["tests", "Tests"],
|
|
705
|
+
["security", "Security"],
|
|
706
|
+
["infrastructure", "Infrastructure"],
|
|
707
|
+
["external_integrations", "External integrations"],
|
|
708
|
+
["environment", "Environment"]
|
|
709
|
+
];
|
|
710
|
+
for (const [key, label] of CATEGORIES) {
|
|
711
|
+
const arr = signals[key];
|
|
712
|
+
if (arr.length === 0) continue;
|
|
713
|
+
if (arr.length === 1) {
|
|
714
|
+
lines.push(`- ${label}: ${arr[0].label}`);
|
|
715
|
+
} else {
|
|
716
|
+
lines.push(`- ${label}: ${arr.length}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return lines.length > 0 ? lines.join("\n") + "\n" : "No signals detected.\n";
|
|
720
|
+
}
|
|
721
|
+
function renderSignalsInventory(signals) {
|
|
722
|
+
const parts = [];
|
|
723
|
+
const CATEGORIES = [
|
|
724
|
+
["auth", "Auth"],
|
|
725
|
+
["payments", "Payments"],
|
|
726
|
+
["webhooks", "Webhooks"],
|
|
727
|
+
["storage", "Storage"],
|
|
728
|
+
["background_jobs", "Background Jobs"],
|
|
729
|
+
["email", "Email / Notifications"],
|
|
730
|
+
["database", "Database"],
|
|
731
|
+
["migrations", "Migrations"],
|
|
732
|
+
["api_routes", "API Routes"],
|
|
733
|
+
["tests", "Tests"],
|
|
734
|
+
["security", "Security"],
|
|
735
|
+
["infrastructure", "Infrastructure"],
|
|
736
|
+
["external_integrations", "External Integrations"],
|
|
737
|
+
["environment", "Environment"]
|
|
738
|
+
];
|
|
739
|
+
for (const [key, label] of CATEGORIES) {
|
|
740
|
+
const arr = signals[key];
|
|
741
|
+
if (arr.length === 0) continue;
|
|
742
|
+
parts.push(`### ${label}
|
|
743
|
+
`);
|
|
744
|
+
for (const s of arr) {
|
|
745
|
+
parts.push(`- ${s.label} \u2014 confidence: ${s.confidence}`);
|
|
746
|
+
if (s.evidence.length > 0) {
|
|
747
|
+
parts.push(` - Evidence: ${s.evidence.map((e) => "`" + e + "`").join(", ")}`);
|
|
748
|
+
}
|
|
749
|
+
if (s.recommended_review) {
|
|
750
|
+
parts.push(` - Review: ${s.recommended_review}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
parts.push("");
|
|
754
|
+
}
|
|
755
|
+
return parts.join("\n");
|
|
756
|
+
}
|
|
757
|
+
function renderSignalsConsole(signals) {
|
|
758
|
+
const lines = [];
|
|
759
|
+
const CATEGORIES = [
|
|
760
|
+
["auth", "Auth"],
|
|
761
|
+
["payments", "Payments"],
|
|
762
|
+
["webhooks", "Webhooks"],
|
|
763
|
+
["storage", "Storage"],
|
|
764
|
+
["background_jobs", "Background jobs"],
|
|
765
|
+
["email", "Email"],
|
|
766
|
+
["database", "Database"],
|
|
767
|
+
["migrations", "Migrations"],
|
|
768
|
+
["api_routes", "API routes"],
|
|
769
|
+
["tests", "Tests"],
|
|
770
|
+
["security", "Security"],
|
|
771
|
+
["infrastructure", "Infrastructure"],
|
|
772
|
+
["external_integrations", "Integrations"],
|
|
773
|
+
["environment", "Environment"]
|
|
774
|
+
];
|
|
775
|
+
for (const [key, label] of CATEGORIES) {
|
|
776
|
+
const arr = signals[key];
|
|
777
|
+
if (arr.length === 0) continue;
|
|
778
|
+
if (arr.length === 1) {
|
|
779
|
+
const warning = arr[0].label.toLowerCase().includes("no test") ? " \u26A0" : "";
|
|
780
|
+
lines.push(` ${label}: ${arr[0].label}${warning}`);
|
|
781
|
+
} else {
|
|
782
|
+
lines.push(` ${label}: ${arr.map((s) => s.label).join(", ")}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return lines;
|
|
786
|
+
}
|
|
787
|
+
|
|
242
788
|
// src/services/scanner.ts
|
|
243
789
|
function detectLanguage(dir) {
|
|
244
790
|
if (exists(join(dir, "tsconfig.json"))) return "typescript";
|
|
@@ -391,6 +937,7 @@ function scan(dir) {
|
|
|
391
937
|
const testDirs = detectTestDirs(dir);
|
|
392
938
|
const hasGit2 = isDir(join(dir, ".git"));
|
|
393
939
|
const suggestedDomains = suggestDomains(dir, codeDirs);
|
|
940
|
+
const signals = buildScanSignals(dir, codeDirs, migrationDirs, testDirs, infraFiles);
|
|
394
941
|
return {
|
|
395
942
|
language,
|
|
396
943
|
framework,
|
|
@@ -401,7 +948,8 @@ function scan(dir) {
|
|
|
401
948
|
infraFiles,
|
|
402
949
|
testDirs,
|
|
403
950
|
hasGit: hasGit2,
|
|
404
|
-
suggestedDomains
|
|
951
|
+
suggestedDomains,
|
|
952
|
+
signals
|
|
405
953
|
};
|
|
406
954
|
}
|
|
407
955
|
|
|
@@ -465,6 +1013,7 @@ function buildBaseline(result, context, now = /* @__PURE__ */ new Date()) {
|
|
|
465
1013
|
infrastructureFiles: result.infraFiles,
|
|
466
1014
|
testDirectories: result.testDirs
|
|
467
1015
|
},
|
|
1016
|
+
signals: result.signals,
|
|
468
1017
|
suggestions: {
|
|
469
1018
|
possibleDomains: result.suggestedDomains.map((d) => d.replace(/^#\s*/, "").trim()).filter(Boolean),
|
|
470
1019
|
openQuestions: buildOpenQuestions(result, context)
|
|
@@ -509,6 +1058,10 @@ function renderInventory(baseline) {
|
|
|
509
1058
|
parts.push(section("Contracts", detected.contractFiles));
|
|
510
1059
|
parts.push(section("Infrastructure", detected.infrastructureFiles));
|
|
511
1060
|
parts.push(section("Tests", detected.testDirectories.map((d) => `${d}/`)));
|
|
1061
|
+
if (signalCount(baseline.signals) > 0) {
|
|
1062
|
+
parts.push("## Detected Signals\n");
|
|
1063
|
+
parts.push(renderSignalsInventory(baseline.signals));
|
|
1064
|
+
}
|
|
512
1065
|
parts.push(section("Possible Domains", suggestions.possibleDomains));
|
|
513
1066
|
parts.push(section("Open Questions", suggestions.openQuestions));
|
|
514
1067
|
return parts.join("\n");
|
|
@@ -766,7 +1319,15 @@ function printResult(result) {
|
|
|
766
1319
|
console.log("No domains detected automatically.");
|
|
767
1320
|
console.log("You can add them manually in .kaddo/config.yml under project.domains.");
|
|
768
1321
|
}
|
|
769
|
-
|
|
1322
|
+
const signalLines = renderSignalsConsole(result.signals);
|
|
1323
|
+
if (signalLines.length > 0) {
|
|
1324
|
+
console.log("Signals:");
|
|
1325
|
+
for (const line of signalLines) console.log(line);
|
|
1326
|
+
console.log("");
|
|
1327
|
+
} else {
|
|
1328
|
+
console.log("Signals: none detected");
|
|
1329
|
+
console.log("");
|
|
1330
|
+
}
|
|
770
1331
|
}
|
|
771
1332
|
function updateConfig(dir, result) {
|
|
772
1333
|
const configPath = join(dir, ".kaddo", "config.yml");
|
|
@@ -4641,8 +5202,8 @@ function parseBlock(block) {
|
|
|
4641
5202
|
}
|
|
4642
5203
|
function decisionCandidatesFromSignals(signals) {
|
|
4643
5204
|
const out = [];
|
|
4644
|
-
for (const
|
|
4645
|
-
const m =
|
|
5205
|
+
for (const sig2 of signals) {
|
|
5206
|
+
const m = sig2.match(/decision candidate\s*:?\s*(.+)$/i);
|
|
4646
5207
|
if (!m) continue;
|
|
4647
5208
|
const rest = m[1].trim();
|
|
4648
5209
|
const token = rest.match(/\b[A-Z][A-Z0-9_]{2,}\b/);
|
|
@@ -8680,6 +9241,416 @@ function buildRoadmapQuality(dir) {
|
|
|
8680
9241
|
return { initiatives, work_item_candidates };
|
|
8681
9242
|
}
|
|
8682
9243
|
|
|
9244
|
+
// src/core/project-route.ts
|
|
9245
|
+
function isUseful(q) {
|
|
9246
|
+
return q === "useful";
|
|
9247
|
+
}
|
|
9248
|
+
var enableKaddo = {
|
|
9249
|
+
id: "enable-kaddo",
|
|
9250
|
+
label: "Enable Kaddo",
|
|
9251
|
+
evaluate: (ctx) => ({
|
|
9252
|
+
status: ctx.hasConfig ? "done" : "current",
|
|
9253
|
+
evidence: ctx.hasConfig ? [".kaddo/config.yml"] : void 0,
|
|
9254
|
+
command: ctx.hasConfig ? void 0 : "kaddo init"
|
|
9255
|
+
})
|
|
9256
|
+
};
|
|
9257
|
+
var scanRepository = {
|
|
9258
|
+
id: "scan-repository",
|
|
9259
|
+
label: "Scan repository",
|
|
9260
|
+
evaluate: (ctx) => {
|
|
9261
|
+
if (!ctx.hasScan) return { status: "pending", command: "kaddo scan" };
|
|
9262
|
+
if (ctx.hasScanWarnings) return { status: "warning", evidence: [".kaddo/scan.json"], reason: "Scan has warnings (e.g. no test directory detected).", command: "kaddo scan" };
|
|
9263
|
+
return { status: "done", evidence: [".kaddo/scan.json"] };
|
|
9264
|
+
}
|
|
9265
|
+
};
|
|
9266
|
+
var defineBusiness = {
|
|
9267
|
+
id: "define-business",
|
|
9268
|
+
label: "Define business context",
|
|
9269
|
+
evaluate: (ctx) => ({
|
|
9270
|
+
status: isUseful(ctx.qBusiness) ? "done" : ctx.qBusiness === "missing" ? "pending" : "warning",
|
|
9271
|
+
evidence: isUseful(ctx.qBusiness) ? ["knowledge/business/business.md"] : void 0,
|
|
9272
|
+
reason: ctx.qBusiness === "placeholder" ? "Business context is still a bootstrap placeholder." : void 0,
|
|
9273
|
+
agent: isUseful(ctx.qBusiness) ? void 0 : "business-agent"
|
|
9274
|
+
})
|
|
9275
|
+
};
|
|
9276
|
+
var defineProduct = {
|
|
9277
|
+
id: "define-product",
|
|
9278
|
+
label: "Define product context",
|
|
9279
|
+
evaluate: (ctx) => ({
|
|
9280
|
+
status: isUseful(ctx.qProduct) ? "done" : ctx.qProduct === "missing" ? "pending" : "warning",
|
|
9281
|
+
evidence: isUseful(ctx.qProduct) ? ["knowledge/product/product.md"] : void 0,
|
|
9282
|
+
agent: isUseful(ctx.qProduct) ? void 0 : "capability-agent"
|
|
9283
|
+
})
|
|
9284
|
+
};
|
|
9285
|
+
var discoverCapabilities = {
|
|
9286
|
+
id: "discover-capabilities",
|
|
9287
|
+
label: "Discover capabilities",
|
|
9288
|
+
evaluate: (ctx) => ({
|
|
9289
|
+
status: isUseful(ctx.qCapabilities) ? "done" : ctx.qCapabilities === "missing" ? "pending" : "warning",
|
|
9290
|
+
evidence: isUseful(ctx.qCapabilities) ? ["knowledge/product/capabilities.md"] : void 0,
|
|
9291
|
+
agent: isUseful(ctx.qCapabilities) ? void 0 : "capability-agent"
|
|
9292
|
+
})
|
|
9293
|
+
};
|
|
9294
|
+
var describeArchitecture = {
|
|
9295
|
+
id: "describe-architecture",
|
|
9296
|
+
label: "Describe current architecture",
|
|
9297
|
+
evaluate: (ctx) => {
|
|
9298
|
+
const done = isUseful(ctx.qCurrentState) && isUseful(ctx.qCodebase);
|
|
9299
|
+
const evidence = [];
|
|
9300
|
+
if (isUseful(ctx.qCurrentState)) evidence.push("knowledge/tech/current-state.md");
|
|
9301
|
+
if (isUseful(ctx.qCodebase)) evidence.push("knowledge/tech/codebase.md");
|
|
9302
|
+
return {
|
|
9303
|
+
status: done ? "done" : "pending",
|
|
9304
|
+
evidence: evidence.length > 0 ? evidence : void 0,
|
|
9305
|
+
agent: done ? void 0 : "architecture-agent"
|
|
9306
|
+
};
|
|
9307
|
+
}
|
|
9308
|
+
};
|
|
9309
|
+
var captureTechDecisions = {
|
|
9310
|
+
id: "capture-technical-decisions",
|
|
9311
|
+
label: "Capture technical decisions",
|
|
9312
|
+
evaluate: (ctx) => {
|
|
9313
|
+
if (ctx.decisionCandidates === 0 && ctx.totalAdrs === 0) {
|
|
9314
|
+
return { status: "optional", reason: "No decision candidates found." };
|
|
9315
|
+
}
|
|
9316
|
+
if (ctx.totalAdrs > 0) {
|
|
9317
|
+
return { status: "done", evidence: ["knowledge/tech/decisions/"], reason: `${ctx.totalAdrs} ADR(s) exist.` };
|
|
9318
|
+
}
|
|
9319
|
+
return {
|
|
9320
|
+
status: "warning",
|
|
9321
|
+
reason: `${ctx.decisionCandidates} decision candidate(s) exist but no ADRs have been materialized.`,
|
|
9322
|
+
command: "kaddo adr",
|
|
9323
|
+
skill: "adr-writing"
|
|
9324
|
+
};
|
|
9325
|
+
}
|
|
9326
|
+
};
|
|
9327
|
+
var createWorkSource = {
|
|
9328
|
+
id: "create-work-source",
|
|
9329
|
+
label: "Create or connect work source",
|
|
9330
|
+
evaluate: (ctx) => ({
|
|
9331
|
+
status: ctx.hasRoadmap || ctx.totalWorkItems > 0 ? "done" : "pending",
|
|
9332
|
+
evidence: ctx.hasRoadmap ? ["knowledge/delivery/roadmap.md"] : void 0,
|
|
9333
|
+
command: ctx.hasRoadmap ? void 0 : "kaddo roadmap",
|
|
9334
|
+
agent: ctx.hasRoadmap ? void 0 : "roadmap-agent"
|
|
9335
|
+
})
|
|
9336
|
+
};
|
|
9337
|
+
var materializeWorkItem = {
|
|
9338
|
+
id: "materialize-work-item",
|
|
9339
|
+
label: "Materialize first Work Item",
|
|
9340
|
+
evaluate: (ctx) => {
|
|
9341
|
+
if (ctx.totalWorkItems > 0) {
|
|
9342
|
+
return { status: "done", evidence: ["knowledge/delivery/work-items/"] };
|
|
9343
|
+
}
|
|
9344
|
+
if (ctx.roadmapCandidates > 0) {
|
|
9345
|
+
return { status: "current", command: "kaddo create --from roadmap", reason: "Roadmap has candidates but no Work Item exists." };
|
|
9346
|
+
}
|
|
9347
|
+
return { status: "pending" };
|
|
9348
|
+
}
|
|
9349
|
+
};
|
|
9350
|
+
var refineWorkItem = {
|
|
9351
|
+
id: "refine-work-item",
|
|
9352
|
+
label: "Refine Work Item",
|
|
9353
|
+
evaluate: (ctx) => {
|
|
9354
|
+
if (ctx.readyWorkItems > 0 || ctx.inProgressWorkItems > 0) return { status: "done" };
|
|
9355
|
+
if (ctx.draftWorkItems > 0) {
|
|
9356
|
+
return {
|
|
9357
|
+
status: "current",
|
|
9358
|
+
reason: `There ${ctx.draftWorkItems === 1 ? "is" : "are"} ${ctx.draftWorkItems} draft Work Item${ctx.draftWorkItems === 1 ? "" : "s"} and no Work Item is ready.`,
|
|
9359
|
+
agent: "work-item-agent",
|
|
9360
|
+
skill: "work-item-refinement"
|
|
9361
|
+
};
|
|
9362
|
+
}
|
|
9363
|
+
return { status: "pending" };
|
|
9364
|
+
}
|
|
9365
|
+
};
|
|
9366
|
+
var suggestOwnership = {
|
|
9367
|
+
id: "suggest-ownership",
|
|
9368
|
+
label: "Suggest ownership",
|
|
9369
|
+
evaluate: (ctx) => {
|
|
9370
|
+
if (ctx.totalWorkItems === 0) return { status: "pending" };
|
|
9371
|
+
if (ctx.ownershipComplete) return { status: "done" };
|
|
9372
|
+
return {
|
|
9373
|
+
status: "next",
|
|
9374
|
+
reason: `Ownership coverage is ${ctx.ownershipCoverage}.`,
|
|
9375
|
+
command: "kaddo owners suggest"
|
|
9376
|
+
};
|
|
9377
|
+
}
|
|
9378
|
+
};
|
|
9379
|
+
var resolveAdrs = {
|
|
9380
|
+
id: "resolve-adrs",
|
|
9381
|
+
label: "Resolve ADRs if needed",
|
|
9382
|
+
evaluate: (ctx) => {
|
|
9383
|
+
if (ctx.decisionCandidates === 0 && ctx.totalAdrs === 0) return { status: "optional" };
|
|
9384
|
+
if (ctx.totalAdrs > 0) return { status: "done" };
|
|
9385
|
+
return {
|
|
9386
|
+
status: "warning",
|
|
9387
|
+
reason: `${ctx.decisionCandidates} decision candidate(s) without ADRs.`,
|
|
9388
|
+
command: "kaddo adr",
|
|
9389
|
+
skill: "adr-writing"
|
|
9390
|
+
};
|
|
9391
|
+
}
|
|
9392
|
+
};
|
|
9393
|
+
var prepareImplementation = {
|
|
9394
|
+
id: "prepare-implementation",
|
|
9395
|
+
label: "Prepare implementation",
|
|
9396
|
+
evaluate: (ctx) => {
|
|
9397
|
+
if (ctx.inProgressWorkItems > 0 || ctx.completedWorkItems > 0) return { status: "done" };
|
|
9398
|
+
if (ctx.readyWorkItems > 0) {
|
|
9399
|
+
if (ctx.adaptersInstalled === 0) {
|
|
9400
|
+
return { status: "current", reason: "Ready Work Items exist but no adapter is installed.", command: "kaddo adapters list" };
|
|
9401
|
+
}
|
|
9402
|
+
return { status: "current", agent: "implementation-agent" };
|
|
9403
|
+
}
|
|
9404
|
+
return { status: "pending" };
|
|
9405
|
+
}
|
|
9406
|
+
};
|
|
9407
|
+
var runGuard2 = {
|
|
9408
|
+
id: "run-guard",
|
|
9409
|
+
label: "Run guard",
|
|
9410
|
+
evaluate: (ctx) => {
|
|
9411
|
+
if (ctx.hasGuardHistory) return { status: "done", evidence: [".kaddo/history/guard-runs.jsonl"] };
|
|
9412
|
+
if (ctx.inProgressWorkItems > 0) return { status: "current", command: "kaddo guard" };
|
|
9413
|
+
return { status: "pending" };
|
|
9414
|
+
}
|
|
9415
|
+
};
|
|
9416
|
+
var captureLearning = {
|
|
9417
|
+
id: "capture-learning",
|
|
9418
|
+
label: "Capture learning",
|
|
9419
|
+
evaluate: (ctx) => {
|
|
9420
|
+
if (ctx.completedWorkItems > 0) return { status: "done" };
|
|
9421
|
+
if (ctx.totalWorkItems === 0) return { status: "optional", reason: "Project is still in early discovery." };
|
|
9422
|
+
return { status: "pending" };
|
|
9423
|
+
}
|
|
9424
|
+
};
|
|
9425
|
+
var identifyLegacyModules = {
|
|
9426
|
+
id: "identify-legacy-modules",
|
|
9427
|
+
label: "Identify legacy modules",
|
|
9428
|
+
evaluate: (ctx) => {
|
|
9429
|
+
const hasLegacy = exists(join(ctx.dir, "knowledge/legacy/risks.md"));
|
|
9430
|
+
return {
|
|
9431
|
+
status: hasLegacy ? "done" : "pending",
|
|
9432
|
+
evidence: hasLegacy ? ["knowledge/legacy/risks.md"] : void 0,
|
|
9433
|
+
agent: hasLegacy ? void 0 : "legacy-agent"
|
|
9434
|
+
};
|
|
9435
|
+
}
|
|
9436
|
+
};
|
|
9437
|
+
var identifyRisks = {
|
|
9438
|
+
id: "identify-risks",
|
|
9439
|
+
label: "Identify operational risks",
|
|
9440
|
+
evaluate: (ctx) => {
|
|
9441
|
+
const hasLegacy = exists(join(ctx.dir, "knowledge/legacy/risks.md"));
|
|
9442
|
+
return { status: hasLegacy ? "done" : "pending", agent: hasLegacy ? void 0 : "legacy-agent" };
|
|
9443
|
+
}
|
|
9444
|
+
};
|
|
9445
|
+
var NEW_STEPS = [
|
|
9446
|
+
enableKaddo,
|
|
9447
|
+
defineBusiness,
|
|
9448
|
+
defineProduct,
|
|
9449
|
+
{ ...discoverCapabilities, label: "Define initial capabilities" },
|
|
9450
|
+
{ ...describeArchitecture, label: "Define initial architecture" },
|
|
9451
|
+
{ ...createWorkSource, label: "Create initial work source" },
|
|
9452
|
+
{ ...materializeWorkItem, label: "Create first Work Item" },
|
|
9453
|
+
refineWorkItem,
|
|
9454
|
+
suggestOwnership,
|
|
9455
|
+
prepareImplementation,
|
|
9456
|
+
runGuard2,
|
|
9457
|
+
captureLearning
|
|
9458
|
+
];
|
|
9459
|
+
var PRE_AI_STEPS = [
|
|
9460
|
+
enableKaddo,
|
|
9461
|
+
scanRepository,
|
|
9462
|
+
defineBusiness,
|
|
9463
|
+
defineProduct,
|
|
9464
|
+
discoverCapabilities,
|
|
9465
|
+
describeArchitecture,
|
|
9466
|
+
captureTechDecisions,
|
|
9467
|
+
createWorkSource,
|
|
9468
|
+
materializeWorkItem,
|
|
9469
|
+
refineWorkItem,
|
|
9470
|
+
suggestOwnership,
|
|
9471
|
+
resolveAdrs,
|
|
9472
|
+
prepareImplementation,
|
|
9473
|
+
runGuard2,
|
|
9474
|
+
captureLearning
|
|
9475
|
+
];
|
|
9476
|
+
var LEGACY_STEPS = [
|
|
9477
|
+
enableKaddo,
|
|
9478
|
+
scanRepository,
|
|
9479
|
+
identifyLegacyModules,
|
|
9480
|
+
{ ...discoverCapabilities, label: "Discover critical capabilities" },
|
|
9481
|
+
describeArchitecture,
|
|
9482
|
+
identifyRisks,
|
|
9483
|
+
captureTechDecisions,
|
|
9484
|
+
{ ...createWorkSource, label: "Create modernization candidates" },
|
|
9485
|
+
{ ...materializeWorkItem, label: "Materialize safe Work Item" },
|
|
9486
|
+
refineWorkItem,
|
|
9487
|
+
suggestOwnership,
|
|
9488
|
+
resolveAdrs,
|
|
9489
|
+
{ ...prepareImplementation, label: "Prepare safe implementation" },
|
|
9490
|
+
runGuard2,
|
|
9491
|
+
captureLearning
|
|
9492
|
+
];
|
|
9493
|
+
function stepsForState(state) {
|
|
9494
|
+
switch (state) {
|
|
9495
|
+
case "new":
|
|
9496
|
+
return NEW_STEPS;
|
|
9497
|
+
case "legacy":
|
|
9498
|
+
return LEGACY_STEPS;
|
|
9499
|
+
case "pre-ai":
|
|
9500
|
+
default:
|
|
9501
|
+
return PRE_AI_STEPS;
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
function buildRouteContext(dir) {
|
|
9505
|
+
const config = loadConfig(dir);
|
|
9506
|
+
const hasScan = exists(join(dir, ".kaddo", "scan.json"));
|
|
9507
|
+
const hasInventory = exists(join(dir, "knowledge", "inventory.md"));
|
|
9508
|
+
const qa = (rel) => analyzeKnowledgeArtifact(dir, rel);
|
|
9509
|
+
const wis = discoverWorkItems(dir);
|
|
9510
|
+
const td = buildTechDecisions(dir);
|
|
9511
|
+
const roadmapPath = join(dir, "knowledge/delivery/roadmap.md");
|
|
9512
|
+
const hasRoadmap = exists(roadmapPath);
|
|
9513
|
+
let roadmapMd = null;
|
|
9514
|
+
if (hasRoadmap) {
|
|
9515
|
+
try {
|
|
9516
|
+
roadmapMd = readFile(roadmapPath);
|
|
9517
|
+
} catch {
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
const stats = roadmapStats(roadmapMd, wis.length);
|
|
9521
|
+
const byState = (s) => wis.filter((w) => w.lifecycle === s).length;
|
|
9522
|
+
const total = wis.length;
|
|
9523
|
+
const withOwnership = wis.filter((w) => w.codeGlobs.length > 0).length;
|
|
9524
|
+
const adapters = installedAdapters(dir);
|
|
9525
|
+
const nextStep = resolveNextStep(dir);
|
|
9526
|
+
return {
|
|
9527
|
+
dir,
|
|
9528
|
+
hasConfig: config !== null,
|
|
9529
|
+
hasScan,
|
|
9530
|
+
hasInventory,
|
|
9531
|
+
qBusiness: qa("knowledge/business/business.md"),
|
|
9532
|
+
qProduct: qa("knowledge/product/product.md"),
|
|
9533
|
+
qCapabilities: qa("knowledge/product/capabilities.md"),
|
|
9534
|
+
qCurrentState: qa("knowledge/tech/current-state.md"),
|
|
9535
|
+
qCodebase: qa("knowledge/tech/codebase.md"),
|
|
9536
|
+
hasRoadmap,
|
|
9537
|
+
roadmapCandidates: stats.work_item_candidates,
|
|
9538
|
+
totalWorkItems: total,
|
|
9539
|
+
draftWorkItems: byState("draft"),
|
|
9540
|
+
readyWorkItems: byState("ready"),
|
|
9541
|
+
inProgressWorkItems: byState("in-progress"),
|
|
9542
|
+
completedWorkItems: byState("completed"),
|
|
9543
|
+
ownershipCoverage: `${withOwnership}/${total}`,
|
|
9544
|
+
ownershipComplete: total > 0 && withOwnership >= total,
|
|
9545
|
+
decisionCandidates: td.candidates,
|
|
9546
|
+
acceptedAdrs: td.accepted_adrs,
|
|
9547
|
+
draftAdrs: td.draft_adrs,
|
|
9548
|
+
totalAdrs: td.adrs,
|
|
9549
|
+
adaptersInstalled: adapters.length,
|
|
9550
|
+
hasGuardHistory: exists(join(dir, ".kaddo", "history", "guard-runs.jsonl")),
|
|
9551
|
+
hasScanWarnings: hasScan ? loadScanWarnings(dir) : false,
|
|
9552
|
+
nextStepId: mapNextStepId(nextStep.id)
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
9555
|
+
function loadScanWarnings(dir) {
|
|
9556
|
+
try {
|
|
9557
|
+
const parsed = JSON.parse(readFile(join(dir, ".kaddo", "scan.json")));
|
|
9558
|
+
return parsed.signals ? hasWarnings(parsed.signals) : false;
|
|
9559
|
+
} catch {
|
|
9560
|
+
return false;
|
|
9561
|
+
}
|
|
9562
|
+
}
|
|
9563
|
+
function mapNextStepId(id) {
|
|
9564
|
+
const MAP = {
|
|
9565
|
+
init: "enable-kaddo",
|
|
9566
|
+
bootstrap: "define-business",
|
|
9567
|
+
"add-agents": "enable-kaddo",
|
|
9568
|
+
"add-skills": "enable-kaddo",
|
|
9569
|
+
scan: "scan-repository",
|
|
9570
|
+
context: "scan-repository",
|
|
9571
|
+
understand: "scan-repository"
|
|
9572
|
+
};
|
|
9573
|
+
return MAP[id] ?? id;
|
|
9574
|
+
}
|
|
9575
|
+
function buildProjectRoute(dir) {
|
|
9576
|
+
const config = loadConfig(dir);
|
|
9577
|
+
const state = config?.project.state ?? "pre-ai";
|
|
9578
|
+
const defs = stepsForState(state);
|
|
9579
|
+
const ctx = buildRouteContext(dir);
|
|
9580
|
+
const steps = defs.map((d) => {
|
|
9581
|
+
const result = d.evaluate(ctx);
|
|
9582
|
+
return { id: d.id, label: d.label, ...result };
|
|
9583
|
+
});
|
|
9584
|
+
for (const s of steps) {
|
|
9585
|
+
if (s.id === ctx.nextStepId && s.status !== "done") {
|
|
9586
|
+
s.status = "current";
|
|
9587
|
+
}
|
|
9588
|
+
}
|
|
9589
|
+
let seenCurrent = false;
|
|
9590
|
+
for (const s of steps) {
|
|
9591
|
+
if (s.status === "current") seenCurrent = true;
|
|
9592
|
+
if (seenCurrent && s.status === "pending") {
|
|
9593
|
+
s.status = "next";
|
|
9594
|
+
break;
|
|
9595
|
+
}
|
|
9596
|
+
}
|
|
9597
|
+
const completed = steps.filter((s) => s.status === "done").length;
|
|
9598
|
+
const total = steps.length;
|
|
9599
|
+
const progressPercent = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
9600
|
+
return {
|
|
9601
|
+
type: state,
|
|
9602
|
+
currentStep: steps.find((s) => s.status === "current")?.id ?? steps.find((s) => s.status !== "done")?.id ?? "complete",
|
|
9603
|
+
completed,
|
|
9604
|
+
total,
|
|
9605
|
+
progressPercent,
|
|
9606
|
+
steps
|
|
9607
|
+
};
|
|
9608
|
+
}
|
|
9609
|
+
var STATUS_MARKER = {
|
|
9610
|
+
done: "[x]",
|
|
9611
|
+
current: "[>]",
|
|
9612
|
+
next: "[ ]",
|
|
9613
|
+
pending: "[ ]",
|
|
9614
|
+
optional: "[o]",
|
|
9615
|
+
blocked: "[!]",
|
|
9616
|
+
warning: "[~]",
|
|
9617
|
+
skipped: "[-]"
|
|
9618
|
+
};
|
|
9619
|
+
function renderRouteMarkdown(route) {
|
|
9620
|
+
const lines = [];
|
|
9621
|
+
lines.push("## Project Route\n");
|
|
9622
|
+
lines.push(`Route: ${route.type}`);
|
|
9623
|
+
lines.push(`Progress: ${route.completed}/${route.total}
|
|
9624
|
+
`);
|
|
9625
|
+
for (const s of route.steps) {
|
|
9626
|
+
lines.push(`- ${STATUS_MARKER[s.status]} ${s.label}`);
|
|
9627
|
+
}
|
|
9628
|
+
lines.push("");
|
|
9629
|
+
return lines.join("\n");
|
|
9630
|
+
}
|
|
9631
|
+
function renderRouteCompact(route) {
|
|
9632
|
+
const lines = [];
|
|
9633
|
+
lines.push(`Route: ${route.type} \xB7 Progress: ${route.completed}/${route.total}
|
|
9634
|
+
`);
|
|
9635
|
+
const current = route.steps.find((s) => s.status === "current");
|
|
9636
|
+
if (current) {
|
|
9637
|
+
const agent = current.agent ? ` \u2014 ${current.agent}` : "";
|
|
9638
|
+
const skill2 = current.skill ? ` / ${current.skill}` : "";
|
|
9639
|
+
lines.push(`Current:
|
|
9640
|
+
- ${current.label}${agent}${skill2}
|
|
9641
|
+
`);
|
|
9642
|
+
}
|
|
9643
|
+
const warnings = route.steps.filter((s) => s.status === "warning");
|
|
9644
|
+
if (warnings.length > 0) {
|
|
9645
|
+
lines.push("Warnings:");
|
|
9646
|
+
for (const w of warnings) {
|
|
9647
|
+
lines.push(`- ${w.label}${w.reason ? ` \u2014 ${w.reason}` : ""}`);
|
|
9648
|
+
}
|
|
9649
|
+
lines.push("");
|
|
9650
|
+
}
|
|
9651
|
+
return lines.join("\n");
|
|
9652
|
+
}
|
|
9653
|
+
|
|
8683
9654
|
// src/core/project-explain.ts
|
|
8684
9655
|
var ARCH_DIR4 = "knowledge";
|
|
8685
9656
|
function normalizeTitle(t) {
|
|
@@ -8735,6 +9706,16 @@ function loadScan(dir) {
|
|
|
8735
9706
|
return null;
|
|
8736
9707
|
}
|
|
8737
9708
|
}
|
|
9709
|
+
function loadScanSignals(dir) {
|
|
9710
|
+
const scanPath = join(dir, ".kaddo", "scan.json");
|
|
9711
|
+
if (!exists(scanPath)) return null;
|
|
9712
|
+
try {
|
|
9713
|
+
const parsed = JSON.parse(readFile(scanPath));
|
|
9714
|
+
return parsed.signals ?? null;
|
|
9715
|
+
} catch {
|
|
9716
|
+
return null;
|
|
9717
|
+
}
|
|
9718
|
+
}
|
|
8738
9719
|
function hasAgents(dir) {
|
|
8739
9720
|
const agentsDir = join(dir, ARCH_DIR4, "agents");
|
|
8740
9721
|
if (!exists(agentsDir)) return false;
|
|
@@ -8905,7 +9886,9 @@ function buildProjectExplanation(dir) {
|
|
|
8905
9886
|
}
|
|
8906
9887
|
},
|
|
8907
9888
|
installedAssets: installedAssetsSummary(dir),
|
|
8908
|
-
roadmapQuality: buildRoadmapQuality(dir)
|
|
9889
|
+
roadmapQuality: buildRoadmapQuality(dir),
|
|
9890
|
+
projectRoute: buildProjectRoute(dir),
|
|
9891
|
+
scanSignals: loadScanSignals(dir)
|
|
8909
9892
|
};
|
|
8910
9893
|
}
|
|
8911
9894
|
function stateLabel(state) {
|
|
@@ -8950,6 +9933,10 @@ function renderExplanationHuman(exp) {
|
|
|
8950
9933
|
lines.push(`- Infrastructure: ${exp.stack.infrastructureFiles.join(", ")}`);
|
|
8951
9934
|
lines.push("");
|
|
8952
9935
|
}
|
|
9936
|
+
if (exp.scanSignals && signalCount(exp.scanSignals) > 0) {
|
|
9937
|
+
lines.push("## Scan Signals");
|
|
9938
|
+
lines.push(renderSignalsCompact(exp.scanSignals));
|
|
9939
|
+
}
|
|
8953
9940
|
const ls = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
|
|
8954
9941
|
lines.push("## Knowledge Status");
|
|
8955
9942
|
lines.push(`- Inventory: ${exp.knowledge.hasInventory ? "available" : "missing"}`);
|
|
@@ -9080,6 +10067,7 @@ function renderExplanationHuman(exp) {
|
|
|
9080
10067
|
lines.push("Review before continuing (non-blocking).");
|
|
9081
10068
|
lines.push("");
|
|
9082
10069
|
}
|
|
10070
|
+
lines.push(renderRouteMarkdown(exp.projectRoute));
|
|
9083
10071
|
const assessment = assessPhase(exp);
|
|
9084
10072
|
lines.push("## Phase");
|
|
9085
10073
|
lines.push(`- Phase: ${assessment.phase}`);
|
|
@@ -9614,6 +10602,8 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
|
|
|
9614
10602
|
graph: loadGraphSummary(dir),
|
|
9615
10603
|
graphHints: loadGraphHints(dir),
|
|
9616
10604
|
skills: discoverInstalledSkills(dir).map((s) => s.id),
|
|
10605
|
+
scanSignals: scanJson?.signals ?? null,
|
|
10606
|
+
projectRoute: buildProjectRoute(dir),
|
|
9617
10607
|
mappedModules,
|
|
9618
10608
|
missing,
|
|
9619
10609
|
// VS-052/VS-073.2: the handoff is driven by the unified next step, so the pack never contradicts
|
|
@@ -9690,6 +10680,8 @@ function renderContextPack(pack) {
|
|
|
9690
10680
|
parts.push(rec.secondary.map((s) => `- ${s.label}`).join("\n") + "\n");
|
|
9691
10681
|
}
|
|
9692
10682
|
}
|
|
10683
|
+
parts.push("## Project Route\n");
|
|
10684
|
+
parts.push(renderRouteCompact(pack.projectRoute));
|
|
9693
10685
|
parts.push("## Knowledge Layers\n");
|
|
9694
10686
|
parts.push(
|
|
9695
10687
|
"Project knowledge is organized in four layers: **Business \u2192 Product \u2192 Tech \u2192 Delivery**.\n"
|
|
@@ -9725,6 +10717,10 @@ function renderContextPack(pack) {
|
|
|
9725
10717
|
} else {
|
|
9726
10718
|
parts.push("Scan baseline missing. Run `kaddo scan` for better context.\n");
|
|
9727
10719
|
}
|
|
10720
|
+
if (pack.scanSignals && signalCount(pack.scanSignals) > 0) {
|
|
10721
|
+
parts.push("## Scan Signals\n");
|
|
10722
|
+
parts.push(renderSignalsCompact(pack.scanSignals));
|
|
10723
|
+
}
|
|
9728
10724
|
parts.push("## Current Knowledge\n");
|
|
9729
10725
|
parts.push((knowledge.summary || "No project knowledge summary found yet.") + "\n");
|
|
9730
10726
|
parts.push("## Roadmap Status\n");
|
|
@@ -9994,7 +10990,8 @@ function enrichUnderstandPlan(plan, opts) {
|
|
|
9994
10990
|
deliveryState: opts.deliveryState,
|
|
9995
10991
|
activeWorkItems: opts.activeWorkItems,
|
|
9996
10992
|
recommendedPaths: opts.recommendedPaths,
|
|
9997
|
-
recommendedSkillPaths: opts.recommendedSkillPaths
|
|
10993
|
+
recommendedSkillPaths: opts.recommendedSkillPaths,
|
|
10994
|
+
projectRoute: opts.projectRoute
|
|
9998
10995
|
};
|
|
9999
10996
|
}
|
|
10000
10997
|
|
|
@@ -10048,6 +11045,10 @@ function renderUnderstand(plan) {
|
|
|
10048
11045
|
].join("\n") + "\n"
|
|
10049
11046
|
);
|
|
10050
11047
|
}
|
|
11048
|
+
if (plan.projectRoute) {
|
|
11049
|
+
parts.push("## Project Route\n");
|
|
11050
|
+
parts.push(renderRouteCompact(plan.projectRoute));
|
|
11051
|
+
}
|
|
10051
11052
|
if (steps.length > 0) {
|
|
10052
11053
|
parts.push("## Recommended Agent Flow\n");
|
|
10053
11054
|
parts.push(`Recommended order for a ${stateLabel2(project.state)} project:
|
|
@@ -10433,7 +11434,8 @@ function runUnderstand() {
|
|
|
10433
11434
|
activeWorkItems: activeWis,
|
|
10434
11435
|
recommendedPaths,
|
|
10435
11436
|
recommendedSkillPaths,
|
|
10436
|
-
language: languageLabel(projectLanguage(config))
|
|
11437
|
+
language: languageLabel(projectLanguage(config)),
|
|
11438
|
+
projectRoute: buildProjectRoute(dir)
|
|
10437
11439
|
});
|
|
10438
11440
|
writeFile(join(dir, ".kaddo", "understand.md"), renderUnderstand(enrichedPlan));
|
|
10439
11441
|
log2.success("Wrote .kaddo/understand.md");
|
|
@@ -11090,7 +12092,7 @@ function findWorkItemArtifacts(dir) {
|
|
|
11090
12092
|
function artifactsMissingOwnership(artifacts) {
|
|
11091
12093
|
return artifacts.filter((a) => !a.hasOwnership);
|
|
11092
12094
|
}
|
|
11093
|
-
function
|
|
12095
|
+
function loadScanSignals2(dir) {
|
|
11094
12096
|
const scanPath = join(dir, SCAN_PATH);
|
|
11095
12097
|
if (!exists(scanPath)) return null;
|
|
11096
12098
|
try {
|
|
@@ -11268,7 +12270,7 @@ async function runOwnersSuggest() {
|
|
|
11268
12270
|
}))
|
|
11269
12271
|
});
|
|
11270
12272
|
const artifact = missing.find((a) => a.relPath === relPath);
|
|
11271
|
-
const signals =
|
|
12273
|
+
const signals = loadScanSignals2(dir);
|
|
11272
12274
|
if (!signals) {
|
|
11273
12275
|
log2.warn("No .kaddo/scan.json found \u2014 run `kaddo scan` for suggestions. You can enter globs manually.");
|
|
11274
12276
|
}
|