@kaddo/cli 3.47.0 → 3.49.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 +737 -33
- 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/);
|
|
@@ -4835,6 +5396,7 @@ function buildFrontMatter(id, type, level, title, answers) {
|
|
|
4835
5396
|
`domains: []`,
|
|
4836
5397
|
`code: []`,
|
|
4837
5398
|
`created_at: ${today2}`,
|
|
5399
|
+
`source: manual`,
|
|
4838
5400
|
`summary: "${answers.problem?.split(".")[0] ?? title}"`,
|
|
4839
5401
|
"---"
|
|
4840
5402
|
];
|
|
@@ -4921,6 +5483,7 @@ function buildModuleFrontMatter(id, modType, title, answers) {
|
|
|
4921
5483
|
`domains: []`,
|
|
4922
5484
|
`code: []`,
|
|
4923
5485
|
`created_at: ${today2}`,
|
|
5486
|
+
`source: manual`,
|
|
4924
5487
|
`summary: "${title}"`,
|
|
4925
5488
|
...extraLines,
|
|
4926
5489
|
"---"
|
|
@@ -5100,6 +5663,8 @@ function buildRoadmapFrontMatter(id, type, level, title, candidate, answers) {
|
|
|
5100
5663
|
// from the specific Work Item candidate it was materialized from.
|
|
5101
5664
|
`source_roadmap_initiative: ${candidate.initiative?.id ?? "unknown"}`,
|
|
5102
5665
|
`source_work_item_candidate: ${candidate.id}`,
|
|
5666
|
+
`source_title: "${q(candidate.title)}"`,
|
|
5667
|
+
`source_context: "Materialized from roadmap candidate ${candidate.id}${candidate.initiative?.id ? ` under initiative ${candidate.initiative.id}` : ""}."`,
|
|
5103
5668
|
...candidate.initiative?.title ? [`source_initiative_title: "${q(candidate.initiative.title)}"`] : [],
|
|
5104
5669
|
...candidate.domain ? [`related_domain: "${q(candidate.domain)}"`] : [],
|
|
5105
5670
|
...yamlList2("related_capabilities", relatedCapabilities),
|
|
@@ -5369,6 +5934,7 @@ function parseArtifact(filePath, raw) {
|
|
|
5369
5934
|
initiative: String(data.initiative ?? data.source_initiative ?? ""),
|
|
5370
5935
|
source: data.source ? String(data.source) : "",
|
|
5371
5936
|
sourceId: String(data.source_id ?? ""),
|
|
5937
|
+
rawFrontmatter: data,
|
|
5372
5938
|
decisions: Array.isArray(data.decisions) ? data.decisions.map(String).filter(Boolean) : [],
|
|
5373
5939
|
capsules: Array.isArray(data.capsules) ? data.capsules.map(String).filter(Boolean) : []
|
|
5374
5940
|
};
|
|
@@ -8680,6 +9246,84 @@ function buildRoadmapQuality(dir) {
|
|
|
8680
9246
|
return { initiatives, work_item_candidates };
|
|
8681
9247
|
}
|
|
8682
9248
|
|
|
9249
|
+
// src/core/work-item-source.ts
|
|
9250
|
+
var VALID_SOURCES = [
|
|
9251
|
+
"manual",
|
|
9252
|
+
"roadmap",
|
|
9253
|
+
"jira",
|
|
9254
|
+
"github",
|
|
9255
|
+
"notion",
|
|
9256
|
+
"xlsx",
|
|
9257
|
+
"csv",
|
|
9258
|
+
"api",
|
|
9259
|
+
"external",
|
|
9260
|
+
"unknown"
|
|
9261
|
+
];
|
|
9262
|
+
function isValidSource(s) {
|
|
9263
|
+
return VALID_SOURCES.includes(s);
|
|
9264
|
+
}
|
|
9265
|
+
function parseWorkItemSource(frontmatter) {
|
|
9266
|
+
const raw = frontmatter.source ? String(frontmatter.source) : "";
|
|
9267
|
+
if (raw && isValidSource(raw)) {
|
|
9268
|
+
return {
|
|
9269
|
+
type: raw,
|
|
9270
|
+
id: optStr(frontmatter.source_id),
|
|
9271
|
+
title: optStr(frontmatter.source_title),
|
|
9272
|
+
context: optStr(frontmatter.source_context),
|
|
9273
|
+
provider: optStr(frontmatter.source_provider),
|
|
9274
|
+
url: optStr(frontmatter.source_url),
|
|
9275
|
+
imported_at: optStr(frontmatter.source_imported_at),
|
|
9276
|
+
synced_at: optStr(frontmatter.source_synced_at),
|
|
9277
|
+
inferred: false
|
|
9278
|
+
};
|
|
9279
|
+
}
|
|
9280
|
+
if (raw && !isValidSource(raw)) {
|
|
9281
|
+
return {
|
|
9282
|
+
type: "unknown",
|
|
9283
|
+
id: optStr(frontmatter.source_id),
|
|
9284
|
+
title: optStr(frontmatter.source_title),
|
|
9285
|
+
context: optStr(frontmatter.source_context),
|
|
9286
|
+
provider: optStr(frontmatter.source_provider),
|
|
9287
|
+
url: optStr(frontmatter.source_url),
|
|
9288
|
+
imported_at: optStr(frontmatter.source_imported_at),
|
|
9289
|
+
synced_at: optStr(frontmatter.source_synced_at),
|
|
9290
|
+
inferred: true,
|
|
9291
|
+
reason: `Invalid source value: "${raw}".`
|
|
9292
|
+
};
|
|
9293
|
+
}
|
|
9294
|
+
if (frontmatter.source_work_item_candidate || frontmatter.source_roadmap_initiative) {
|
|
9295
|
+
return {
|
|
9296
|
+
type: "roadmap",
|
|
9297
|
+
id: optStr(frontmatter.source_id) ?? optStr(frontmatter.source_work_item_candidate),
|
|
9298
|
+
title: optStr(frontmatter.source_initiative_title),
|
|
9299
|
+
inferred: true,
|
|
9300
|
+
reason: "Inferred from legacy roadmap fields."
|
|
9301
|
+
};
|
|
9302
|
+
}
|
|
9303
|
+
if (frontmatter.source_id) {
|
|
9304
|
+
return {
|
|
9305
|
+
type: "unknown",
|
|
9306
|
+
id: optStr(frontmatter.source_id),
|
|
9307
|
+
inferred: true,
|
|
9308
|
+
reason: "Has source_id but no source type."
|
|
9309
|
+
};
|
|
9310
|
+
}
|
|
9311
|
+
return {
|
|
9312
|
+
type: "unknown",
|
|
9313
|
+
inferred: true,
|
|
9314
|
+
reason: "No source metadata found."
|
|
9315
|
+
};
|
|
9316
|
+
}
|
|
9317
|
+
function renderSourceCompact(source) {
|
|
9318
|
+
const parts = [source.type];
|
|
9319
|
+
if (source.id) parts.push(source.id);
|
|
9320
|
+
return parts.join(" \xB7 ");
|
|
9321
|
+
}
|
|
9322
|
+
function optStr(v) {
|
|
9323
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
9324
|
+
return void 0;
|
|
9325
|
+
}
|
|
9326
|
+
|
|
8683
9327
|
// src/core/project-route.ts
|
|
8684
9328
|
function isUseful(q) {
|
|
8685
9329
|
return q === "useful";
|
|
@@ -8696,11 +9340,11 @@ var enableKaddo = {
|
|
|
8696
9340
|
var scanRepository = {
|
|
8697
9341
|
id: "scan-repository",
|
|
8698
9342
|
label: "Scan repository",
|
|
8699
|
-
evaluate: (ctx) =>
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
}
|
|
9343
|
+
evaluate: (ctx) => {
|
|
9344
|
+
if (!ctx.hasScan) return { status: "pending", command: "kaddo scan" };
|
|
9345
|
+
if (ctx.hasScanWarnings) return { status: "warning", evidence: [".kaddo/scan.json"], reason: "Scan has warnings (e.g. no test directory detected).", command: "kaddo scan" };
|
|
9346
|
+
return { status: "done", evidence: [".kaddo/scan.json"] };
|
|
9347
|
+
}
|
|
8704
9348
|
};
|
|
8705
9349
|
var defineBusiness = {
|
|
8706
9350
|
id: "define-business",
|
|
@@ -8766,12 +9410,12 @@ var captureTechDecisions = {
|
|
|
8766
9410
|
var createWorkSource = {
|
|
8767
9411
|
id: "create-work-source",
|
|
8768
9412
|
label: "Create or connect work source",
|
|
8769
|
-
evaluate: (ctx) =>
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
}
|
|
9413
|
+
evaluate: (ctx) => {
|
|
9414
|
+
if (!ctx.hasRoadmap && ctx.totalWorkItems === 0) return { status: "pending", command: "kaddo roadmap", agent: "roadmap-agent" };
|
|
9415
|
+
if (ctx.hasUnknownSources) return { status: "warning", evidence: ["Work Items with unknown source"], reason: "Some Work Items have no source metadata." };
|
|
9416
|
+
const evidence = ctx.hasRoadmap ? ["knowledge/delivery/roadmap.md"] : void 0;
|
|
9417
|
+
return { status: "done", evidence };
|
|
9418
|
+
}
|
|
8775
9419
|
};
|
|
8776
9420
|
var materializeWorkItem = {
|
|
8777
9421
|
id: "materialize-work-item",
|
|
@@ -8987,9 +9631,22 @@ function buildRouteContext(dir) {
|
|
|
8987
9631
|
totalAdrs: td.adrs,
|
|
8988
9632
|
adaptersInstalled: adapters.length,
|
|
8989
9633
|
hasGuardHistory: exists(join(dir, ".kaddo", "history", "guard-runs.jsonl")),
|
|
9634
|
+
hasScanWarnings: hasScan ? loadScanWarnings(dir) : false,
|
|
9635
|
+
hasUnknownSources: wis.some((w) => {
|
|
9636
|
+
const src = parseWorkItemSource(w.rawFrontmatter);
|
|
9637
|
+
return src.type === "unknown" && src.inferred;
|
|
9638
|
+
}),
|
|
8990
9639
|
nextStepId: mapNextStepId(nextStep.id)
|
|
8991
9640
|
};
|
|
8992
9641
|
}
|
|
9642
|
+
function loadScanWarnings(dir) {
|
|
9643
|
+
try {
|
|
9644
|
+
const parsed = JSON.parse(readFile(join(dir, ".kaddo", "scan.json")));
|
|
9645
|
+
return parsed.signals ? hasWarnings(parsed.signals) : false;
|
|
9646
|
+
} catch {
|
|
9647
|
+
return false;
|
|
9648
|
+
}
|
|
9649
|
+
}
|
|
8993
9650
|
function mapNextStepId(id) {
|
|
8994
9651
|
const MAP = {
|
|
8995
9652
|
init: "enable-kaddo",
|
|
@@ -9136,6 +9793,16 @@ function loadScan(dir) {
|
|
|
9136
9793
|
return null;
|
|
9137
9794
|
}
|
|
9138
9795
|
}
|
|
9796
|
+
function loadScanSignals(dir) {
|
|
9797
|
+
const scanPath = join(dir, ".kaddo", "scan.json");
|
|
9798
|
+
if (!exists(scanPath)) return null;
|
|
9799
|
+
try {
|
|
9800
|
+
const parsed = JSON.parse(readFile(scanPath));
|
|
9801
|
+
return parsed.signals ?? null;
|
|
9802
|
+
} catch {
|
|
9803
|
+
return null;
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9139
9806
|
function hasAgents(dir) {
|
|
9140
9807
|
const agentsDir = join(dir, ARCH_DIR4, "agents");
|
|
9141
9808
|
if (!exists(agentsDir)) return false;
|
|
@@ -9184,17 +9851,21 @@ function buildProjectExplanation(dir) {
|
|
|
9184
9851
|
hasAgents: hasAgents(dir)
|
|
9185
9852
|
};
|
|
9186
9853
|
const workItemArtifacts = discoverWorkItems(dir);
|
|
9187
|
-
const items = workItemArtifacts.map((a) =>
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9854
|
+
const items = workItemArtifacts.map((a) => {
|
|
9855
|
+
const src = parseWorkItemSource(a.rawFrontmatter);
|
|
9856
|
+
return {
|
|
9857
|
+
id: a.id || a.title,
|
|
9858
|
+
title: a.title,
|
|
9859
|
+
type: a.type,
|
|
9860
|
+
status: a.status,
|
|
9861
|
+
lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
|
|
9862
|
+
initiative: a.initiative,
|
|
9863
|
+
knowledgeLevel: a.knowledgeLevel,
|
|
9864
|
+
hasOwnership: a.codeGlobs.length > 0,
|
|
9865
|
+
domains: a.domains,
|
|
9866
|
+
source: { type: src.type, ...src.id ? { id: src.id } : {}, inferred: src.inferred }
|
|
9867
|
+
};
|
|
9868
|
+
});
|
|
9198
9869
|
const byState = lifecycleCounts(items.map((i) => i.lifecycle));
|
|
9199
9870
|
const byType = {};
|
|
9200
9871
|
for (const i of items) {
|
|
@@ -9307,7 +9978,8 @@ function buildProjectExplanation(dir) {
|
|
|
9307
9978
|
},
|
|
9308
9979
|
installedAssets: installedAssetsSummary(dir),
|
|
9309
9980
|
roadmapQuality: buildRoadmapQuality(dir),
|
|
9310
|
-
projectRoute: buildProjectRoute(dir)
|
|
9981
|
+
projectRoute: buildProjectRoute(dir),
|
|
9982
|
+
scanSignals: loadScanSignals(dir)
|
|
9311
9983
|
};
|
|
9312
9984
|
}
|
|
9313
9985
|
function stateLabel(state) {
|
|
@@ -9352,6 +10024,10 @@ function renderExplanationHuman(exp) {
|
|
|
9352
10024
|
lines.push(`- Infrastructure: ${exp.stack.infrastructureFiles.join(", ")}`);
|
|
9353
10025
|
lines.push("");
|
|
9354
10026
|
}
|
|
10027
|
+
if (exp.scanSignals && signalCount(exp.scanSignals) > 0) {
|
|
10028
|
+
lines.push("## Scan Signals");
|
|
10029
|
+
lines.push(renderSignalsCompact(exp.scanSignals));
|
|
10030
|
+
}
|
|
9355
10031
|
const ls = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
|
|
9356
10032
|
lines.push("## Knowledge Status");
|
|
9357
10033
|
lines.push(`- Inventory: ${exp.knowledge.hasInventory ? "available" : "missing"}`);
|
|
@@ -9386,6 +10062,17 @@ function renderExplanationHuman(exp) {
|
|
|
9386
10062
|
for (const [t, n] of typeEntries) lines.push(`- ${typeLabel(t)}: ${n}`);
|
|
9387
10063
|
lines.push("");
|
|
9388
10064
|
}
|
|
10065
|
+
const sourceCounts = {};
|
|
10066
|
+
for (const i of exp.workItems.items) {
|
|
10067
|
+
const t = i.source?.type ?? "unknown";
|
|
10068
|
+
sourceCounts[t] = (sourceCounts[t] ?? 0) + 1;
|
|
10069
|
+
}
|
|
10070
|
+
const sourceEntries = Object.entries(sourceCounts).filter(([, n]) => n > 0);
|
|
10071
|
+
if (sourceEntries.length > 0) {
|
|
10072
|
+
lines.push("## Work Item Sources");
|
|
10073
|
+
for (const [t, n] of sourceEntries) lines.push(`- ${t.charAt(0).toUpperCase() + t.slice(1)}: ${n}`);
|
|
10074
|
+
lines.push("");
|
|
10075
|
+
}
|
|
9389
10076
|
const grouped = exp.workItems.initiatives.filter(
|
|
9390
10077
|
(g) => LIFECYCLE_STATES.some((s2) => g.states[s2] > 0)
|
|
9391
10078
|
);
|
|
@@ -9854,7 +10541,8 @@ function toContextWorkItem(a) {
|
|
|
9854
10541
|
status: a.status,
|
|
9855
10542
|
lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
|
|
9856
10543
|
knowledgeLevel: a.knowledgeLevel,
|
|
9857
|
-
domains: a.domains
|
|
10544
|
+
domains: a.domains,
|
|
10545
|
+
source: parseWorkItemSource(a.rawFrontmatter)
|
|
9858
10546
|
};
|
|
9859
10547
|
}
|
|
9860
10548
|
function toContextArtifact(a) {
|
|
@@ -10017,6 +10705,7 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
|
|
|
10017
10705
|
graph: loadGraphSummary(dir),
|
|
10018
10706
|
graphHints: loadGraphHints(dir),
|
|
10019
10707
|
skills: discoverInstalledSkills(dir).map((s) => s.id),
|
|
10708
|
+
scanSignals: scanJson?.signals ?? null,
|
|
10020
10709
|
projectRoute: buildProjectRoute(dir),
|
|
10021
10710
|
mappedModules,
|
|
10022
10711
|
missing,
|
|
@@ -10131,6 +10820,10 @@ function renderContextPack(pack) {
|
|
|
10131
10820
|
} else {
|
|
10132
10821
|
parts.push("Scan baseline missing. Run `kaddo scan` for better context.\n");
|
|
10133
10822
|
}
|
|
10823
|
+
if (pack.scanSignals && signalCount(pack.scanSignals) > 0) {
|
|
10824
|
+
parts.push("## Scan Signals\n");
|
|
10825
|
+
parts.push(renderSignalsCompact(pack.scanSignals));
|
|
10826
|
+
}
|
|
10134
10827
|
parts.push("## Current Knowledge\n");
|
|
10135
10828
|
parts.push((knowledge.summary || "No project knowledge summary found yet.") + "\n");
|
|
10136
10829
|
parts.push("## Roadmap Status\n");
|
|
@@ -10190,7 +10883,10 @@ function renderContextPack(pack) {
|
|
|
10190
10883
|
const level = wi.knowledgeLevel ? ` [${wi.knowledgeLevel}]` : "";
|
|
10191
10884
|
const status = wi.lifecycle ? ` (${wi.lifecycle})` : wi.status ? ` (${wi.status})` : "";
|
|
10192
10885
|
const domains = wi.domains.length > 0 ? ` \xB7 domains: ${wi.domains.join(", ")}` : "";
|
|
10193
|
-
|
|
10886
|
+
const line = `- ${wi.id || wi.title} [${wi.type}]${level}${status} \u2014 ${wi.title}${domains}`;
|
|
10887
|
+
const src = wi.source && wi.source.type !== "unknown" ? `
|
|
10888
|
+
- Source: ${renderSourceCompact(wi.source)}` : "";
|
|
10889
|
+
return `${line}${src}`;
|
|
10194
10890
|
});
|
|
10195
10891
|
parts.push(lines.join("\n") + "\n");
|
|
10196
10892
|
} else {
|
|
@@ -10500,7 +11196,11 @@ function renderUnderstand(plan) {
|
|
|
10500
11196
|
if (plan.activeWorkItems && plan.activeWorkItems.length > 0) {
|
|
10501
11197
|
parts.push("## Active Work Items\n");
|
|
10502
11198
|
parts.push(
|
|
10503
|
-
plan.activeWorkItems.map((w) =>
|
|
11199
|
+
plan.activeWorkItems.map((w) => {
|
|
11200
|
+
const src = w.source && w.source.type !== "unknown" ? `
|
|
11201
|
+
- Source: ${w.source.type}${w.source.id ? ` \xB7 ${w.source.id}` : ""}` : "";
|
|
11202
|
+
return `- ${w.id} [${w.type}] ${w.lifecycle} \u2014 ${w.title}${src}`;
|
|
11203
|
+
}).join("\n") + "\n"
|
|
10504
11204
|
);
|
|
10505
11205
|
}
|
|
10506
11206
|
parts.push("## Context Pack\n");
|
|
@@ -10821,7 +11521,11 @@ function runUnderstand() {
|
|
|
10821
11521
|
type: w.type,
|
|
10822
11522
|
lifecycle: w.lifecycle ?? "draft",
|
|
10823
11523
|
knowledgeLevel: w.knowledgeLevel,
|
|
10824
|
-
hasOwnership: w.codeGlobs.length > 0
|
|
11524
|
+
hasOwnership: w.codeGlobs.length > 0,
|
|
11525
|
+
source: (() => {
|
|
11526
|
+
const s = parseWorkItemSource(w.rawFrontmatter);
|
|
11527
|
+
return { type: s.type, ...s.id ? { id: s.id } : {} };
|
|
11528
|
+
})()
|
|
10825
11529
|
}));
|
|
10826
11530
|
const recommendedPaths = [];
|
|
10827
11531
|
if (rec.agent) {
|
|
@@ -11502,7 +12206,7 @@ function findWorkItemArtifacts(dir) {
|
|
|
11502
12206
|
function artifactsMissingOwnership(artifacts) {
|
|
11503
12207
|
return artifacts.filter((a) => !a.hasOwnership);
|
|
11504
12208
|
}
|
|
11505
|
-
function
|
|
12209
|
+
function loadScanSignals2(dir) {
|
|
11506
12210
|
const scanPath = join(dir, SCAN_PATH);
|
|
11507
12211
|
if (!exists(scanPath)) return null;
|
|
11508
12212
|
try {
|
|
@@ -11680,7 +12384,7 @@ async function runOwnersSuggest() {
|
|
|
11680
12384
|
}))
|
|
11681
12385
|
});
|
|
11682
12386
|
const artifact = missing.find((a) => a.relPath === relPath);
|
|
11683
|
-
const signals =
|
|
12387
|
+
const signals = loadScanSignals2(dir);
|
|
11684
12388
|
if (!signals) {
|
|
11685
12389
|
log2.warn("No .kaddo/scan.json found \u2014 run `kaddo scan` for suggestions. You can enter globs manually.");
|
|
11686
12390
|
}
|