@valentia-ai-skills/framework 2.0.7 → 2.0.8

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/bin/cli.js CHANGED
@@ -17,6 +17,7 @@ const path = require("path");
17
17
  const readline = require("readline");
18
18
  const https = require("https");
19
19
  const http = require("http");
20
+ const { pathToFileURL } = require("url");
20
21
 
21
22
  // ── Constants ──
22
23
 
@@ -31,6 +32,24 @@ const ANALYZE_FUNCTION_URL =
31
32
  process.env.AI_SKILLS_ANALYZE_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/analyze-commit";
32
33
  const SCAN_FUNCTION_URL =
33
34
  process.env.AI_SKILLS_SCAN_URL || "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/scan-results";
35
+ const UPLOAD_CODE_AUDIT_URL =
36
+ process.env.AI_SKILLS_UPLOAD_CODE_AUDIT_URL ||
37
+ "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/upload-code-audit";
38
+ const MANAGE_CODE_AUDITS_URL =
39
+ process.env.AI_SKILLS_MANAGE_CODE_AUDITS_URL ||
40
+ "https://znshdhjquohrzvbnloki.supabase.co/functions/v1/manage-code-audits";
41
+
42
+ let codeAuditConfigPromise = null;
43
+
44
+ async function loadCodeAuditConfig() {
45
+ if (!codeAuditConfigPromise) {
46
+ const configUrl = pathToFileURL(
47
+ path.join(__dirname, "..", "skills-console", "code-audit-config.js")
48
+ ).href;
49
+ codeAuditConfigPromise = import(configUrl);
50
+ }
51
+ return codeAuditConfigPromise;
52
+ }
34
53
 
35
54
  // ── Language Detection ──
36
55
 
@@ -1321,6 +1340,320 @@ function fetchJSONWithAuth(url, body, token) {
1321
1340
  });
1322
1341
  }
1323
1342
 
