@aiready/pattern-detect 0.16.18 → 0.16.20

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.
Files changed (49) hide show
  1. package/dist/analyzer-entry-BVz-HnZd.d.mts +119 -0
  2. package/dist/analyzer-entry-BwuoiCNm.d.ts +119 -0
  3. package/dist/analyzer-entry.d.mts +3 -0
  4. package/dist/analyzer-entry.d.ts +3 -0
  5. package/dist/analyzer-entry.js +693 -0
  6. package/dist/analyzer-entry.mjs +12 -0
  7. package/dist/chunk-65UQ5J2J.mjs +64 -0
  8. package/dist/chunk-6JTVOBJX.mjs +64 -0
  9. package/dist/chunk-BKRPSTT2.mjs +64 -0
  10. package/dist/chunk-CMWW24HW.mjs +259 -0
  11. package/dist/chunk-DNZS4ESD.mjs +391 -0
  12. package/dist/chunk-GLKAGFKX.mjs +391 -0
  13. package/dist/chunk-GREN7X5H.mjs +143 -0
  14. package/dist/chunk-I6ETJC7L.mjs +179 -0
  15. package/dist/chunk-JBUZ6YHE.mjs +391 -0
  16. package/dist/chunk-KWMNN3TG.mjs +391 -0
  17. package/dist/chunk-LYKRYBSM.mjs +64 -0
  18. package/dist/chunk-MHU3CL4R.mjs +64 -0
  19. package/dist/chunk-RS73WLNI.mjs +251 -0
  20. package/dist/chunk-SVCSIZ2A.mjs +259 -0
  21. package/dist/chunk-THF4RW63.mjs +254 -0
  22. package/dist/chunk-UB3CGOQ7.mjs +64 -0
  23. package/dist/chunk-VGMM3L3O.mjs +143 -0
  24. package/dist/chunk-WBBO35SC.mjs +112 -0
  25. package/dist/chunk-WMOGJFME.mjs +391 -0
  26. package/dist/chunk-XNPID6FU.mjs +391 -0
  27. package/dist/cli.js +62 -219
  28. package/dist/cli.mjs +72 -97
  29. package/dist/context-rules-entry-y2uJSngh.d.mts +60 -0
  30. package/dist/context-rules-entry-y2uJSngh.d.ts +60 -0
  31. package/dist/context-rules-entry.d.mts +2 -0
  32. package/dist/context-rules-entry.d.ts +2 -0
  33. package/dist/context-rules-entry.js +207 -0
  34. package/dist/context-rules-entry.mjs +12 -0
  35. package/dist/detector-entry.d.mts +14 -0
  36. package/dist/detector-entry.d.ts +14 -0
  37. package/dist/detector-entry.js +301 -0
  38. package/dist/detector-entry.mjs +7 -0
  39. package/dist/index.d.mts +7 -235
  40. package/dist/index.d.ts +7 -235
  41. package/dist/index.js +9 -126
  42. package/dist/index.mjs +17 -9
  43. package/dist/scoring-entry.d.mts +23 -0
  44. package/dist/scoring-entry.d.ts +23 -0
  45. package/dist/scoring-entry.js +133 -0
  46. package/dist/scoring-entry.mjs +6 -0
  47. package/dist/types-DU2mmhwb.d.mts +36 -0
  48. package/dist/types-DU2mmhwb.d.ts +36 -0
  49. package/package.json +24 -4
package/dist/cli.js CHANGED
@@ -192,144 +192,27 @@ function filterBySeverity(duplicates, minSeverity) {
192
192
  });
193
193
  }
194
194
 
