opencroc 0.5.0 → 0.6.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/README.en.md CHANGED
@@ -294,7 +294,7 @@ export default defineConfig({
294
294
  - [x] Plugin system
295
295
  - [x] HTML / JSON / Markdown report generation
296
296
  - [x] NestJS controller parser
297
- - [ ] Visual dashboard (opencroc.com)
297
+ - [x] Visual dashboard (opencroc.com)
298
298
  - [x] Drizzle ORM adapter
299
299
 
300
300
  ## Documentation
package/README.ja.md CHANGED
@@ -294,7 +294,7 @@ export default defineConfig({
294
294
  - [x] Plugin system
295
295
  - [x] HTML / JSON / Markdown report generation
296
296
  - [x] NestJS controller parser
297
- - [ ] Visual dashboard (opencroc.com)
297
+ - [x] Visual dashboard (opencroc.com)
298
298
  - [x] Drizzle ORM adapter
299
299
 
300
300
  ## ドキュメント
package/README.md CHANGED
@@ -294,7 +294,7 @@ export default defineConfig({
294
294
  - [x] Plugin system
295
295
  - [x] HTML / JSON / Markdown report generation
296
296
  - [x] NestJS controller parser
297
- - [ ] Visual dashboard (opencroc.com)
297
+ - [x] Visual dashboard (opencroc.com)
298
298
  - [x] Drizzle ORM adapter
299
299
 
300
300
  ## Documentation
package/README.zh-CN.md CHANGED
@@ -294,7 +294,7 @@ export default defineConfig({
294
294
  - [x] Plugin system
295
295
  - [x] HTML / JSON / Markdown report generation
296
296
  - [x] NestJS controller parser
297
- - [ ] Visual dashboard (opencroc.com)
297
+ - [x] Visual dashboard (opencroc.com)
298
298
  - [x] Drizzle ORM adapter
299
299
 
300
300
  ## 文档
package/dist/cli/index.js CHANGED
@@ -853,20 +853,20 @@ function detectCycles(dag) {
853
853
  const color = /* @__PURE__ */ new Map();
854
854
  for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
855
855
  const warnings = [];
856
- const path8 = [];
856
+ const path9 = [];
857
857
  function dfs(node) {
858
858
  color.set(node, 1 /* GRAY */);
859
- path8.push(node);
859
+ path9.push(node);
860
860
  for (const neighbor of adjacency.get(node) || []) {
861
861
  const nc = color.get(neighbor);
862
862
  if (nc === 1 /* GRAY */) {
863
- const cycleStart = path8.indexOf(neighbor);
864
- warnings.push(`Cycle detected: ${path8.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
863
+ const cycleStart = path9.indexOf(neighbor);
864
+ warnings.push(`Cycle detected: ${path9.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
865
865
  } else if (nc === 0 /* WHITE */) {
866
866
  dfs(neighbor);
867
867
  }
868
868
  }
869
- path8.pop();
869
+ path9.pop();
870
870
  color.set(node, 2 /* BLACK */);
871
871
  }
872
872
  for (const node of dag.nodes) {
@@ -2127,11 +2127,369 @@ var init_report = __esm({
2127
2127
  }
2128
2128
  });
2129
2129
 
2130
+ // src/dashboard/index.ts
2131
+ function escapeHtml2(s) {
2132
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2133
+ }
2134
+ function number(v) {
2135
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
2136
+ }
2137
+ function buildDashboardDataFromPipeline(result) {
2138
+ const moduleCards = result.modules.map((mod) => {
2139
+ const er = result.erDiagrams.get(mod);
2140
+ const plan = result.chainPlans.get(mod);
2141
+ return {
2142
+ module: mod,
2143
+ tables: er?.tables.length ?? 0,
2144
+ relations: er?.relations.length ?? 0,
2145
+ chains: plan?.chains.length ?? 0,
2146
+ steps: plan?.totalSteps ?? 0
2147
+ };
2148
+ });
2149
+ const totals = {
2150
+ modules: result.modules.length,
2151
+ tables: moduleCards.reduce((s, m) => s + m.tables, 0),
2152
+ relations: moduleCards.reduce((s, m) => s + m.relations, 0),
2153
+ chains: moduleCards.reduce((s, m) => s + m.chains, 0),
2154
+ steps: moduleCards.reduce((s, m) => s + m.steps, 0),
2155
+ files: result.generatedFiles.length,
2156
+ errors: result.validationErrors.filter((e) => e.severity === "error").length,
2157
+ warnings: result.validationErrors.filter((e) => e.severity === "warning").length
2158
+ };
2159
+ return {
2160
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2161
+ durationMs: result.duration,
2162
+ modules: [...result.modules],
2163
+ totals,
2164
+ moduleCards,
2165
+ files: result.generatedFiles.map((f) => ({ filePath: f.filePath, module: f.module, chain: f.chain })),
2166
+ issues: result.validationErrors.map((e) => ({
2167
+ severity: e.severity,
2168
+ module: e.module,
2169
+ field: e.field,
2170
+ message: e.message
2171
+ }))
2172
+ };
2173
+ }
2174
+ function buildDashboardDataFromReportJson(input) {
2175
+ const src = input ?? {};
2176
+ const modules = Array.isArray(src.modules) ? src.modules.map((m) => String(m)) : [];
2177
+ const er = src.erDiagrams ?? {};
2178
+ const plans = src.chainPlans ?? {};
2179
+ const files = Array.isArray(src.generatedFiles) ? src.generatedFiles.map((f) => {
2180
+ const row = f;
2181
+ return {
2182
+ filePath: String(row.filePath ?? ""),
2183
+ module: String(row.module ?? ""),
2184
+ chain: String(row.chain ?? "")
2185
+ };
2186
+ }) : [];
2187
+ const rawIssues = Array.isArray(src.validationErrors) ? src.validationErrors : [];
2188
+ const issues = rawIssues.map((item) => {
2189
+ const row = item;
2190
+ return {
2191
+ severity: String(row.severity ?? "warning"),
2192
+ module: String(row.module ?? "unknown"),
2193
+ field: String(row.field ?? "unknown"),
2194
+ message: String(row.message ?? "")
2195
+ };
2196
+ });
2197
+ const moduleCards = modules.map((mod) => ({
2198
+ module: mod,
2199
+ tables: number(er[mod]?.tables),
2200
+ relations: number(er[mod]?.relations),
2201
+ chains: number(plans[mod]?.chains),
2202
+ steps: number(plans[mod]?.totalSteps)
2203
+ }));
2204
+ return {
2205
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2206
+ durationMs: number(src.duration),
2207
+ modules,
2208
+ totals: {
2209
+ modules: modules.length,
2210
+ tables: moduleCards.reduce((s, m) => s + m.tables, 0),
2211
+ relations: moduleCards.reduce((s, m) => s + m.relations, 0),
2212
+ chains: moduleCards.reduce((s, m) => s + m.chains, 0),
2213
+ steps: moduleCards.reduce((s, m) => s + m.steps, 0),
2214
+ files: files.length,
2215
+ errors: issues.filter((i) => i.severity === "error").length,
2216
+ warnings: issues.filter((i) => i.severity === "warning").length
2217
+ },
2218
+ moduleCards,
2219
+ files,
2220
+ issues
2221
+ };
2222
+ }
2223
+ function generateVisualDashboardHtml(data) {
2224
+ const moduleCardHtml = data.moduleCards.map(
2225
+ (m) => `<article class="module-card reveal">
2226
+ <h3>${escapeHtml2(m.module)}</h3>
2227
+ <div class="meta">${m.tables} tables \xB7 ${m.relations} relations</div>
2228
+ <div class="bars">
2229
+ <div class="bar"><span>Chains</span><strong>${m.chains}</strong></div>
2230
+ <div class="bar"><span>Steps</span><strong>${m.steps}</strong></div>
2231
+ </div>
2232
+ </article>`
2233
+ ).join("\n");
2234
+ const fileRows = data.files.slice(0, 20).map(
2235
+ (f) => `<tr><td><code>${escapeHtml2(f.filePath)}</code></td><td>${escapeHtml2(f.module)}</td><td>${escapeHtml2(f.chain)}</td></tr>`
2236
+ ).join("\n");
2237
+ const issueRows = data.issues.map(
2238
+ (i) => `<tr class="${escapeHtml2(i.severity)}"><td>${escapeHtml2(i.severity)}</td><td>${escapeHtml2(i.module)}</td><td>${escapeHtml2(i.field)}</td><td>${escapeHtml2(i.message)}</td></tr>`
2239
+ ).join("\n");
2240
+ return `<!DOCTYPE html>
2241
+ <html lang="en">
2242
+ <head>
2243
+ <meta charset="utf-8" />
2244
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2245
+ <title>OpenCroc Visual Dashboard</title>
2246
+ <style>
2247
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
2248
+ :root {
2249
+ --bg: #f4efe6;
2250
+ --ink: #18222c;
2251
+ --card: #fff8ef;
2252
+ --line: #d8c7b4;
2253
+ --accent: #0b7a75;
2254
+ --accent-2: #f26a2e;
2255
+ --ok: #2f7d32;
2256
+ --warn: #b7791f;
2257
+ --err: #c0392b;
2258
+ }
2259
+ * { box-sizing: border-box; }
2260
+ body {
2261
+ margin: 0;
2262
+ font-family: 'Space Grotesk', 'Segoe UI', sans-serif;
2263
+ color: var(--ink);
2264
+ background:
2265
+ radial-gradient(circle at 15% 10%, #f9e2c5 0%, transparent 32%),
2266
+ radial-gradient(circle at 85% 0%, #d3efe4 0%, transparent 28%),
2267
+ var(--bg);
2268
+ min-height: 100vh;
2269
+ }
2270
+ .wrap { max-width: 1160px; margin: 0 auto; padding: 24px 18px 40px; }
2271
+ .hero {
2272
+ border: 2px solid var(--line);
2273
+ border-radius: 18px;
2274
+ background: linear-gradient(130deg, #fff8ef 0%, #f8f2ea 45%, #f3ece3 100%);
2275
+ padding: 22px;
2276
+ box-shadow: 0 12px 24px rgba(24, 34, 44, 0.08);
2277
+ position: relative;
2278
+ overflow: hidden;
2279
+ }
2280
+ .hero::after {
2281
+ content: '';
2282
+ position: absolute;
2283
+ right: -42px;
2284
+ top: -38px;
2285
+ width: 160px;
2286
+ height: 160px;
2287
+ border-radius: 50%;
2288
+ background: conic-gradient(from 50deg, #f26a2e 0deg, #f7a35a 90deg, #0b7a75 220deg, #f26a2e 360deg);
2289
+ opacity: 0.14;
2290
+ }
2291
+ h1 { margin: 0; font-size: clamp(1.6rem, 3vw, 2.5rem); letter-spacing: -0.03em; }
2292
+ .subtitle { margin-top: 8px; font-size: 0.95rem; opacity: 0.82; }
2293
+ .kpi-grid {
2294
+ margin-top: 16px;
2295
+ display: grid;
2296
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
2297
+ gap: 10px;
2298
+ }
2299
+ .kpi {
2300
+ border: 1px solid var(--line);
2301
+ border-radius: 12px;
2302
+ padding: 10px 12px;
2303
+ background: #fffdf9;
2304
+ }
2305
+ .kpi .label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.7; }
2306
+ .kpi .value { font-size: 1.45rem; font-weight: 700; margin-top: 4px; }
2307
+ .kpi.error .value { color: var(--err); }
2308
+ .kpi.warning .value { color: var(--warn); }
2309
+ .kpi.files .value { color: var(--accent); }
2310
+
2311
+ .section-title {
2312
+ margin: 24px 0 10px;
2313
+ font-size: 1rem;
2314
+ text-transform: uppercase;
2315
+ letter-spacing: 0.08em;
2316
+ color: #344250;
2317
+ }
2318
+
2319
+ .module-grid {
2320
+ display: grid;
2321
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2322
+ gap: 12px;
2323
+ }
2324
+ .module-card {
2325
+ border: 1px solid var(--line);
2326
+ border-radius: 14px;
2327
+ padding: 14px;
2328
+ background: var(--card);
2329
+ box-shadow: 0 8px 16px rgba(24, 34, 44, 0.06);
2330
+ }
2331
+ .module-card h3 { margin: 0 0 6px; font-size: 1.05rem; }
2332
+ .module-card .meta { font-size: 0.85rem; opacity: 0.78; }
2333
+ .bars { margin-top: 10px; display: grid; gap: 8px; }
2334
+ .bar { display: flex; justify-content: space-between; border-top: 1px dashed var(--line); padding-top: 7px; }
2335
+
2336
+ table {
2337
+ width: 100%;
2338
+ border-collapse: collapse;
2339
+ border: 1px solid var(--line);
2340
+ border-radius: 12px;
2341
+ overflow: hidden;
2342
+ background: #fffdf9;
2343
+ font-family: 'IBM Plex Mono', 'Consolas', monospace;
2344
+ font-size: 0.82rem;
2345
+ }
2346
+ thead { background: #efe5d8; }
2347
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #eadfce; }
2348
+ tbody tr:last-child td { border-bottom: none; }
2349
+ tr.error td:first-child { color: var(--err); font-weight: 700; }
2350
+ tr.warning td:first-child { color: var(--warn); font-weight: 700; }
2351
+
2352
+ code {
2353
+ font-family: 'IBM Plex Mono', 'Consolas', monospace;
2354
+ background: #f5ece0;
2355
+ border: 1px solid #eadfce;
2356
+ border-radius: 6px;
2357
+ padding: 1px 5px;
2358
+ }
2359
+
2360
+ .reveal { opacity: 0; transform: translateY(12px); animation: rise .55s ease forwards; }
2361
+ .module-card.reveal:nth-child(2) { animation-delay: .06s; }
2362
+ .module-card.reveal:nth-child(3) { animation-delay: .12s; }
2363
+ .module-card.reveal:nth-child(4) { animation-delay: .18s; }
2364
+ .module-card.reveal:nth-child(5) { animation-delay: .24s; }
2365
+ @keyframes rise {
2366
+ to { opacity: 1; transform: translateY(0); }
2367
+ }
2368
+
2369
+ @media (max-width: 700px) {
2370
+ .wrap { padding: 14px 12px 28px; }
2371
+ .hero { padding: 16px; }
2372
+ table { font-size: 0.76rem; }
2373
+ th, td { padding: 7px 8px; }
2374
+ }
2375
+ </style>
2376
+ </head>
2377
+ <body>
2378
+ <main class="wrap">
2379
+ <section class="hero reveal">
2380
+ <h1>OpenCroc Visual Dashboard</h1>
2381
+ <p class="subtitle">Pipeline finished in ${data.durationMs}ms \xB7 Generated ${escapeHtml2(data.generatedAt)}</p>
2382
+ <div class="kpi-grid">
2383
+ <div class="kpi"><div class="label">Modules</div><div class="value">${data.totals.modules}</div></div>
2384
+ <div class="kpi"><div class="label">Tables</div><div class="value">${data.totals.tables}</div></div>
2385
+ <div class="kpi"><div class="label">Relations</div><div class="value">${data.totals.relations}</div></div>
2386
+ <div class="kpi"><div class="label">Chains</div><div class="value">${data.totals.chains}</div></div>
2387
+ <div class="kpi"><div class="label">Steps</div><div class="value">${data.totals.steps}</div></div>
2388
+ <div class="kpi files"><div class="label">Files</div><div class="value">${data.totals.files}</div></div>
2389
+ <div class="kpi error"><div class="label">Errors</div><div class="value">${data.totals.errors}</div></div>
2390
+ <div class="kpi warning"><div class="label">Warnings</div><div class="value">${data.totals.warnings}</div></div>
2391
+ </div>
2392
+ </section>
2393
+
2394
+ <h2 class="section-title">Module Health</h2>
2395
+ <section class="module-grid">
2396
+ ${moduleCardHtml || '<article class="module-card">No module data</article>'}
2397
+ </section>
2398
+
2399
+ <h2 class="section-title">Generated Files (Top 20)</h2>
2400
+ <section>
2401
+ <table>
2402
+ <thead><tr><th>File</th><th>Module</th><th>Chain</th></tr></thead>
2403
+ <tbody>${fileRows || '<tr><td colspan="3">No files generated</td></tr>'}</tbody>
2404
+ </table>
2405
+ </section>
2406
+
2407
+ <h2 class="section-title">Validation Issues</h2>
2408
+ <section>
2409
+ <table>
2410
+ <thead><tr><th>Severity</th><th>Module</th><th>Field</th><th>Message</th></tr></thead>
2411
+ <tbody>${issueRows || '<tr><td colspan="4">No validation issues</td></tr>'}</tbody>
2412
+ </table>
2413
+ </section>
2414
+ </main>
2415
+ </body>
2416
+ </html>`;
2417
+ }
2418
+ var init_dashboard = __esm({
2419
+ "src/dashboard/index.ts"() {
2420
+ "use strict";
2421
+ init_esm_shims();
2422
+ }
2423
+ });
2424
+
2425
+ // src/cli/commands/dashboard.ts
2426
+ var dashboard_exports = {};
2427
+ __export(dashboard_exports, {
2428
+ dashboard: () => dashboard
2429
+ });
2430
+ import * as fs7 from "fs";
2431
+ import * as path8 from "path";
2432
+ import chalk8 from "chalk";
2433
+ async function dashboard(opts) {
2434
+ let dashboardHtml;
2435
+ if (opts.input) {
2436
+ const inputPath = path8.resolve(opts.input);
2437
+ if (!fs7.existsSync(inputPath)) {
2438
+ console.error(chalk8.red(`Input report not found: ${inputPath}`));
2439
+ process.exitCode = 1;
2440
+ return;
2441
+ }
2442
+ const raw = fs7.readFileSync(inputPath, "utf-8");
2443
+ let parsed;
2444
+ try {
2445
+ parsed = JSON.parse(raw);
2446
+ } catch {
2447
+ console.error(chalk8.red("Invalid JSON input. Please provide opencroc-report.json output."));
2448
+ process.exitCode = 1;
2449
+ return;
2450
+ }
2451
+ const data = buildDashboardDataFromReportJson(parsed);
2452
+ dashboardHtml = generateVisualDashboardHtml(data);
2453
+ console.log(chalk8.cyan(`Building visual dashboard from ${inputPath}...`));
2454
+ } else {
2455
+ let loaded;
2456
+ try {
2457
+ loaded = await loadConfig();
2458
+ } catch {
2459
+ console.error(chalk8.red("No opencroc config found. Run `opencroc init` first."));
2460
+ process.exitCode = 1;
2461
+ return;
2462
+ }
2463
+ const { config } = loaded;
2464
+ console.log(chalk8.cyan("Running pipeline to build visual dashboard..."));
2465
+ const pipeline = createPipeline(config);
2466
+ const result = await pipeline.run();
2467
+ const data = buildDashboardDataFromPipeline(result);
2468
+ dashboardHtml = generateVisualDashboardHtml(data);
2469
+ }
2470
+ const outDir = opts.output ? path8.resolve(opts.output) : path8.resolve("./opencroc-output");
2471
+ if (!fs7.existsSync(outDir)) {
2472
+ fs7.mkdirSync(outDir, { recursive: true });
2473
+ }
2474
+ const outPath = path8.join(outDir, "opencroc-dashboard.html");
2475
+ fs7.writeFileSync(outPath, dashboardHtml, "utf-8");
2476
+ console.log(chalk8.green(`\u2714 visual dashboard \u2192 ${outPath}`));
2477
+ }
2478
+ var init_dashboard2 = __esm({
2479
+ "src/cli/commands/dashboard.ts"() {
2480
+ "use strict";
2481
+ init_esm_shims();
2482
+ init_load_config();
2483
+ init_pipeline();
2484
+ init_dashboard();
2485
+ }
2486
+ });
2487
+
2130
2488
  // src/cli/index.ts
2131
2489
  init_esm_shims();
2132
2490
  import { Command } from "commander";
2133
2491
  var program = new Command();
2134
- program.name("opencroc").description("AI-native E2E testing framework").version("0.5.0");
2492
+ program.name("opencroc").description("AI-native E2E testing framework").version("0.6.0");
2135
2493
  program.command("init").description("Initialize OpenCroc in the current project").option("-y, --yes", "Skip prompts and use defaults").action(async (opts) => {
2136
2494
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2137
2495
  await initProject2(opts);
@@ -2160,5 +2518,9 @@ program.command("report").description("Generate pipeline report (HTML/JSON/Markd
2160
2518
  const { report: report2 } = await Promise.resolve().then(() => (init_report(), report_exports));
2161
2519
  await report2(opts);
2162
2520
  });
2521
+ program.command("dashboard").description("Generate visual dashboard (opencroc-dashboard.html)").option("-i, --input <file>", "Build from existing opencroc-report.json file").option("-o, --output <dir>", "Output directory", "./opencroc-output").action(async (opts) => {
2522
+ const { dashboard: dashboard2 } = await Promise.resolve().then(() => (init_dashboard2(), dashboard_exports));
2523
+ await dashboard2(opts);
2524
+ });
2163
2525
  program.parse();
2164
2526
  //# sourceMappingURL=index.js.map