1343
+ function walkFiles(rootPath) {
1344
+ const files = [];
1345
+
1346
+ function visit(currentPath) {
1347
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
1348
+ for (const entry of entries) {
1349
+ const absolutePath = path.join(currentPath, entry.name);
1350
+ if (entry.isDirectory()) {
1351
+ visit(absolutePath);
1352
+ } else if (entry.isFile()) {
1353
+ files.push(absolutePath);
1354
+ }
1355
+ }
1356
+ }
1357
+
1358
+ visit(rootPath);
1359
+ return files.sort();
1360
+ }
1361
+
1362
+ function countLines(content) {
1363
+ if (!content) return 0;
1364
+ return content.split(/\r?\n/).length;
1365
+ }
1366
+
1367
+ function coerceNumber(value, fallback = 0) {
1368
+ const number = Number(value);
1369
+ return Number.isFinite(number) ? number : fallback;
1370
+ }
1371
+
1372
+ function normalizeAuditFindingCount(value) {
1373
+ const source = value && typeof value === "object" ? value : {};
1374
+ return {
1375
+ critical: Math.max(0, coerceNumber(source.critical, 0)),
1376
+ high: Math.max(0, coerceNumber(source.high, 0)),
1377
+ medium: Math.max(0, coerceNumber(source.medium, 0)),
1378
+ low: Math.max(0, coerceNumber(source.low, 0)),
1379
+ };
1380
+ }
1381
+
1382
+ function normalizeAuditScoreEntry(rawEntry, fallbackWeight, codeAuditConfig) {
1383
+ if (typeof rawEntry === "number") {
1384
+ const score = Math.max(0, Math.min(100, Math.round(rawEntry)));
1385
+ return {
1386
+ score,
1387
+ grade: codeAuditConfig.gradeFromCodeAuditScore(score),
1388
+ weight: fallbackWeight,
1389
+ finding_count: normalizeAuditFindingCount({}),
1390
+ };
1391
+ }
1392
+
1393
+ const entry = rawEntry && typeof rawEntry === "object" ? rawEntry : {};
1394
+ const score = Math.max(
1395
+ 0,
1396
+ Math.min(
1397
+ 100,
1398
+ Math.round(
1399
+ coerceNumber(
1400
+ entry.score ??
1401
+ entry.value ??
1402
+ entry.category_score,
1403
+ 0
1404
+ )
1405
+ )
1406
+ )
1407
+ );
1408
+
1409
+ return {
1410
+ score,
1411
+ grade: String(
1412
+ entry.grade ??
1413
+ entry.category_grade ??
1414
+ codeAuditConfig.gradeFromCodeAuditScore(score)
1415
+ ).toUpperCase(),
1416
+ weight:
1417
+ entry.weight === undefined || entry.weight === null
1418
+ ? fallbackWeight
1419
+ : coerceNumber(entry.weight, fallbackWeight ?? 0),
1420
+ finding_count: normalizeAuditFindingCount(
1421
+ entry.finding_count ??
1422
+ entry.findings ??
1423
+ entry.severity_counts ??
1424
+ {}
1425
+ ),
1426
+ };
1427
+ }
1428
+
1429
+ async function normalizeAuditManifestForCli(manifest) {
1430
+ const codeAuditConfig = await loadCodeAuditConfig();
1431
+ const summary = manifest.summary && typeof manifest.summary === "object" ? manifest.summary : {};
1432
+ const overall = manifest.overall && typeof manifest.overall === "object" ? manifest.overall : {};
1433
+ const rawScores =
1434
+ (manifest.scores && typeof manifest.scores === "object" ? manifest.scores : null) ||
1435
+ (manifest.category_scores && typeof manifest.category_scores === "object" ? manifest.category_scores : null) ||
1436
+ (manifest.categories && typeof manifest.categories === "object" ? manifest.categories : null) ||
1437
+ (summary.scores && typeof summary.scores === "object" ? summary.scores : null) ||
1438
+ {};
1439
+
1440
+ const scores = {};
1441
+ for (const definition of codeAuditConfig.getCodeAuditDashboardDefinitions()) {
1442
+ if (!definition.scoreKey) continue;
1443
+ scores[definition.scoreKey] = normalizeAuditScoreEntry(
1444
+ rawScores[definition.scoreKey] ?? rawScores[definition.documentType] ?? {},
1445
+ definition.weight ?? null,
1446
+ codeAuditConfig
1447
+ );
1448
+ }
1449
+
1450
+ const severitySource =
1451
+ (manifest.findings && typeof manifest.findings === "object" ? manifest.findings : null) ||
1452
+ (manifest.finding_summary && typeof manifest.finding_summary === "object" ? manifest.finding_summary : null) ||
1453
+ (summary.findings && typeof summary.findings === "object" ? summary.findings : null) ||
1454
+ {};
1455
+ const normalizedCounts = normalizeAuditFindingCount(severitySource);
1456
+
1457
+ const overallScore = Math.max(
1458
+ 0,
1459
+ Math.min(
1460
+ 100,
1461
+ Math.round(
1462
+ coerceNumber(
1463
+ manifest.overall_score ??
1464
+ manifest.overallScore ??
1465
+ overall.score ??
1466
+ summary.overall_score,
1467
+ 0
1468
+ )
1469
+ )
1470
+ )
1471
+ );
1472
+
1473
+ const overallGrade = String(
1474
+ manifest.overall_grade ??
1475
+ manifest.overallGrade ??
1476
+ overall.grade ??
1477
+ summary.overall_grade ??
1478
+ codeAuditConfig.gradeFromCodeAuditScore(overallScore)
1479
+ ).toUpperCase();
1480
+
1481
+ return {
1482
+ audited_at:
1483
+ manifest.audited_at ??
1484
+ manifest.auditedAt ??
1485
+ manifest.generated_at ??
1486
+ manifest.generatedAt ??
1487
+ summary.audited_at ??
1488
+ new Date().toISOString(),
1489
+ tech_stack:
1490
+ (manifest.tech_stack && typeof manifest.tech_stack === "object" ? manifest.tech_stack : null) ||
1491
+ (manifest.techStack && typeof manifest.techStack === "object" ? manifest.techStack : null) ||
1492
+ (manifest.stack && typeof manifest.stack === "object" ? manifest.stack : null) ||
1493
+ {},
1494
+ overall_score: overallScore,
1495
+ overall_grade: overallGrade,
1496
+ scores,
1497
+ finding_count: normalizedCounts,
1498
+ total_findings:
1499
+ coerceNumber(
1500
+ severitySource.total ??
1501
+ manifest.total_findings ??
1502
+ summary.total_findings,
1503
+ 0
1504
+ ) ||
1505
+ normalizedCounts.critical +
1506
+ normalizedCounts.high +
1507
+ normalizedCounts.medium +
1508
+ normalizedCounts.low,
1509
+ is_healthcare: Boolean(
1510
+ manifest.is_healthcare ??
1511
+ manifest.isHealthcare ??
1512
+ summary.is_healthcare ??
1513
+ false
1514
+ ),
1515
+ };
1516
+ }
1517
+
1518
+ function validateAuditManifestForCli(summary, codeAuditConfig) {
1519
+ const missing = [];
1520
+ if (!summary.audited_at) missing.push("audited_at");
1521
+ if (summary.overall_score === undefined || summary.overall_score === null) missing.push("overall_score");
1522
+ if (!summary.overall_grade) missing.push("overall_grade");
1523
+
1524
+ const missingScores = codeAuditConfig
1525
+ .getCodeAuditDashboardDefinitions()
1526
+ .map((definition) => definition.scoreKey)
1527
+ .filter((scoreKey) => scoreKey && !summary.scores[scoreKey]);
1528
+
1529
+ if (missing.length > 0) {
1530
+ return `manifest.json missing required fields: ${missing.join(", ")}`;
1531
+ }
1532
+
1533
+ if (missingScores.length > 0) {
1534
+ return `manifest.scores missing required categories: ${missingScores.join(", ")}`;
1535
+ }
1536
+
1537
+ return null;
1538
+ }
1539
+
1540
+ function formatScoreDelta(delta) {
1541
+ if (delta === null || delta === undefined) return "n/a";
1542
+ const numericDelta = Number(delta);
1543
+ if (!Number.isFinite(numericDelta)) return String(delta);
1544
+ return numericDelta > 0 ? `+${numericDelta}` : `${numericDelta}`;
1545
+ }
1546
+
1547
+ const BUILTIN_REPORT_SPECS = {
1548
+ overview: {
1549
+ category: "guide",
1550
+ name: "Overview",
1551
+ fileNames: ["OVERVIEW.md", "overview.md"],
1552
+ },
1553
+ api_registry: {
1554
+ category: "report",
1555
+ name: "API Registry",
1556
+ fileNames: ["API_REGISTRY.md", "api_registry.md"],
1557
+ },
1558
+ business_rules: {
1559
+ category: "report",
1560
+ name: "Business Rules",
1561
+ fileNames: ["BUSINESS_RULES.md", "business_rules.md"],
1562
+ },
1563
+ data_models: {
1564
+ category: "report",
1565
+ name: "Data Models",
1566
+ fileNames: ["DATA_MODELS.md", "data_models.md"],
1567
+ },
1568
+ dependencies: {
1569
+ category: "report",
1570
+ name: "Dependencies",
1571
+ fileNames: ["DEPENDENCIES.md", "dependencies.md"],
1572
+ },
1573
+ env_config: {
1574
+ category: "report",
1575
+ name: "Env Config",
1576
+ fileNames: ["ENV_CONFIG.md", "env_config.md"],
1577
+ },
1578
+ risk_report: {
1579
+ category: "report",
1580
+ name: "Risk Report",
1581
+ fileNames: ["RISK_REPORT.md", "risk_report.md"],
1582
+ },
1583
+ glossary: {
1584
+ category: "report",
1585
+ name: "Glossary",
1586
+ fileNames: ["GLOSSARY.md", "glossary.md"],
1587
+ },
1588
+ reproduction_guide: {
1589
+ category: "guide",
1590
+ name: "Reproduction Guide",
1591
+ fileNames: ["REPRODUCTION_GUIDE.md", "reproduction_guide.md"],
1592
+ },
1593
+ };
1594
+
1595
+ function titleizeReportKey(value) {
1596
+ return String(value || "")
1597
+ .replace(/\.[^.]+$/, "")
1598
+ .replace(/[_-]+/g, " ")
1599
+ .replace(/\s+/g, " ")
1600
+ .trim()
1601
+ .replace(/\b\w/g, (char) => char.toUpperCase());
1602
+ }
1603
+
1604
+ function normalizeDocumentType(value) {
1605
+ const normalized = String(value || "")
1606
+ .trim()
1607
+ .toLowerCase()
1608
+ .replace(/\.[^.]+$/, "")
1609
+ .replace(/[^a-z0-9]+/g, "_")
1610
+ .replace(/^_+|_+$/g, "");
1611
+ return normalized || "report";
1612
+ }
1613
+
1614
+ function toPosixRelative(rootPath, filePath) {
1615
+ return path.relative(rootPath, filePath).split(path.sep).join("/");
1616
+ }
1617
+
1618
+ function findLegacyDocumentPath(rootPath, relativeFile) {
1619
+ if (!relativeFile) return null;
1620
+ const candidates = [path.join(rootPath, relativeFile)];
1621
+ if (!relativeFile.includes("/") && !relativeFile.includes("\\")) {
1622
+ candidates.push(path.join(rootPath, "reports", relativeFile));
1623
+ }
1624
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
1625
+ }
1626
+
1627
+ function normalizeManifestReportEntries(manifestReports) {
1628
+ if (!manifestReports) return [];
1629
+
1630
+ if (Array.isArray(manifestReports)) {
1631
+ return manifestReports.map((entry, index) => {
1632
+ if (typeof entry === "string") {
1633
+ return { file: entry, sourceKey: `report_${index + 1}` };
1634
+ }
1635
+ if (entry && typeof entry === "object") {
1636
+ return { ...entry, sourceKey: entry.sourceKey || entry.document_type || entry.name || `report_${index + 1}` };
1637
+ }
1638
+ return null;
1639
+ }).filter(Boolean);
1640
+ }
1641
+
1642
+ if (typeof manifestReports === "object") {
1643
+ return Object.entries(manifestReports).map(([key, value]) => {
1644
+ if (typeof value === "string") {
1645
+ return { file: value, sourceKey: key };
1646
+ }
1647
+ if (value && typeof value === "object") {
1648
+ return { ...value, sourceKey: key };
1649
+ }
1650
+ return null;
1651
+ }).filter(Boolean);
1652
+ }
1653
+
1654
+ return [];
1655
+ }
1656
+
1324
1657
  async function cmdUploadLegacyScan() {
1325
1658
  console.log(c("blue", "\n━━━ AI Skills Framework — Upload Legacy Scan ━━━\n"));
1326
1659
 
@@ -1480,45 +1813,109 @@ async function cmdUploadLegacyScan() {
1480
1813
  console.log(`Diagrams: ${c("green", diagramPayload.length.toString())} file(s)`);
1481
1814
 
1482
1815
  // ── Read report/guide files ──
1483
- const GUIDE_TYPES = new Set(["overview", "reproduction_guide"]);
1484
1816
  const reportPayload = [];
1485
- const reportFileMap = {
1486
- overview: ["OVERVIEW.md", "overview.md"],
1487
- api_registry: ["API_REGISTRY.md", "api_registry.md"],
1488
- business_rules: ["BUSINESS_RULES.md", "business_rules.md"],
1489
- data_models: ["DATA_MODELS.md", "data_models.md"],
1490
- dependencies: ["DEPENDENCIES.md", "dependencies.md"],
1491
- env_config: ["ENV_CONFIG.md", "env_config.md"],
1492
- risk_report: ["RISK_REPORT.md", "risk_report.md"],
1493
- glossary: ["GLOSSARY.md", "glossary.md"],
1494
- reproduction_guide: ["REPRODUCTION_GUIDE.md", "reproduction_guide.md"],
1495
- };
1817
+ const usedReportPaths = new Set();
1818
+ const usedReportTypes = new Set();
1819
+ const manifestReportEntries = normalizeManifestReportEntries(manifest.reports);
1820
+ let customReportCount = 0;
1496
1821
 
1497
- for (const [key, candidates] of Object.entries(reportFileMap)) {
1498
- for (const candidate of candidates) {
1499
- const reportPaths = [
1500
- path.join(resolvedPath, candidate),
1501
- path.join(resolvedPath, "reports", candidate),
1502
- ];
1503
- const found = reportPaths.find((p) => fs.existsSync(p));
1504
- if (found) {
1505
- const isGuide = GUIDE_TYPES.has(key);
1506
- const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1507
- reportPayload.push({
1508
- category: isGuide ? "guide" : "report",
1509
- document_type: key,
1510
- name: displayName,
1511
- content: fs.readFileSync(found, "utf-8"),
1512
- priority: 0,
1513
- metadata: {},
1514
- });
1515
- break;
1822
+ function addReportDocument({ category, documentType, name, absolutePath, relativePath, source }) {
1823
+ if (!absolutePath || usedReportPaths.has(absolutePath) || usedReportTypes.has(documentType)) {
1824
+ return false;
1825
+ }
1826
+
1827
+ reportPayload.push({
1828
+ category,
1829
+ document_type: documentType,
1830
+ name,
1831
+ content: fs.readFileSync(absolutePath, "utf-8"),
1832
+ priority: 0,
1833
+ metadata: {
1834
+ source_path: relativePath,
1835
+ source_origin: source,
1836
+ },
1837
+ });
1838
+ usedReportPaths.add(absolutePath);
1839
+ usedReportTypes.add(documentType);
1840
+ if (!(documentType in BUILTIN_REPORT_SPECS)) {
1841
+ customReportCount += 1;
1842
+ }
1843
+ return true;
1844
+ }
1845
+
1846
+ for (const entry of manifestReportEntries) {
1847
+ const absolutePath = findLegacyDocumentPath(resolvedPath, entry.file || entry.path);
1848
+ if (!absolutePath) {
1849
+ if (entry.file || entry.path) {
1850
+ console.log(c("yellow", ` ⚠ Report file not found: ${entry.file || entry.path} — skipping`));
1516
1851
  }
1852
+ continue;
1853
+ }
1854
+
1855
+ const inferredType = normalizeDocumentType(entry.document_type || entry.slug || entry.id || entry.sourceKey || path.basename(absolutePath, path.extname(absolutePath)));
1856
+ const builtinSpec = BUILTIN_REPORT_SPECS[inferredType];
1857
+ const requestedCategory = String(entry.category || entry.type || "").trim().toLowerCase();
1858
+
1859
+ if (!builtinSpec && requestedCategory && requestedCategory !== "report") {
1860
+ console.log(c("yellow", ` ⚠ Custom report "${entry.name || inferredType}" must use type/category "report" — skipping`));
1861
+ continue;
1862
+ }
1863
+
1864
+ addReportDocument({
1865
+ category: builtinSpec?.category || "report",
1866
+ documentType: builtinSpec ? inferredType : inferredType,
1867
+ name: entry.name || builtinSpec?.name || titleizeReportKey(inferredType),
1868
+ absolutePath,
1869
+ relativePath: toPosixRelative(resolvedPath, absolutePath),
1870
+ source: "manifest",
1871
+ });
1872
+ }
1873
+
1874
+ for (const [documentType, spec] of Object.entries(BUILTIN_REPORT_SPECS)) {
1875
+ const found = spec.fileNames
1876
+ .map((candidate) => findLegacyDocumentPath(resolvedPath, candidate))
1877
+ .find(Boolean);
1878
+ if (found) {
1879
+ addReportDocument({
1880
+ category: spec.category,
1881
+ documentType,
1882
+ name: spec.name,
1883
+ absolutePath: found,
1884
+ relativePath: toPosixRelative(resolvedPath, found),
1885
+ source: "builtin",
1886
+ });
1517
1887
  }
1518
1888
  }
1519
1889
 
1520
- const reportCount = reportPayload.length;
1521
- console.log(`Reports: ${c("green", reportCount.toString())} file(s)\n`);
1890
+ const reportsDir = path.join(resolvedPath, "reports");
1891
+ if (fs.existsSync(reportsDir)) {
1892
+ for (const file of fs.readdirSync(reportsDir).sort()) {
1893
+ if (!/\.md$/i.test(file)) continue;
1894
+ const absolutePath = path.join(reportsDir, file);
1895
+ if (usedReportPaths.has(absolutePath)) continue;
1896
+
1897
+ const documentType = normalizeDocumentType(path.basename(file, path.extname(file)));
1898
+ const builtinSpec = BUILTIN_REPORT_SPECS[documentType];
1899
+
1900
+ addReportDocument({
1901
+ category: builtinSpec?.category || "report",
1902
+ documentType,
1903
+ name: builtinSpec?.name || titleizeReportKey(documentType),
1904
+ absolutePath,
1905
+ relativePath: toPosixRelative(resolvedPath, absolutePath),
1906
+ source: "auto",
1907
+ });
1908
+ }
1909
+ }
1910
+
1911
+ const reportCount = reportPayload.filter((doc) => doc.category === "report").length;
1912
+ const guideCount = reportPayload.filter((doc) => doc.category === "guide").length;
1913
+ const reportSummary = guideCount > 0 ? `${reportCount} report(s), ${guideCount} guide(s)` : `${reportCount} report(s)`;
1914
+ console.log(`Reports: ${c("green", reportSummary)}`);
1915
+ if (customReportCount > 0) {
1916
+ console.log(c("dim", ` Included ${customReportCount} custom report(s) from manifest entries or reports/.`));
1917
+ }
1918
+ console.log("");
1522
1919
 
1523
1920
  // ── Build unified documents array ──
1524
1921
  const documentsPayload = [...skillPayload, ...diagramPayload, ...reportPayload];
@@ -1531,7 +1928,8 @@ async function cmdUploadLegacyScan() {
1531
1928
  console.log(` Documents total: ${documentsPayload.length}`);
1532
1929
  console.log(` — Skills: ${skillPayload.length}`);
1533
1930
  console.log(` — Diagrams: ${diagramPayload.length}`);
1534
- console.log(` — Reports/Guides: ${reportCount}`);
1931
+ console.log(` — Reports: ${reportCount}`);
1932
+ console.log(` — Guides: ${guideCount}`);
1535
1933
  if (stats.completeness_score !== undefined) {
1536
1934
  console.log(` Completeness: ${stats.completeness_score}%`);
1537
1935
  }
@@ -1557,7 +1955,7 @@ async function cmdUploadLegacyScan() {
1557
1955
 
1558
1956
  // ── Upload ──
1559
1957
  console.log(c("dim", `Uploading to Valentia (${projectName})...\n`));
1560
- process.stdout.write(` ${c("dim", "→")} Sending ${documentsPayload.length} documents (${skillPayload.length} skills, ${diagramPayload.length} diagrams, ${reportCount} reports)...`);
1958
+ process.stdout.write(` ${c("dim", "→")} Sending ${documentsPayload.length} documents (${skillPayload.length} skills, ${diagramPayload.length} diagrams, ${reportCount} reports, ${guideCount} guides)...`);
1561
1959
 
1562
1960
  let result;
1563
1961
  try {
@@ -1744,23 +2142,47 @@ async function cmdLegacyProjects() {
1744
2142
  }
1745
2143
  }
1746
2144
 
1747
- // ── Report & guide documents — write as DOCUMENT_TYPE.md at root ──
1748
- const REPORT_FILENAMES = {
1749
- api_registry: "API_REGISTRY.md",
1750
- business_rules: "BUSINESS_RULES.md",
1751
- data_models: "DATA_MODELS.md",
1752
- dependencies: "DEPENDENCIES.md",
1753
- env_config: "ENV_CONFIG.md",
1754
- risk_report: "RISK_REPORT.md",
1755
- glossary: "GLOSSARY.md",
1756
- overview: "OVERVIEW.md",
1757
- reproduction_guide: "REPRODUCTION_GUIDE.md",
1758
- };
2145
+ // ── Report & guide documents — preserve built-ins, keep custom reports under reports/ ──
2146
+ const REPORT_FILENAMES = Object.fromEntries(
2147
+ Object.entries(BUILTIN_REPORT_SPECS).map(([documentType, spec]) => [documentType, spec.fileNames[0]])
2148
+ );
2149
+ const customReportsDir = path.join(projectDir, "reports");
2150
+ const manifestReports = [];
1759
2151
 
1760
2152
  for (const doc of [...reportDocs, ...guideDocs]) {
1761
- const fileName = REPORT_FILENAMES[doc.document_type] || (doc.document_type.toUpperCase() + ".md");
1762
- fs.writeFileSync(path.join(projectDir, fileName), doc.content);
1763
- console.log(` ${c("green", "✓")} ${fileName}`);
2153
+ const metadataPath = doc.metadata?.source_path ? String(doc.metadata.source_path).replace(/^\/+/, "") : "";
2154
+ let relativePath = REPORT_FILENAMES[doc.document_type] || "";
2155
+
2156
+ if (!relativePath && metadataPath) {
2157
+ relativePath = metadataPath;
2158
+ }
2159
+ if (!relativePath) {
2160
+ const safeName = doc.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || doc.document_type;
2161
+ relativePath = `reports/${safeName}.md`;
2162
+ }
2163
+
2164
+ if (relativePath.includes("/") || relativePath.includes("\\")) {
2165
+ mkdirp(path.dirname(path.join(projectDir, relativePath)));
2166
+ }
2167
+ fs.writeFileSync(path.join(projectDir, relativePath), doc.content);
2168
+ console.log(` ${c("green", "✓")} ${relativePath}`);
2169
+
2170
+ if (doc.document_type in BUILTIN_REPORT_SPECS) {
2171
+ manifestReports.push({
2172
+ file: relativePath,
2173
+ document_type: doc.document_type,
2174
+ name: doc.name,
2175
+ type: doc.category,
2176
+ });
2177
+ } else {
2178
+ mkdirp(customReportsDir);
2179
+ manifestReports.push({
2180
+ file: relativePath,
2181
+ document_type: doc.document_type,
2182
+ name: doc.name,
2183
+ type: "report",
2184
+ });
2185
+ }
1764
2186
  }
1765
2187
 
1766
2188
  // ── Diagram documents ──
@@ -1791,12 +2213,6 @@ async function cmdLegacyProjects() {
1791
2213
  return { name: doc.name, file: `diagrams/${safeName}.mmd`, type: doc.document_type };
1792
2214
  });
1793
2215
 
1794
- const manifestReports = {};
1795
- for (const doc of [...reportDocs, ...guideDocs]) {
1796
- const fileName = REPORT_FILENAMES[doc.document_type] || (doc.document_type.toUpperCase() + ".md");
1797
- manifestReports[doc.document_type] = fileName;
1798
- }
1799
-
1800
2216
  const manifest = {
1801
2217
  project: project.name,
1802
2218
  scanned_at: project.last_scanned_at || new Date().toISOString(),
@@ -1854,6 +2270,297 @@ async function cmdLegacyProjects() {
1854
2270
  }
1855
2271
  }
1856
2272
 
2273
+ // ── Code Audit Commands ──
2274
+
2275
+ async function cmdUploadCodeAudit() {
2276
+ console.log(c("blue", "\n━━━ AI Skills Framework — Upload Code Audit ━━━\n"));
2277
+
2278
+ const args = process.argv.slice(3);
2279
+ let auditPath = null;
2280
+ let authToken = null;
2281
+ let dryRun = false;
2282
+
2283
+ for (let i = 0; i < args.length; i++) {
2284
+ if (args[i] === "--path" && args[i + 1]) auditPath = args[++i];
2285
+ else if (args[i] === "--token" && args[i + 1]) authToken = args[++i];
2286
+ else if (args[i] === "--dry-run") dryRun = true;
2287
+ }
2288
+
2289
+ if (!auditPath) {
2290
+ console.log(c("red", "✗ --path is required."));
2291
+ console.log(c("dim", " Usage: npx ai-skills upload-code-audit --path ./CodeMatters/\n"));
2292
+ process.exit(1);
2293
+ }
2294
+
2295
+ const resolvedPath = path.resolve(auditPath);
2296
+ if (!fs.existsSync(resolvedPath)) {
2297
+ console.log(c("red", `✗ Path not found: ${resolvedPath}`));
2298
+ process.exit(1);
2299
+ }
2300
+
2301
+ const manifestPath = path.join(resolvedPath, "manifest.json");
2302
+ if (!fs.existsSync(manifestPath)) {
2303
+ console.log(c("red", `✗ manifest.json not found in ${resolvedPath}`));
2304
+ console.log(c("dim", " The CodeMatters folder must contain a manifest.json file.\n"));
2305
+ process.exit(1);
2306
+ }
2307
+
2308
+ const codeAuditConfig = await loadCodeAuditConfig();
2309
+
2310
+ let manifest;
2311
+ try {
2312
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
2313
+ } catch (err) {
2314
+ console.log(c("red", `✗ Failed to parse manifest.json: ${err.message}`));
2315
+ process.exit(1);
2316
+ }
2317
+
2318
+ const manifestSummary = await normalizeAuditManifestForCli(manifest);
2319
+ const manifestError = validateAuditManifestForCli(manifestSummary, codeAuditConfig);
2320
+ if (manifestError) {
2321
+ console.log(c("red", `✗ ${manifestError}`));
2322
+ process.exit(1);
2323
+ }
2324
+
2325
+ const projectName =
2326
+ manifest.project_name ??
2327
+ manifest.project ??
2328
+ manifest.name ??
2329
+ manifest.repository_name;
2330
+
2331
+ if (!projectName || typeof projectName !== "string") {
2332
+ console.log(c("red", "✗ manifest.json missing required field: project_name"));
2333
+ process.exit(1);
2334
+ }
2335
+
2336
+ console.log(`Project: ${c("bold", projectName)}`);
2337
+ console.log(`Audited: ${c("dim", manifestSummary.audited_at)}`);
2338
+ console.log(`Overall: ${c("bold", `${manifestSummary.overall_score}/100 (${manifestSummary.overall_grade})`)}`);
2339
+ if (manifestSummary.tech_stack && Object.keys(manifestSummary.tech_stack).length > 0) {
2340
+ const stackSummary = Object.entries(manifestSummary.tech_stack)
2341
+ .filter(([, value]) => value)
2342
+ .map(([key, value]) => `${key}: ${value}`)
2343
+ .join(", ");
2344
+ console.log(`Stack: ${c("cyan", stackSummary)}`);
2345
+ }
2346
+
2347
+ const allFiles = walkFiles(resolvedPath);
2348
+ const includedFiles = allFiles.filter((absolutePath) => {
2349
+ const relativePath = path.relative(resolvedPath, absolutePath).split(path.sep).join("/");
2350
+ if (relativePath === "manifest.json") return false;
2351
+ return /\.(md|markdown|mmd|mermaid)$/i.test(absolutePath);
2352
+ });
2353
+
2354
+ const documentsPayload = [];
2355
+ for (const absolutePath of includedFiles) {
2356
+ const relativePath = path.relative(resolvedPath, absolutePath).split(path.sep).join("/");
2357
+ const fileName = path.basename(absolutePath);
2358
+ const ext = path.extname(fileName).toLowerCase();
2359
+ const content = fs.readFileSync(absolutePath, "utf-8");
2360
+ const documentType = ext === ".mmd" || ext === ".mermaid"
2361
+ ? "diagram"
2362
+ : codeAuditConfig.inferCodeAuditDocumentType(fileName);
2363
+ const definition = codeAuditConfig.getCodeAuditDocumentConfig(documentType);
2364
+ const scoreEntry = definition?.scoreKey ? manifestSummary.scores[definition.scoreKey] : null;
2365
+
2366
+ documentsPayload.push({
2367
+ name: fileName,
2368
+ relative_path: `./${relativePath}`,
2369
+ document_type: documentType,
2370
+ category: ext === ".mmd" || ext === ".mermaid"
2371
+ ? "diagram"
2372
+ : definition?.category || codeAuditConfig.inferCodeAuditDocumentCategory(documentType),
2373
+ content,
2374
+ line_count: countLines(content),
2375
+ category_score: scoreEntry?.score ?? null,
2376
+ category_grade: scoreEntry?.grade ?? null,
2377
+ finding_count: scoreEntry?.finding_count ?? null,
2378
+ });
2379
+ }
2380
+
2381
+ const manifestContent = fs.readFileSync(manifestPath, "utf-8");
2382
+ documentsPayload.push({
2383
+ name: "manifest.json",
2384
+ relative_path: "./manifest.json",
2385
+ document_type: "manifest",
2386
+ category: "meta",
2387
+ content: manifestContent,
2388
+ line_count: countLines(manifestContent),
2389
+ category_score: null,
2390
+ category_grade: null,
2391
+ finding_count: null,
2392
+ });
2393
+
2394
+ const overviewCount = documentsPayload.filter((doc) => doc.category === "overview").length;
2395
+ const reportCount = documentsPayload.filter((doc) => doc.category === "audit-report").length;
2396
+ const planCount = documentsPayload.filter((doc) => doc.category === "plan").length;
2397
+ const diagramCount = documentsPayload.filter((doc) => doc.category === "diagram").length;
2398
+ const metaCount = documentsPayload.filter((doc) => doc.category === "meta").length;
2399
+
2400
+ console.log(`Documents: ${c("green", documentsPayload.length.toString())} file(s)`);
2401
+ console.log(c("dim", ` Overview: ${overviewCount} Reports: ${reportCount} Plans: ${planCount} Diagrams: ${diagramCount} Meta: ${metaCount}`));
2402
+ console.log(`Findings: ${c("dim", `critical ${manifestSummary.finding_count.critical}, high ${manifestSummary.finding_count.high}, medium ${manifestSummary.finding_count.medium}, low ${manifestSummary.finding_count.low}`)}`);
2403
+ console.log("");
2404
+
2405
+ if (dryRun) {
2406
+ console.log(c("yellow", "Dry run — payload summary:"));
2407
+ console.log(` Project name: ${projectName}`);
2408
+ console.log(` Audited at: ${manifestSummary.audited_at}`);
2409
+ console.log(` Overall: ${manifestSummary.overall_score}/100 (${manifestSummary.overall_grade})`);
2410
+ console.log(` Documents: ${documentsPayload.length}`);
2411
+ console.log(c("dim", "\nNo data was uploaded. Remove --dry-run to upload.\n"));
2412
+ return;
2413
+ }
2414
+
2415
+ let email = null;
2416
+ if (!authToken) {
2417
+ const config = loadConfig();
2418
+ if (config?.email) {
2419
+ email = config.email;
2420
+ console.log(c("dim", `Using saved email: ${email}`));
2421
+ } else {
2422
+ console.log(c("yellow", "No saved config found. Enter your email to authenticate."));
2423
+ email = await ask(`${c("bold", "Work email:")} `);
2424
+ const otpResult = await requestOtpForEmail(email);
2425
+ email = otpResult.email;
2426
+ await verifyOtp(email);
2427
+ }
2428
+ }
2429
+
2430
+ console.log(c("dim", `Uploading audit to Valentia (${projectName})...\n`));
2431
+ process.stdout.write(` ${c("dim", "→")} Sending ${documentsPayload.length} documents...`);
2432
+
2433
+ let result;
2434
+ try {
2435
+ result = await fetchJSONWithAuth(
2436
+ UPLOAD_CODE_AUDIT_URL,
2437
+ {
2438
+ project_name: projectName,
2439
+ manifest,
2440
+ documents: documentsPayload,
2441
+ email,
2442
+ },
2443
+ authToken
2444
+ );
2445
+ console.log(c("green", " done!\n"));
2446
+ } catch (err) {
2447
+ console.log(c("red", " failed!"));
2448
+ console.log(c("red", `\n✗ Upload failed: ${err.message}`));
2449
+ console.log(c("dim", " Retry with --dry-run to validate your payload without uploading.\n"));
2450
+ process.exit(1);
2451
+ }
2452
+
2453
+ console.log(c("green", "✓ Upload complete!\n"));
2454
+ console.log(` Audit ID: ${c("bold", result.audit_id)}`);
2455
+ console.log(` Overall: ${c("bold", `${result.overall_score}/100 (${result.overall_grade})`)}`);
2456
+ console.log(` Documents stored: ${c("bold", String(result.documents_stored || 0))}`);
2457
+ if (result.previous_audit) {
2458
+ console.log(` Previous audit: ${c("dim", `${result.previous_audit.score}/100 (${result.previous_audit.grade || "?"})`)}`);
2459
+ console.log(` Delta: ${c("dim", result.previous_audit.delta || "0")}`);
2460
+ }
2461
+ if ((result.storage_backups_failed || 0) > 0) {
2462
+ console.log(c("yellow", ` Storage backups skipped for ${result.storage_backups_failed} document(s)`));
2463
+ }
2464
+ if (result.console_url) {
2465
+ console.log(`\n View in console: ${c("bold", result.console_url)}`);
2466
+ }
2467
+ console.log("");
2468
+ }
2469
+
2470
+ async function cmdAuditStatus() {
2471
+ const args = process.argv.slice(3);
2472
+ let projectName = null;
2473
+
2474
+ for (let i = 0; i < args.length; i++) {
2475
+ if (args[i] === "--project" && args[i + 1]) projectName = args[++i];
2476
+ else if (!projectName) projectName = args[i];
2477
+ }
2478
+
2479
+ if (!projectName) {
2480
+ console.log(c("red", "Usage: npx ai-skills audit-status --project <project-name>"));
2481
+ process.exit(1);
2482
+ }
2483
+
2484
+ const config = loadConfig();
2485
+ const email = config?.email;
2486
+ if (!email) {
2487
+ console.log(c("yellow", "No saved config. Run 'npx ai-skills setup' first."));
2488
+ process.exit(1);
2489
+ }
2490
+
2491
+ const codeAuditConfig = await loadCodeAuditConfig();
2492
+
2493
+ console.log(c("blue", `\n━━━ Code Audit Status: ${projectName} ━━━\n`));
2494
+
2495
+ let result;
2496
+ try {
2497
+ result = await fetchJSONWithAuth(
2498
+ MANAGE_CODE_AUDITS_URL,
2499
+ { action: "status", email, project_name: projectName },
2500
+ null
2501
+ );
2502
+ } catch (err) {
2503
+ console.log(c("red", `✗ ${err.message}`));
2504
+ process.exit(1);
2505
+ }
2506
+
2507
+ const audit = result.audit;
2508
+ const overallColor =
2509
+ audit.overall_score >= 90 ? "green" :
2510
+ audit.overall_score >= 75 ? "blue" :
2511
+ audit.overall_score >= 60 ? "yellow" :
2512
+ audit.overall_score >= 40 ? "yellow" :
2513
+ "red";
2514
+ const categories = Array.isArray(result.categories) ? result.categories : [];
2515
+ const categoryByType = Object.fromEntries(categories.map((category) => [category.document_type, category]));
2516
+ const history = Array.isArray(result.history) ? result.history : [];
2517
+
2518
+ console.log(` Project: ${c("bold", result.project_name)}`);
2519
+ console.log(` Overall: ${c(overallColor, `${audit.overall_score}/100 (${audit.overall_grade})`)}`);
2520
+ console.log(` Status: ${audit.status}`);
2521
+ console.log(` Audited: ${new Date(audit.audited_at).toLocaleString()}`);
2522
+ console.log(` Findings: critical ${audit.critical_count} high ${audit.high_count} medium ${audit.medium_count} low ${audit.low_count} total ${audit.total_findings}`);
2523
+ if (audit.score_delta !== null && audit.score_delta !== undefined) {
2524
+ console.log(` Delta: ${formatScoreDelta(audit.score_delta)}`);
2525
+ }
2526
+ if (audit.console_url) {
2527
+ console.log(` Console: ${c("dim", audit.console_url)}`);
2528
+ }
2529
+ console.log("");
2530
+
2531
+ console.log(c("bold", " Category Scores"));
2532
+ for (const definition of codeAuditConfig.getCodeAuditDashboardDefinitions()) {
2533
+ const category = categoryByType[definition.documentType];
2534
+ if (!category) continue;
2535
+ const findingCount = category.finding_count || {};
2536
+ console.log(
2537
+ ` ${definition.label.padEnd(16)} ${String(category.score).padStart(3)}/100 ${category.grade || "?"} ` +
2538
+ `${findingCount.critical || 0}c ${findingCount.high || 0}h ${findingCount.medium || 0}m ${findingCount.low || 0}l`
2539
+ );
2540
+ }
2541
+
2542
+ if (history.length > 0) {
2543
+ console.log(`\n${c("bold", " Recent History")}`);
2544
+ for (const entry of history.slice(-5)) {
2545
+ console.log(
2546
+ ` ${new Date(entry.audited_at).toLocaleDateString()} ` +
2547
+ `${String(entry.overall_score).padStart(3)}/100 (${entry.overall_grade}) ` +
2548
+ `findings ${entry.total_findings} critical ${entry.critical_count}`
2549
+ );
2550
+ }
2551
+ }
2552
+
2553
+ if (result.previous_audit) {
2554
+ console.log(`\n${c("bold", " Previous Audit")}`);
2555
+ console.log(
2556
+ ` ${new Date(result.previous_audit.audited_at).toLocaleDateString()} ` +
2557
+ `${result.previous_audit.overall_score}/100 (${result.previous_audit.overall_grade})`
2558
+ );
2559
+ }
2560
+
2561
+ console.log("");
2562
+ }
2563
+
1857
2564
  // ── Main ──
1858
2565
 
1859
2566
  const command = process.argv[2] || "setup";
@@ -1865,6 +2572,8 @@ switch (command) {
1865
2572
  case "list": cmdList(); break;
1866
2573
  case "doctor": cmdDoctor(); break;
1867
2574
  case "analyze": cmdAnalyze(); break;
2575
+ case "upload-code-audit": cmdUploadCodeAudit(); break;
2576
+ case "audit-status": cmdAuditStatus(); break;
1868
2577
  case "upload-legacy-scan": cmdUploadLegacyScan(); break;
1869
2578
  case "legacy-projects": cmdLegacyProjects(); break;
1870
2579
  case "help": case "--help": case "-h":
@@ -1877,6 +2586,8 @@ Usage:
1877
2586
  npx ai-skills status Show installed toolkit, team, and tools
1878
2587
  npx ai-skills list List locally bundled skills
1879
2588
  npx ai-skills analyze Analyze last commit against active skills
2589
+ npx ai-skills upload-code-audit Upload a CodeMatters audit package
2590
+ npx ai-skills audit-status --project <name> Show latest audit summary for a project
1880
2591
  npx ai-skills upload-legacy-scan Upload a legacy codebase intelligence package
1881
2592
  npx ai-skills legacy-projects add <name> Download project → recreate intelligence folder locally
1882
2593
  npx ai-skills legacy-projects add <name> --path <dir> Save to a specific directory (default: cwd)
@@ -1887,6 +2598,9 @@ Usage:
1887
2598
 
1888
2599
  Flags:
1889
2600
  analyze --last Analyze the most recent commit
2601
+ upload-code-audit --path <dir> Path to CodeMatters folder
2602
+ upload-code-audit --dry-run Validate payload without uploading
2603
+ upload-code-audit --token <tok> Explicit auth token
1890
2604
  upload-legacy-scan --path <dir> Path to intelligence folder
1891
2605
  upload-legacy-scan --dry-run Validate payload without uploading
1892
2606
  upload-legacy-scan --token <tok> Explicit auth token
@@ -1895,6 +2609,8 @@ Toolkit includes: skills, agents, commands, hooks, rules, MCP configs.
1895
2609
 
1896
2610
  Environment:
1897
2611
  AI_SKILLS_API_URL Override the Supabase Edge Function URL
2612
+ AI_SKILLS_UPLOAD_CODE_AUDIT_URL Override the upload-code-audit Edge Function URL
2613
+ AI_SKILLS_MANAGE_CODE_AUDITS_URL Override the manage-code-audits Edge Function URL
1898
2614
  `); break;
1899
2615
  default:
1900
2616
  console.log(c("red", `Unknown command: ${command}`));