195
- // src/detector.ts
195
+ // src/core/normalizer.ts
196
196
  function normalizeCode(code, isPython = false) {
197
+ if (!code) return "";
197
198
  let normalized = code;
198
199
  if (isPython) {
199
200
  normalized = normalized.replace(/#.*/g, "");
200
201
  } else {
201
- normalized = normalized.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
202
+ normalized = normalized.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
202
203
  }
203
- return normalized.replace(/['"`]/g, '"').replace(/\s+/g, " ").trim().toLowerCase();
204
+ return normalized.replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim().toLowerCase();
204
205
  }
206
+
207
+ // src/detector.ts
205
208
  function extractBlocks(file, content) {
206
- const isPython = file.toLowerCase().endsWith(".py");
207
- if (isPython) {
208
- return extractBlocksPython(file, content);
209
- }
210
- const blocks = [];
211
- const lines = content.split("\n");
212
- const blockRegex = /^\s*(?:export\s+)?(?:async\s+)?(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|readonly\s+|virtual\s+|abstract\s+|override\s+)*(function|class|interface|type|enum|record|struct|void|func|[a-zA-Z0-9_<>[]]+)\s+([a-zA-Z0-9_]+)(?:\s*\(|(?:\s+extends|\s+implements|\s+where)?\s*\{)|^\s*(?:export\s+)?const\s+([a-zA-Z0-9_]+)\s*=\s*[a-zA-Z0-9_.]+\.object\(|^\s*(app\.(?:get|post|put|delete|patch|use))\(/gm;
213
- let match;
214
- while ((match = blockRegex.exec(content)) !== null) {
215
- const startLine = content.substring(0, match.index).split("\n").length;
216
- let type;
217
- let name;
218
- if (match[1]) {
219
- type = match[1];
220
- name = match[2];
221
- } else if (match[3]) {
222
- type = "const";
223
- name = match[3];
224
- } else {
225
- type = "handler";
226
- name = match[4];
227
- }
228
- let endLine = -1;
229
- let openBraces = 0;
230
- let foundStart = false;
231
- for (let i = match.index; i < content.length; i++) {
232
- if (content[i] === "{") {
233
- openBraces++;
234
- foundStart = true;
235
- } else if (content[i] === "}") {
236
- openBraces--;
237
- }
238
- if (foundStart && openBraces === 0) {
239
- endLine = content.substring(0, i + 1).split("\n").length;
240
- break;
241
- }
242
- }
243
- if (endLine === -1) {
244
- const remaining = content.slice(match.index);
245
- const nextLineMatch = remaining.indexOf("\n");
246
- if (nextLineMatch !== -1) {
247
- endLine = startLine;
248
- } else {
249
- endLine = lines.length;
250
- }
251
- }
252
- endLine = Math.max(startLine, endLine);
253
- const blockCode = lines.slice(startLine - 1, endLine).join("\n");
254
- const tokens = (0, import_core2.estimateTokens)(blockCode);
255
- blocks.push({
256
- file,
257
- startLine,
258
- endLine,
259
- code: blockCode,
260
- tokens,
261
- patternType: inferPatternType(type, name)
262
- });
263
- }
264
- return blocks;
265
- }
266
- function extractBlocksPython(file, content) {
267
- const blocks = [];
268
- const lines = content.split("\n");
269
- const blockRegex = /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z0-9_]+)/gm;
270
- let match;
271
- while ((match = blockRegex.exec(content)) !== null) {
272
- const startLinePos = content.substring(0, match.index).split("\n").length;
273
- const startLineIdx = startLinePos - 1;
274
- const initialIndent = lines[startLineIdx].search(/\S/);
275
- let endLineIdx = startLineIdx;
276
- for (let i = startLineIdx + 1; i < lines.length; i++) {
277
- const line = lines[i];
278
- if (line.trim().length === 0) {
279
- endLineIdx = i;
280
- continue;
281
- }
282
- const currentIndent = line.search(/\S/);
283
- if (currentIndent <= initialIndent) {
284
- break;
285
- }
286
- endLineIdx = i;
287
- }
288
- while (endLineIdx > startLineIdx && lines[endLineIdx].trim().length === 0) {
289
- endLineIdx--;
290
- }
291
- const blockCode = lines.slice(startLineIdx, endLineIdx + 1).join("\n");
292
- const tokens = (0, import_core2.estimateTokens)(blockCode);
293
- blocks.push({
294
- file,
295
- startLine: startLinePos,
296
- endLine: endLineIdx + 1,
297
- code: blockCode,
298
- tokens,
299
- patternType: inferPatternType(match[1], match[2])
300
- });
301
- }
302
- return blocks;
303
- }
304
- function inferPatternType(keyword, name) {
305
- const n = name.toLowerCase();
306
- if (keyword === "handler" || n.includes("handler") || n.includes("controller") || n.startsWith("app.")) {
307
- return "api-handler";
308
- }
309
- if (n.includes("validate") || n.includes("schema")) return "validator";
310
- if (n.includes("util") || n.includes("helper")) return "utility";
311
- if (keyword === "class") return "class-method";
312
- if (n.match(/^[A-Z]/)) return "component";
313
- if (keyword === "function") return "function";
314
- return "unknown";
209
+ return (0, import_core2.extractCodeBlocks)(file, content);
315
210
  }
316
211
  function calculateSimilarity(a, b) {
317
- if (a === b) return 1;
318
- const tokensA = a.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
319
- const tokensB = b.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
320
- if (tokensA.length === 0 || tokensB.length === 0) return 0;
321
- const setA = new Set(tokensA);
322
- const setB = new Set(tokensB);
323
- const intersection = new Set([...setA].filter((x) => setB.has(x)));
324
- const union = /* @__PURE__ */ new Set([...setA, ...setB]);
325
- return intersection.size / union.size;
212
+ return (0, import_core2.calculateStringSimilarity)(a, b);
326
213
  }
327
214
  function calculateConfidence(similarity, tokens, lines) {
328
- let confidence = similarity;
329
- if (lines > 20) confidence += 0.05;
330
- if (tokens > 200) confidence += 0.05;
331
- if (lines < 5) confidence -= 0.1;
332
- return Math.max(0, Math.min(1, confidence));
215
+ return (0, import_core2.calculateHeuristicConfidence)(similarity, tokens, lines);
333
216
  }
334
217
  async function detectDuplicatePatterns(fileContents, options) {
335
218
  const {
@@ -985,84 +868,49 @@ function getPatternIcon(type) {
985
868
  function generateHTMLReport(results, summary) {
986
869
  const data = summary ? { results, summary, metadata: { version: "0.11.22" } } : results;
987
870
  const { metadata } = data;
988
- return `<!DOCTYPE html>
989
- <html lang="en">
990
- <head>
991
- <meta charset="UTF-8">
992
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
993
- <title>AIReady - Pattern Detection Report</title>
994
- <style>
995
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 2rem; background-color: #f9f9f9; }
996
- h1, h2 { color: #1a1a1a; border-bottom: 2px solid #eaeaea; padding-bottom: 0.5rem; }
997
- .card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 2rem; border: 1px solid #eaeaea; }
998
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
999
- .stat-card { background: #fff; padding: 1rem; border-radius: 6px; text-align: center; border: 1px solid #eaeaea; }
1000
- .stat-value { font-size: 1.8rem; font-weight: bold; color: #2563eb; }
1001
- .stat-label { font-size: 0.875rem; color: #666; text-transform: uppercase; }
1002
- table { width: 100%; border-collapse: collapse; margin-top: 1rem; background: white; border-radius: 4px; overflow: hidden; }
1003
- th, td { text-align: left; padding: 0.875rem 1rem; border-bottom: 1px solid #eaeaea; }
1004
- th { background-color: #f8fafc; font-weight: 600; color: #475569; }
1005
- tr:last-child td { border-bottom: none; }
1006
- .critical { color: #dc2626; font-weight: bold; }
1007
- .major { color: #ea580c; font-weight: bold; }
1008
- .minor { color: #2563eb; }
1009
- code { background: #f1f5f9; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.875rem; color: #334155; }
1010
- .footer { margin-top: 4rem; text-align: center; color: #94a3b8; font-size: 0.875rem; }
1011
- </style>
1012
- </head>
871
+ const s = data.summary;
872
+ const head = (0, import_core8.generateReportHead)("AIReady - Pattern Detection Report");
873
+ const score = Math.max(
874
+ 0,
875
+ 100 - Math.round((s.duplicates?.length || 0) / (s.totalFiles || 1) * 20)
876
+ );
877
+ const scoreCard = (0, import_core8.generateScoreCard)(
878
+ `${score}%`,
879
+ "AI Ready Score (Deduplication)"
880
+ );
881
+ const stats = (0, import_core8.generateStatCards)([
882
+ { value: s.totalFiles, label: "Files Analyzed" },
883
+ { value: s.duplicates?.length || 0, label: "Duplicate Clusters" },
884
+ { value: s.totalIssues, label: "Total Issues" }
885
+ ]);
886
+ const tableRows = (s.duplicates || []).map((dup) => [
887
+ `<span class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</span>`,
888
+ dup.patternType,
889
+ dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
890
+ dup.tokenCost.toLocaleString()
891
+ ]);
892
+ const table = (0, import_core8.generateTable)({
893
+ headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
894
+ rows: tableRows
895
+ });
896
+ const body = `${scoreCard}
897
+ ${stats}
898
+ <div class="card">
899
+ <h2>Duplicate Patterns</h2>
900
+ ${table}
901
+ </div>`;
902
+ const footer = (0, import_core8.generateReportFooter)({
903
+ title: "Pattern Detection Report",
904
+ packageName: "pattern-detect",
905
+ packageUrl: "https://github.com/caopengau/aiready-pattern-detect",
906
+ bugUrl: "https://github.com/caopengau/aiready-pattern-detect/issues",
907
+ version: metadata.version
908
+ });
909
+ return `${head}
1013
910
  <body>
1014
911
  <h1>Pattern Detection Report</h1>
1015
- <div class="stat-card" style="margin-bottom: 2rem;">
1016
- <div class="stat-label">AI Ready Score (Deduplication)</div>
1017
- <div class="stat-value">${Math.max(0, 100 - Math.round((summary.duplicates?.length || 0) / (summary.totalFiles || 1) * 20))}%</div>
1018
- </div>
1019
-
1020
- <div class="stats">
1021
- <div class="stat-card">
1022
- <div class="stat-value">${summary.totalFiles}</div>
1023
- <div class="stat-label">Files Analyzed</div>
1024
- </div>
1025
- <div class="stat-card">
1026
- <div class="stat-value">${summary.duplicates?.length || 0}</div>
1027
- <div class="stat-label">Duplicate Clusters</div>
1028
- </div>
1029
- <div class="stat-card">
1030
- <div class="stat-value">${summary.totalIssues}</div>
1031
- <div class="stat-label">Total Issues</div>
1032
- </div>
1033
- </div>
1034
-
1035
- <div class="card">
1036
- <h2>Duplicate Patterns</h2>
1037
- <table>
1038
- <thead>
1039
- <tr>
1040
- <th>Similarity</th>
1041
- <th>Type</th>
1042
- <th>Locations</th>
1043
- <th>Tokens Wasted</th>
1044
- </tr>
1045
- </thead>
1046
- <tbody>
1047
- ${(summary.duplicates || []).map(
1048
- (dup) => `
1049
- <tr>
1050
- <td class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</td>
1051
- <td>${dup.patternType}</td>
1052
- <td>${dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>")}</td>
1053
- <td>${dup.tokenCost.toLocaleString()}</td>
1054
- </tr>
1055
- `
1056
- ).join("")}
1057
- </tbody>
1058
- </table>
1059
- </div>
1060
-
1061
- <div class="footer">
1062
- <p>Generated by <strong>@aiready/pattern-detect</strong> v${metadata.version}</p>
1063
- <p>Like AIReady? <a href="https://github.com/caopengau/aiready-pattern-detect">Star us on GitHub</a></p>
1064
- <p>Found a bug? <a href="https://github.com/caopengau/aiready-pattern-detect/issues">Report it here</a></p>
1065
- </div>
912
+ ${body}
913
+ ${footer}
1066
914
  </body>
1067
915
  </html>`;
1068
916
  }
@@ -1228,12 +1076,7 @@ async function patternActionHandler(directory, options) {
1228
1076
  \u2713 HTML report saved to ${outputPath}`));
1229
1077
  return;
1230
1078
  }
1231
- const terminalWidth = process.stdout.columns || 80;
1232
- const dividerWidth = Math.min(60, terminalWidth - 2);
1233
- const divider = "\u2501".repeat(dividerWidth);
1234
- console.log(import_chalk.default.cyan(divider));
1235
- console.log(import_chalk.default.bold.white(" PATTERN ANALYSIS SUMMARY"));
1236
- console.log(import_chalk.default.cyan(divider) + "\n");
1079
+ (0, import_core9.printTerminalHeader)("PATTERN ANALYSIS SUMMARY");
1237
1080
  console.log(import_chalk.default.white(`\u{1F4C1} Files analyzed: ${import_chalk.default.bold(results.length)}`));
1238
1081
  console.log(
1239
1082
  import_chalk.default.yellow(
@@ -1248,9 +1091,9 @@ async function patternActionHandler(directory, options) {
1248
1091
  console.log(import_chalk.default.gray(`\u23F1 Analysis time: ${import_chalk.default.bold(elapsedTime + "s")}`));
1249
1092
  const sortedTypes = Object.entries(summary.patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
1250
1093
  if (sortedTypes.length > 0) {
1251
- console.log(import_chalk.default.cyan("\n" + divider));
1094
+ console.log("\n" + (0, import_core9.getTerminalDivider)());
1252
1095
  console.log(import_chalk.default.bold.white(" PATTERNS BY TYPE"));
1253
- console.log(import_chalk.default.cyan(divider) + "\n");
1096
+ console.log((0, import_core9.getTerminalDivider)() + "\n");
1254
1097
  sortedTypes.forEach(([type, count]) => {
1255
1098
  const icon = getPatternIcon(type);
1256
1099
  console.log(
@@ -1259,11 +1102,11 @@ async function patternActionHandler(directory, options) {
1259
1102
  });
1260
1103
  }
1261
1104
  if (!finalOptions.showRawDuplicates && groups && groups.length > 0) {
1262
- console.log(import_chalk.default.cyan("\n" + divider));
1105
+ console.log("\n" + (0, import_core9.getTerminalDivider)());
1263
1106
  console.log(
1264
1107
  import_chalk.default.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
1265
1108
  );
1266
- console.log(import_chalk.default.cyan(divider) + "\n");
1109
+ console.log((0, import_core9.getTerminalDivider)() + "\n");
1267
1110
  const topGroups = groups.sort((a, b) => {
1268
1111
  const bVal = (0, import_core9.getSeverityValue)(b.severity);
1269
1112
  const aVal = (0, import_core9.getSeverityValue)(a.severity);
@@ -1304,11 +1147,11 @@ async function patternActionHandler(directory, options) {
1304
1147
  }
1305
1148
  }
1306
1149
  if (!finalOptions.showRawDuplicates && clusters && clusters.length > 0) {
1307
- console.log(import_chalk.default.cyan("\n" + divider));
1150
+ console.log("\n" + (0, import_core9.getTerminalDivider)());
1308
1151
  console.log(
1309
1152
  import_chalk.default.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
1310
1153
  );
1311
- console.log(import_chalk.default.cyan(divider) + "\n");
1154
+ console.log((0, import_core9.getTerminalDivider)() + "\n");
1312
1155
  clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
1313
1156
  const severityBadge = (0, import_core9.getSeverityBadge)(cluster.severity);
1314
1157
  console.log(`${idx + 1}. ${severityBadge} ${import_chalk.default.bold(cluster.name)}`);
@@ -1336,9 +1179,9 @@ async function patternActionHandler(directory, options) {
1336
1179
  });
1337
1180
  }
1338
1181
  if (totalIssues > 0 && (finalOptions.showRawDuplicates || !groups || groups.length === 0)) {
1339
- console.log(import_chalk.default.cyan("\n" + divider));
1182
+ console.log("\n" + (0, import_core9.getTerminalDivider)());
1340
1183
  console.log(import_chalk.default.bold.white(" TOP DUPLICATE PATTERNS"));
1341
- console.log(import_chalk.default.cyan(divider) + "\n");
1184
+ console.log((0, import_core9.getTerminalDivider)() + "\n");
1342
1185
  const topDuplicates = filteredDuplicates.sort((a, b) => {
1343
1186
  const bVal = (0, import_core9.getSeverityValue)(b.severity);
1344
1187
  const aVal = (0, import_core9.getSeverityValue)(a.severity);
@@ -1385,9 +1228,9 @@ async function patternActionHandler(directory, options) {
1385
1228
  (issue) => (0, import_core9.getSeverityValue)(issue.severity) === 4
1386
1229
  );
1387
1230
  if (criticalIssues.length > 0) {
1388
- console.log(import_chalk.default.cyan(divider));
1231
+ console.log((0, import_core9.getTerminalDivider)());
1389
1232
  console.log(import_chalk.default.bold.white(" CRITICAL ISSUES (>95% similar)"));
1390
- console.log(import_chalk.default.cyan(divider) + "\n");
1233
+ console.log((0, import_core9.getTerminalDivider)() + "\n");
1391
1234
  criticalIssues.slice(0, 5).forEach((issue) => {
1392
1235
  console.log(
1393
1236
  import_chalk.default.red("\u25CF ") + import_chalk.default.white(`${issue.file}:${issue.location.line}`)
@@ -1424,7 +1267,7 @@ async function patternActionHandler(directory, options) {
1424
1267
  );
1425
1268
  console.log("");
1426
1269
  }
1427
- console.log(import_chalk.default.cyan(divider));
1270
+ console.log((0, import_core9.getTerminalDivider)());
1428
1271
  if (totalIssues > 0) {
1429
1272
  console.log(
1430
1273
  import_chalk.default.white(
package/dist/cli.mjs CHANGED
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-6JTVOBJX.mjs";
2
3
  import {
3
4
  analyzePatterns,
4
- filterBySeverity,
5
5
  generateSummary
6
- } from "./chunk-BUBQ3W6W.mjs";
6
+ } from "./chunk-DNZS4ESD.mjs";
7
+ import "./chunk-VGMM3L3O.mjs";
8
+ import {
9
+ filterBySeverity
10
+ } from "./chunk-I6ETJC7L.mjs";
11
+ import "./chunk-WBBO35SC.mjs";
7
12
 
8
13
  // src/cli.ts
9
14
  import { Command } from "commander";
@@ -18,11 +23,21 @@ import {
18
23
  resolveOutputPath,
19
24
  Severity,
20
25
  getSeverityBadge as getSeverityBadge2,
21
- getSeverityValue as getSeverityValue2
26
+ getSeverityValue as getSeverityValue2,
27
+ printTerminalHeader,
28
+ getTerminalDivider
22
29
  } from "@aiready/core";
23
30
 
24
31
  // src/cli-output.ts
25
- import { getSeverityBadge, getSeverityValue } from "@aiready/core";
32
+ import {
33
+ getSeverityBadge,
34
+ getSeverityValue,
35
+ generateReportHead,
36
+ generateStatCards,
37
+ generateScoreCard,
38
+ generateTable,
39
+ generateReportFooter
40
+ } from "@aiready/core";
26
41
  function getPatternIcon(type) {
27
42
  const icons = {
28
43
  "api-handler": "\u{1F50C}",
@@ -38,84 +53,49 @@ function getPatternIcon(type) {
38
53
  function generateHTMLReport(results, summary) {
39
54
  const data = summary ? { results, summary, metadata: { version: "0.11.22" } } : results;
40
55
  const { metadata } = data;
41
- return `<!DOCTYPE html>
42
- <html lang="en">
43
- <head>
44
- <meta charset="UTF-8">
45
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
- <title>AIReady - Pattern Detection Report</title>
47
- <style>
48
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 2rem; background-color: #f9f9f9; }
49
- h1, h2 { color: #1a1a1a; border-bottom: 2px solid #eaeaea; padding-bottom: 0.5rem; }
50
- .card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 2rem; border: 1px solid #eaeaea; }
51
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
52
- .stat-card { background: #fff; padding: 1rem; border-radius: 6px; text-align: center; border: 1px solid #eaeaea; }
53
- .stat-value { font-size: 1.8rem; font-weight: bold; color: #2563eb; }
54
- .stat-label { font-size: 0.875rem; color: #666; text-transform: uppercase; }
55
- table { width: 100%; border-collapse: collapse; margin-top: 1rem; background: white; border-radius: 4px; overflow: hidden; }
56
- th, td { text-align: left; padding: 0.875rem 1rem; border-bottom: 1px solid #eaeaea; }
57
- th { background-color: #f8fafc; font-weight: 600; color: #475569; }
58
- tr:last-child td { border-bottom: none; }
59
- .critical { color: #dc2626; font-weight: bold; }
60
- .major { color: #ea580c; font-weight: bold; }
61
- .minor { color: #2563eb; }
62
- code { background: #f1f5f9; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.875rem; color: #334155; }
63
- .footer { margin-top: 4rem; text-align: center; color: #94a3b8; font-size: 0.875rem; }
64
- </style>
65
- </head>
56
+ const s = data.summary;
57
+ const head = generateReportHead("AIReady - Pattern Detection Report");
58
+ const score = Math.max(
59
+ 0,
60
+ 100 - Math.round((s.duplicates?.length || 0) / (s.totalFiles || 1) * 20)
61
+ );
62
+ const scoreCard = generateScoreCard(
63
+ `${score}%`,
64
+ "AI Ready Score (Deduplication)"
65
+ );
66
+ const stats = generateStatCards([
67
+ { value: s.totalFiles, label: "Files Analyzed" },
68
+ { value: s.duplicates?.length || 0, label: "Duplicate Clusters" },
69
+ { value: s.totalIssues, label: "Total Issues" }
70
+ ]);
71
+ const tableRows = (s.duplicates || []).map((dup) => [
72
+ `<span class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</span>`,
73
+ dup.patternType,
74
+ dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
75
+ dup.tokenCost.toLocaleString()
76
+ ]);
77
+ const table = generateTable({
78
+ headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
79
+ rows: tableRows
80
+ });
81
+ const body = `${scoreCard}
82
+ ${stats}
83
+ <div class="card">
84
+ <h2>Duplicate Patterns</h2>
85
+ ${table}
86
+ </div>`;
87
+ const footer = generateReportFooter({
88
+ title: "Pattern Detection Report",
89
+ packageName: "pattern-detect",
90
+ packageUrl: "https://github.com/caopengau/aiready-pattern-detect",
91
+ bugUrl: "https://github.com/caopengau/aiready-pattern-detect/issues",
92
+ version: metadata.version
93
+ });
94
+ return `${head}
66
95
  <body>
67
96
  <h1>Pattern Detection Report</h1>
68
- <div class="stat-card" style="margin-bottom: 2rem;">
69
- <div class="stat-label">AI Ready Score (Deduplication)</div>
70
- <div class="stat-value">${Math.max(0, 100 - Math.round((summary.duplicates?.length || 0) / (summary.totalFiles || 1) * 20))}%</div>
71
- </div>
72
-
73
- <div class="stats">
74
- <div class="stat-card">
75
- <div class="stat-value">${summary.totalFiles}</div>
76
- <div class="stat-label">Files Analyzed</div>
77
- </div>
78
- <div class="stat-card">
79
- <div class="stat-value">${summary.duplicates?.length || 0}</div>
80
- <div class="stat-label">Duplicate Clusters</div>
81
- </div>
82
- <div class="stat-card">
83
- <div class="stat-value">${summary.totalIssues}</div>
84
- <div class="stat-label">Total Issues</div>
85
- </div>
86
- </div>
87
-
88
- <div class="card">
89
- <h2>Duplicate Patterns</h2>
90
- <table>
91
- <thead>
92
- <tr>
93
- <th>Similarity</th>
94
- <th>Type</th>
95
- <th>Locations</th>
96
- <th>Tokens Wasted</th>
97
- </tr>
98
- </thead>
99
- <tbody>
100
- ${(summary.duplicates || []).map(
101
- (dup) => `
102
- <tr>
103
- <td class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</td>
104
- <td>${dup.patternType}</td>
105
- <td>${dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>")}</td>
106
- <td>${dup.tokenCost.toLocaleString()}</td>
107
- </tr>
108
- `
109
- ).join("")}
110
- </tbody>
111
- </table>
112
- </div>
113
-
114
- <div class="footer">
115
- <p>Generated by <strong>@aiready/pattern-detect</strong> v${metadata.version}</p>
116
- <p>Like AIReady? <a href="https://github.com/caopengau/aiready-pattern-detect">Star us on GitHub</a></p>
117
- <p>Found a bug? <a href="https://github.com/caopengau/aiready-pattern-detect/issues">Report it here</a></p>
118
- </div>
97
+ ${body}
98
+ ${footer}
119
99
  </body>
120
100
  </html>`;
121
101
  }
@@ -281,12 +261,7 @@ async function patternActionHandler(directory, options) {
281
261
  \u2713 HTML report saved to ${outputPath}`));
282
262
  return;
283
263
  }
284
- const terminalWidth = process.stdout.columns || 80;
285
- const dividerWidth = Math.min(60, terminalWidth - 2);
286
- const divider = "\u2501".repeat(dividerWidth);
287
- console.log(chalk.cyan(divider));
288
- console.log(chalk.bold.white(" PATTERN ANALYSIS SUMMARY"));
289
- console.log(chalk.cyan(divider) + "\n");
264
+ printTerminalHeader("PATTERN ANALYSIS SUMMARY");
290
265
  console.log(chalk.white(`\u{1F4C1} Files analyzed: ${chalk.bold(results.length)}`));
291
266
  console.log(
292
267
  chalk.yellow(
@@ -301,9 +276,9 @@ async function patternActionHandler(directory, options) {
301
276
  console.log(chalk.gray(`\u23F1 Analysis time: ${chalk.bold(elapsedTime + "s")}`));
302
277
  const sortedTypes = Object.entries(summary.patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
303
278
  if (sortedTypes.length > 0) {
304
- console.log(chalk.cyan("\n" + divider));
279
+ console.log("\n" + getTerminalDivider());
305
280
  console.log(chalk.bold.white(" PATTERNS BY TYPE"));
306
- console.log(chalk.cyan(divider) + "\n");
281
+ console.log(getTerminalDivider() + "\n");
307
282
  sortedTypes.forEach(([type, count]) => {
308
283
  const icon = getPatternIcon(type);
309
284
  console.log(
@@ -312,11 +287,11 @@ async function patternActionHandler(directory, options) {
312
287
  });
313
288
  }
314
289
  if (!finalOptions.showRawDuplicates && groups && groups.length > 0) {
315
- console.log(chalk.cyan("\n" + divider));
290
+ console.log("\n" + getTerminalDivider());
316
291
  console.log(
317
292
  chalk.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
318
293
  );
319
- console.log(chalk.cyan(divider) + "\n");
294
+ console.log(getTerminalDivider() + "\n");
320
295
  const topGroups = groups.sort((a, b) => {
321
296
  const bVal = getSeverityValue2(b.severity);
322
297
  const aVal = getSeverityValue2(a.severity);
@@ -357,11 +332,11 @@ async function patternActionHandler(directory, options) {
357
332
  }
358
333
  }
359
334
  if (!finalOptions.showRawDuplicates && clusters && clusters.length > 0) {
360
- console.log(chalk.cyan("\n" + divider));
335
+ console.log("\n" + getTerminalDivider());
361
336
  console.log(
362
337
  chalk.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
363
338
  );
364
- console.log(chalk.cyan(divider) + "\n");
339
+ console.log(getTerminalDivider() + "\n");
365
340
  clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
366
341
  const severityBadge = getSeverityBadge2(cluster.severity);
367
342
  console.log(`${idx + 1}. ${severityBadge} ${chalk.bold(cluster.name)}`);
@@ -389,9 +364,9 @@ async function patternActionHandler(directory, options) {
389
364
  });
390
365
  }
391
366
  if (totalIssues > 0 && (finalOptions.showRawDuplicates || !groups || groups.length === 0)) {
392
- console.log(chalk.cyan("\n" + divider));
367
+ console.log("\n" + getTerminalDivider());
393
368
  console.log(chalk.bold.white(" TOP DUPLICATE PATTERNS"));
394
- console.log(chalk.cyan(divider) + "\n");
369
+ console.log(getTerminalDivider() + "\n");
395
370
  const topDuplicates = filteredDuplicates.sort((a, b) => {
396
371
  const bVal = getSeverityValue2(b.severity);
397
372
  const aVal = getSeverityValue2(a.severity);
@@ -438,9 +413,9 @@ async function patternActionHandler(directory, options) {
438
413
  (issue) => getSeverityValue2(issue.severity) === 4
439
414
  );
440
415
  if (criticalIssues.length > 0) {
441
- console.log(chalk.cyan(divider));
416
+ console.log(getTerminalDivider());
442
417
  console.log(chalk.bold.white(" CRITICAL ISSUES (>95% similar)"));
443
- console.log(chalk.cyan(divider) + "\n");
418
+ console.log(getTerminalDivider() + "\n");
444
419
  criticalIssues.slice(0, 5).forEach((issue) => {
445
420
  console.log(
446
421
  chalk.red("\u25CF ") + chalk.white(`${issue.file}:${issue.location.line}`)
@@ -477,7 +452,7 @@ async function patternActionHandler(directory, options) {
477
452
  );
478
453
  console.log("");
479
454
  }
480
- console.log(chalk.cyan(divider));
455
+ console.log(getTerminalDivider());
481
456
  if (totalIssues > 0) {
482
457
  console.log(
483
458
  chalk.white(