opencroc 1.5.1 → 1.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/dist/cli/index.js CHANGED
@@ -1729,6 +1729,7 @@ var init_ollama = __esm({
1729
1729
  });
1730
1730
 
1731
1731
  // src/llm/index.ts
1732
+ var SYSTEM_PROMPTS;
1732
1733
  var init_llm = __esm({
1733
1734
  "src/llm/index.ts"() {
1734
1735
  "use strict";
@@ -1737,36 +1738,336 @@ var init_llm = __esm({
1737
1738
  init_ollama();
1738
1739
  init_openai();
1739
1740
  init_ollama();
1741
+ SYSTEM_PROMPTS = {
1742
+ failureAnalysis: `You are an expert test failure analyst for an E2E testing framework.
1743
+ Given a test failure error message and its context, analyze the root cause and suggest a fix.
1744
+ Respond in JSON format: { "rootCause": string, "category": string, "suggestedFix": string, "confidence": number }
1745
+ Categories: backend-5xx, timeout, endpoint-not-found, data-constraint, network, frontend-render, test-script, unknown.`,
1746
+ chainPlanning: `You are an API test chain planner.
1747
+ Given a list of API endpoints and their dependencies, generate an optimal test execution order.
1748
+ Consider data dependencies, authentication requirements, and cleanup steps.
1749
+ Respond in JSON format: { "chains": [{ "name": string, "steps": [{ "endpoint": string, "method": string, "description": string }] }] }`
1750
+ };
1740
1751
  }
1741
1752
  });
1742
1753
 
1743
1754
  // src/self-healing/dialog-loop-runner.ts
1755
+ function createJsonResultParser() {
1756
+ return {
1757
+ parse(stdout2) {
1758
+ const failures = [];
1759
+ const lines = stdout2.split("\n");
1760
+ for (const line of lines) {
1761
+ const failMatch = line.match(/[✘✗×]\s+.*?›\s+(.+)/);
1762
+ if (failMatch) {
1763
+ failures.push({
1764
+ title: failMatch[1].trim(),
1765
+ error: failMatch[1].trim()
1766
+ });
1767
+ }
1768
+ }
1769
+ return failures;
1770
+ },
1771
+ countTotal(stdout2) {
1772
+ let total = 0;
1773
+ const passMatch = stdout2.match(/(\d+)\s+passed/);
1774
+ const failMatch = stdout2.match(/(\d+)\s+failed/);
1775
+ if (passMatch) total += parseInt(passMatch[1], 10);
1776
+ if (failMatch) total += parseInt(failMatch[1], 10);
1777
+ return total || 1;
1778
+ }
1779
+ };
1780
+ }
1781
+ async function runDialogLoop(options) {
1782
+ const cfg = { ...DEFAULTS2, ...options.config };
1783
+ const { runner, parser, fixer } = options;
1784
+ const history = [];
1785
+ const errorTracker = /* @__PURE__ */ new Map();
1786
+ for (let iteration = 1; iteration <= cfg.maxIterations + 1; iteration++) {
1787
+ const iterStart = Date.now();
1788
+ const { stdout: stdout2 } = await runner.run();
1789
+ const failures = parser.parse(stdout2);
1790
+ const totalTests = parser.countTotal(stdout2);
1791
+ const passed = totalTests - failures.length;
1792
+ const iterResult = {
1793
+ iteration,
1794
+ totalTests,
1795
+ passed,
1796
+ failed: failures.length,
1797
+ failedTests: failures.map((f) => f.title),
1798
+ fixesApplied: [],
1799
+ durationMs: 0
1800
+ };
1801
+ if (failures.length === 0) {
1802
+ iterResult.durationMs = Date.now() - iterStart;
1803
+ history.push(iterResult);
1804
+ options.onIteration?.(iterResult);
1805
+ break;
1806
+ }
1807
+ if (iteration > cfg.maxIterations) {
1808
+ iterResult.durationMs = Date.now() - iterStart;
1809
+ history.push(iterResult);
1810
+ options.onIteration?.(iterResult);
1811
+ break;
1812
+ }
1813
+ const newFailures = failures.filter((f) => {
1814
+ const key = `${f.title}::${f.error}`;
1815
+ const count = (errorTracker.get(key) ?? 0) + 1;
1816
+ errorTracker.set(key, count);
1817
+ return count <= cfg.sameErrorThreshold;
1818
+ });
1819
+ if (newFailures.length === 0) {
1820
+ iterResult.durationMs = Date.now() - iterStart;
1821
+ history.push(iterResult);
1822
+ options.onIteration?.(iterResult);
1823
+ break;
1824
+ }
1825
+ for (const failure of newFailures) {
1826
+ const outcome = await fixer.apply(failure);
1827
+ if (outcome.success) {
1828
+ iterResult.fixesApplied.push(failure.title);
1829
+ }
1830
+ }
1831
+ iterResult.durationMs = Date.now() - iterStart;
1832
+ history.push(iterResult);
1833
+ options.onIteration?.(iterResult);
1834
+ if (iterResult.fixesApplied.length === 0 || !cfg.autoRerunOnFix) {
1835
+ break;
1836
+ }
1837
+ }
1838
+ const final = history[history.length - 1];
1839
+ const totalFixesApplied = history.reduce((sum, h) => sum + h.fixesApplied.length, 0);
1840
+ return {
1841
+ iterations: history,
1842
+ finalPassed: final?.passed ?? 0,
1843
+ finalFailed: final?.failed ?? 0,
1844
+ totalFixesApplied,
1845
+ success: (final?.failed ?? 1) === 0
1846
+ };
1847
+ }
1848
+ var DEFAULTS2;
1744
1849
  var init_dialog_loop_runner = __esm({
1745
1850
  "src/self-healing/dialog-loop-runner.ts"() {
1746
1851
  "use strict";
1747
1852
  init_esm_shims();
1853
+ DEFAULTS2 = {
1854
+ maxIterations: 3,
1855
+ pollIntervalMs: 1e4,
1856
+ sameErrorThreshold: 2,
1857
+ autoRerunOnFix: true
1858
+ };
1748
1859
  }
1749
1860
  });
1750
1861
 
1751
1862
  // src/self-healing/controlled-fixer.ts
1752
1863
  import { existsSync as existsSync8, copyFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1753
1864
  import { dirname as dirname2 } from "path";
1865
+ async function applyControlledFix(opts) {
1866
+ const fs8 = opts.fs ?? defaultFs;
1867
+ const scope = opts.options?.scope ?? "config-only";
1868
+ const dryRun = opts.options?.dryRun ?? true;
1869
+ const verify = opts.options?.verify ?? true;
1870
+ const configPath = opts.configPath;
1871
+ const backupPath = configPath + ".backup";
1872
+ if (!fs8.exists(configPath)) {
1873
+ return { success: false, scope, fixedItems: [], rolledBack: false, error: `Config file not found: ${configPath}` };
1874
+ }
1875
+ const originalContent = fs8.read(configPath);
1876
+ fs8.write(backupPath, originalContent);
1877
+ const validation = opts.validator.validate(originalContent);
1878
+ if (validation.passed) {
1879
+ cleanup(fs8, backupPath);
1880
+ return { success: true, scope, fixedItems: [], rolledBack: false };
1881
+ }
1882
+ let fixResult;
1883
+ try {
1884
+ fixResult = opts.fixer.fix(originalContent, validation.errors);
1885
+ } catch (err) {
1886
+ rollback(fs8, backupPath, configPath);
1887
+ return { success: false, scope, fixedItems: [], rolledBack: true, error: `Fix threw: ${err instanceof Error ? err.message : String(err)}` };
1888
+ }
1889
+ if (!fixResult.success) {
1890
+ rollback(fs8, backupPath, configPath);
1891
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Remaining errors: ${fixResult.remainingErrors.join("; ")}` };
1892
+ }
1893
+ if (dryRun) {
1894
+ const dryValidation = opts.validator.validate(fixResult.fixedContent);
1895
+ if (!dryValidation.passed) {
1896
+ rollback(fs8, backupPath, configPath);
1897
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Dry-run validation failed: ${dryValidation.errors.join("; ")}` };
1898
+ }
1899
+ }
1900
+ fs8.write(configPath, fixResult.fixedContent);
1901
+ if (verify) {
1902
+ const reloaded = fs8.read(configPath);
1903
+ const postValidation = opts.validator.validate(reloaded);
1904
+ if (!postValidation.passed) {
1905
+ rollback(fs8, backupPath, configPath);
1906
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Post-write verification failed: ${postValidation.errors.join("; ")}` };
1907
+ }
1908
+ }
1909
+ cleanup(fs8, backupPath);
1910
+ let prUrl;
1911
+ if (scope === "config-and-source" && opts.attribution && opts.prGenerator) {
1912
+ try {
1913
+ prUrl = await opts.prGenerator.generate(opts.attribution);
1914
+ } catch {
1915
+ }
1916
+ }
1917
+ return { success: true, scope, fixedItems: fixResult.fixedItems, rolledBack: false, prUrl };
1918
+ }
1919
+ function rollback(fs8, backupPath, configPath) {
1920
+ if (fs8.exists(backupPath)) {
1921
+ const backup = fs8.read(backupPath);
1922
+ fs8.write(configPath, backup);
1923
+ fs8.remove(backupPath);
1924
+ }
1925
+ }
1926
+ function cleanup(fs8, backupPath) {
1927
+ if (fs8.exists(backupPath)) {
1928
+ fs8.remove(backupPath);
1929
+ }
1930
+ }
1931
+ var defaultFs;
1754
1932
  var init_controlled_fixer = __esm({
1755
1933
  "src/self-healing/controlled-fixer.ts"() {
1756
1934
  "use strict";
1757
1935
  init_esm_shims();
1936
+ defaultFs = {
1937
+ exists: existsSync8,
1938
+ read: (p) => readFileSync2(p, "utf-8"),
1939
+ write: (p, c) => {
1940
+ mkdirSync3(dirname2(p), { recursive: true });
1941
+ writeFileSync3(p, c, "utf-8");
1942
+ },
1943
+ copy: copyFileSync,
1944
+ remove: unlinkSync,
1945
+ mkdirp: (d) => mkdirSync3(d, { recursive: true })
1946
+ };
1758
1947
  }
1759
1948
  });
1760
1949
 
1761
1950
  // src/self-healing/auto-fix-generator.ts
1951
+ async function generateFixPR(attribution, git, patchWriter, options) {
1952
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1953
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1954
+ const branch = `${opts.branchPrefix}${ts}`;
1955
+ const patchFile = `report/patch-${ts}.patch`;
1956
+ await git.exec("git", ["checkout", "-b", branch]);
1957
+ await patchWriter.mkdir("report");
1958
+ await patchWriter.write(patchFile, attribution.fixSuggestion.codePatch);
1959
+ try {
1960
+ await git.exec("git", ["apply", patchFile]);
1961
+ } catch {
1962
+ }
1963
+ await git.exec("git", ["add", "."]);
1964
+ await git.exec("git", ["commit", "-m", `fix: AI auto-patch for "${attribution.testName}"`]);
1965
+ await git.exec("git", ["push", "origin", branch]);
1966
+ const prArgs = [
1967
+ "pr",
1968
+ "create",
1969
+ "--draft",
1970
+ "--title",
1971
+ `[AI Fix] ${attribution.testName}`,
1972
+ "--body",
1973
+ buildPRBody(attribution)
1974
+ ];
1975
+ const { stdout: prUrl } = await git.exec("gh", prArgs);
1976
+ await git.exec("git", ["checkout", opts.baseBranch]);
1977
+ return { prUrl: prUrl.trim(), branch, patchFile };
1978
+ }
1979
+ function buildPRBody(a) {
1980
+ return [
1981
+ "## AI Auto-Fix PR",
1982
+ "",
1983
+ `**Test:** ${a.testName}`,
1984
+ `**Category:** ${a.category} | **Severity:** ${a.severity} | **Confidence:** ${(a.confidence * 100).toFixed(0)}%`,
1985
+ "",
1986
+ "### Root Cause",
1987
+ a.rootCause,
1988
+ "",
1989
+ "### Fix",
1990
+ a.fixSuggestion.description,
1991
+ "",
1992
+ "---",
1993
+ "> This PR was auto-generated by AI and **must be reviewed before merging**."
1994
+ ].join("\n");
1995
+ }
1996
+ var DEFAULT_OPTIONS;
1762
1997
  var init_auto_fix_generator = __esm({
1763
1998
  "src/self-healing/auto-fix-generator.ts"() {
1764
1999
  "use strict";
1765
2000
  init_esm_shims();
2001
+ DEFAULT_OPTIONS = {
2002
+ branchPrefix: "autofix/",
2003
+ baseBranch: "main",
2004
+ draftOnly: true
2005
+ };
1766
2006
  }
1767
2007
  });
1768
2008
 
1769
2009
  // src/self-healing/index.ts
2010
+ var self_healing_exports = {};
2011
+ __export(self_healing_exports, {
2012
+ analyzeFailureWithLLM: () => analyzeFailureWithLLM,
2013
+ applyControlledFix: () => applyControlledFix,
2014
+ categorizeFailure: () => categorizeFailure,
2015
+ createJsonResultParser: () => createJsonResultParser,
2016
+ createSelfHealingLoop: () => createSelfHealingLoop,
2017
+ generateFixPR: () => generateFixPR,
2018
+ runDialogLoop: () => runDialogLoop
2019
+ });
2020
+ function categorizeFailure(errorMessage) {
2021
+ const msg = errorMessage.toLowerCase();
2022
+ if (/5\d{2}|internal server error/.test(msg))
2023
+ return { category: "backend-5xx", confidence: 0.9 };
2024
+ if (/timeout|timed?\s*out/.test(msg))
2025
+ return { category: "timeout", confidence: 0.8 };
2026
+ if (/404|not found/.test(msg))
2027
+ return { category: "endpoint-not-found", confidence: 0.85 };
2028
+ if (/4[0-2]\d|validation|constraint/.test(msg))
2029
+ return { category: "data-constraint", confidence: 0.75 };
2030
+ if (/econnrefused|enotfound|network/.test(msg))
2031
+ return { category: "network", confidence: 0.9 };
2032
+ if (/selector|locator|element/.test(msg))
2033
+ return { category: "frontend-render", confidence: 0.7 };
2034
+ if (/storage\s*state|auth|login/.test(msg))
2035
+ return { category: "test-script", confidence: 0.8 };
2036
+ return { category: "unknown", confidence: 0.5 };
2037
+ }
2038
+ async function analyzeFailureWithLLM(errorMessage, llm) {
2039
+ const heuristic = categorizeFailure(errorMessage);
2040
+ if (!llm) {
2041
+ return {
2042
+ rootCause: errorMessage,
2043
+ category: heuristic.category,
2044
+ suggestedFix: "",
2045
+ confidence: heuristic.confidence
2046
+ };
2047
+ }
2048
+ try {
2049
+ const response = await llm.chat([
2050
+ { role: "system", content: SYSTEM_PROMPTS.failureAnalysis },
2051
+ { role: "user", content: `Analyze this test failure:
2052
+
2053
+ ${errorMessage}` }
2054
+ ]);
2055
+ const parsed = JSON.parse(response);
2056
+ return {
2057
+ rootCause: parsed.rootCause || errorMessage,
2058
+ category: parsed.category || heuristic.category,
2059
+ suggestedFix: parsed.suggestedFix || "",
2060
+ confidence: parsed.confidence || heuristic.confidence
2061
+ };
2062
+ } catch {
2063
+ return {
2064
+ rootCause: errorMessage,
2065
+ category: heuristic.category,
2066
+ suggestedFix: "",
2067
+ confidence: heuristic.confidence
2068
+ };
2069
+ }
2070
+ }
1770
2071
  async function attemptConfigFix(_testResultsDir, _mode, _llm) {
1771
2072
  return {
1772
2073
  success: false,
@@ -1868,6 +2169,13 @@ var init_heal = __esm({
1868
2169
  });
1869
2170
 
1870
2171
  // src/ci/index.ts
2172
+ var ci_exports = {};
2173
+ __export(ci_exports, {
2174
+ generateCiTemplate: () => generateCiTemplate,
2175
+ generateGitHubActionsTemplate: () => generateGitHubActionsTemplate,
2176
+ generateGitLabCITemplate: () => generateGitLabCITemplate,
2177
+ listCiPlatforms: () => listCiPlatforms
2178
+ });
1871
2179
  function generateGitHubActionsTemplate(opts = {}) {
1872
2180
  const nodeVersions = opts.nodeVersions ?? ["20.x"];
1873
2181
  const install = opts.installCommand ?? "npm ci";
@@ -1997,8 +2305,8 @@ var init_ci = __esm({
1997
2305
  });
1998
2306
 
1999
2307
  // src/cli/commands/ci.ts
2000
- var ci_exports = {};
2001
- __export(ci_exports, {
2308
+ var ci_exports2 = {};
2309
+ __export(ci_exports2, {
2002
2310
  ci: () => ci
2003
2311
  });
2004
2312
  import * as fs5 from "fs";
@@ -2044,6 +2352,143 @@ var init_ci2 = __esm({
2044
2352
  });
2045
2353
 
2046
2354
  // src/reporters/checklist-reporter.ts
2355
+ function classifyFailure(error) {
2356
+ if (!error) return "other";
2357
+ if (error.includes("[BACKEND_5XX]")) return "backend-5xx";
2358
+ if (error.includes("[MIXED_5XX]")) return "mixed-5xx";
2359
+ if (error.includes("[SLOW_API_FATAL]")) return "slow-api";
2360
+ if (error.includes("[LOG_COMPLETION_FAIL]")) return "log-fail";
2361
+ if (error.includes("[LOG_COMPLETION_TIMEOUT]")) return "log-timeout";
2362
+ if (/waitForSelector|toHaveURL|Timeout/i.test(error)) return "frontend-load";
2363
+ return "other";
2364
+ }
2365
+ function buildFailureSummary(records) {
2366
+ const failed = records.filter((r) => r.status === "failed");
2367
+ const cats = failed.map((r) => classifyFailure(r.error));
2368
+ return {
2369
+ totalFailed: failed.length,
2370
+ backend5xx: cats.filter((c) => c === "backend-5xx").length,
2371
+ mixed5xx: cats.filter((c) => c === "mixed-5xx").length,
2372
+ slowApi: cats.filter((c) => c === "slow-api").length,
2373
+ logFail: cats.filter((c) => c === "log-fail").length,
2374
+ logTimeout: cats.filter((c) => c === "log-timeout").length,
2375
+ frontendLoad: cats.filter((c) => c === "frontend-load").length,
2376
+ other: cats.filter((c) => c === "other").length
2377
+ };
2378
+ }
2379
+ function aggregateLogCompletion(records) {
2380
+ let totalCandidates = 0;
2381
+ let succeeded = 0;
2382
+ let failed = 0;
2383
+ let timedOut = 0;
2384
+ const timedOutFreq = /* @__PURE__ */ new Map();
2385
+ for (const r of records) {
2386
+ const lc = r.logCompletion;
2387
+ if (!lc || lc.candidateCount === 0) continue;
2388
+ totalCandidates += lc.candidateCount;
2389
+ succeeded += lc.succeeded.length;
2390
+ failed += lc.failed.length;
2391
+ timedOut += lc.timedOut.length;
2392
+ for (const item of lc.timedOut) {
2393
+ const key = `${item.method}:${item.path}`;
2394
+ const existing = timedOutFreq.get(key);
2395
+ if (existing) existing.count += 1;
2396
+ else timedOutFreq.set(key, { method: item.method, path: item.path, count: 1 });
2397
+ }
2398
+ }
2399
+ const timedOutTop5 = Array.from(timedOutFreq.values()).sort((a, b) => b.count - a.count).slice(0, 5).map((t) => ({ method: t.method, path: t.path, occurrences: t.count }));
2400
+ const matchRate = totalCandidates > 0 ? (succeeded + failed) / totalCandidates * 100 : 0;
2401
+ const effectiveRate = totalCandidates > 0 ? succeeded / totalCandidates * 100 : 0;
2402
+ return { totalCandidates, succeeded, failed, timedOut, matchRate, effectiveRate, timedOutTop5 };
2403
+ }
2404
+ function extractUrls(error) {
2405
+ if (!error) return [];
2406
+ const matched = error.match(/https?:\/\/[^\s\n]+/g);
2407
+ return matched ? Array.from(new Set(matched)) : [];
2408
+ }
2409
+ function parseApiDomain(urlStr) {
2410
+ try {
2411
+ const u = new URL(urlStr);
2412
+ const segments = u.pathname.split("/").filter(Boolean);
2413
+ const v1Index = segments.findIndex((s) => s === "v1");
2414
+ if (v1Index === -1 || v1Index + 1 >= segments.length) return segments[0] || null;
2415
+ const afterV1 = segments.slice(v1Index + 1);
2416
+ if (afterV1.length === 0) return null;
2417
+ const first = afterV1[0];
2418
+ if (/^\d+$/.test(first) && afterV1.length > 1) return afterV1[1];
2419
+ return first;
2420
+ } catch {
2421
+ return null;
2422
+ }
2423
+ }
2424
+ function buildBackendChecklist(records) {
2425
+ const domainMap = /* @__PURE__ */ new Map();
2426
+ const failed = records.filter((r) => r.status === "failed");
2427
+ for (const item of failed) {
2428
+ const cat = classifyFailure(item.error);
2429
+ if (cat !== "backend-5xx" && cat !== "mixed-5xx") continue;
2430
+ const urls = extractUrls(item.error);
2431
+ for (const url of urls) {
2432
+ const domain = parseApiDomain(url);
2433
+ if (!domain) continue;
2434
+ const current = domainMap.get(domain) ?? { tests: /* @__PURE__ */ new Set(), endpoints: /* @__PURE__ */ new Set() };
2435
+ current.tests.add(item.title);
2436
+ current.endpoints.add(url);
2437
+ domainMap.set(domain, current);
2438
+ }
2439
+ }
2440
+ return Array.from(domainMap.entries()).map(([domain, v]) => ({
2441
+ domain,
2442
+ tests: Array.from(v.tests).sort(),
2443
+ endpoints: Array.from(v.endpoints).sort()
2444
+ })).sort((a, b) => b.tests.length - a.tests.length || a.domain.localeCompare(b.domain));
2445
+ }
2446
+ function renderChecklistMarkdown(items, summary, logSummary) {
2447
+ const lines = [
2448
+ "# Backend Fix Checklist",
2449
+ ""
2450
+ ];
2451
+ if (logSummary) {
2452
+ lines.push(
2453
+ `- Log match rate: ${logSummary.matchRate.toFixed(2)}% (candidates=${logSummary.totalCandidates}, succeeded=${logSummary.succeeded}, failed=${logSummary.failed}, timedOut=${logSummary.timedOut})`,
2454
+ `- Effective success rate: ${logSummary.effectiveRate.toFixed(2)}%`
2455
+ );
2456
+ }
2457
+ lines.push(
2458
+ `- Backend 5xx: ${summary.backend5xx}`,
2459
+ `- Mixed 5xx: ${summary.mixed5xx}`,
2460
+ `- Slow API: ${summary.slowApi}`,
2461
+ `- Log fail: ${summary.logFail}`,
2462
+ `- Log timeout: ${summary.logTimeout}`,
2463
+ `- Frontend load: ${summary.frontendLoad}`,
2464
+ `- Other: ${summary.other}`,
2465
+ ""
2466
+ );
2467
+ if (logSummary && logSummary.timedOutTop5.length > 0) {
2468
+ lines.push("### Timed-out APIs Top 5", "", "| # | Method | Path | Occurrences |", "|---|--------|------|-------------|");
2469
+ logSummary.timedOutTop5.forEach((t, i) => {
2470
+ lines.push(`| ${i + 1} | ${t.method} | ${t.path} | ${t.occurrences} |`);
2471
+ });
2472
+ lines.push("");
2473
+ }
2474
+ if (items.length === 0) {
2475
+ lines.push("No backend 5xx failures this run.");
2476
+ return lines.join("\n") + "\n";
2477
+ }
2478
+ for (const item of items) {
2479
+ lines.push(
2480
+ `## ${item.domain}`,
2481
+ "",
2482
+ `- Failed tests: ${item.tests.length}`,
2483
+ "- Affected tests:"
2484
+ );
2485
+ for (const t of item.tests) lines.push(` - ${t}`);
2486
+ lines.push("- Failed endpoints:");
2487
+ for (const e of item.endpoints) lines.push(` - ${e}`);
2488
+ lines.push("");
2489
+ }
2490
+ return lines.join("\n") + "\n";
2491
+ }
2047
2492
  var init_checklist_reporter = __esm({
2048
2493
  "src/reporters/checklist-reporter.ts"() {
2049
2494
  "use strict";
@@ -2052,6 +2497,99 @@ var init_checklist_reporter = __esm({
2052
2497
  });
2053
2498
 
2054
2499
  // src/reporters/workorder-reporter.ts
2500
+ function assignPriority(item, isLogRate) {
2501
+ if (isLogRate) return "P0";
2502
+ if (item.tests.length >= 3) return "P0";
2503
+ if (item.tests.length === 2) return "P1";
2504
+ return "P2";
2505
+ }
2506
+ function buildWorkorders(opts) {
2507
+ const { checklist, logSummary, logRateThreshold = 90 } = opts;
2508
+ const result = [];
2509
+ let idx = 1;
2510
+ if (logSummary && logSummary.totalCandidates > 0 && logSummary.matchRate < logRateThreshold) {
2511
+ const logItem = {
2512
+ domain: "Log Completion Standards",
2513
+ tests: logSummary.timedOutTop5.map((t) => `${t.method} ${t.path} (\xD7${t.occurrences})`),
2514
+ endpoints: logSummary.timedOutTop5.map((t) => t.path)
2515
+ };
2516
+ result.push({
2517
+ index: idx++,
2518
+ domain: logItem.domain,
2519
+ priority: "P0",
2520
+ tests: logItem.tests,
2521
+ endpoints: logItem.endpoints,
2522
+ objective: "Add missing end-phase structured logs for timed-out APIs",
2523
+ acceptanceCriteria: [
2524
+ `Log completion match rate \u2265 ${logRateThreshold}%`,
2525
+ "TimedOut API count drops to 0 or only SSE/long-polling endpoints remain"
2526
+ ]
2527
+ });
2528
+ }
2529
+ for (const item of checklist) {
2530
+ const priority = assignPriority(item, false);
2531
+ result.push({
2532
+ index: idx++,
2533
+ domain: item.domain,
2534
+ priority,
2535
+ tests: item.tests,
2536
+ endpoints: item.endpoints,
2537
+ objective: "Fix 500 errors and return a valid business response",
2538
+ acceptanceCriteria: [
2539
+ "HTTP status returns 2xx for affected endpoints",
2540
+ "No [BACKEND_5XX] errors in corresponding page traversal",
2541
+ "All covered tests pass"
2542
+ ]
2543
+ });
2544
+ }
2545
+ return result;
2546
+ }
2547
+ function renderWorkordersMarkdown(workorders, summary, logSummary) {
2548
+ const lines = [
2549
+ "# Backend Work Orders",
2550
+ ""
2551
+ ];
2552
+ if (logSummary) {
2553
+ lines.push(
2554
+ `- Log match rate: ${logSummary.matchRate.toFixed(2)}%`,
2555
+ `- Effective success rate: ${logSummary.effectiveRate.toFixed(2)}%`
2556
+ );
2557
+ }
2558
+ lines.push(
2559
+ `- Total failed: ${summary.totalFailed}`,
2560
+ `- Backend 5xx: ${summary.backend5xx}`,
2561
+ `- Mixed 5xx: ${summary.mixed5xx}`,
2562
+ `- Slow API: ${summary.slowApi}`,
2563
+ `- Log fail: ${summary.logFail}`,
2564
+ `- Log timeout: ${summary.logTimeout}`,
2565
+ `- Frontend load: ${summary.frontendLoad}`,
2566
+ `- Other: ${summary.other}`,
2567
+ ""
2568
+ );
2569
+ if (workorders.length === 0) {
2570
+ lines.push("No backend work orders this run.");
2571
+ return lines.join("\n") + "\n";
2572
+ }
2573
+ for (const wo of workorders) {
2574
+ lines.push(
2575
+ `## Workorder ${wo.index} - ${wo.domain}`,
2576
+ "",
2577
+ `- Priority: ${wo.priority}`,
2578
+ `- Affected tests: ${wo.tests.length}`,
2579
+ "- Scope:"
2580
+ );
2581
+ for (const t of wo.tests) lines.push(` - ${t}`);
2582
+ if (wo.endpoints.length > 0) {
2583
+ lines.push("- Endpoints:");
2584
+ for (const e of wo.endpoints) lines.push(` - ${e}`);
2585
+ }
2586
+ lines.push(`- Objective: ${wo.objective}`);
2587
+ lines.push("- Acceptance criteria:");
2588
+ for (const c of wo.acceptanceCriteria) lines.push(` - ${c}`);
2589
+ lines.push("");
2590
+ }
2591
+ return lines.join("\n") + "\n";
2592
+ }
2055
2593
  var init_workorder_reporter = __esm({
2056
2594
  "src/reporters/workorder-reporter.ts"() {
2057
2595
  "use strict";
@@ -2060,14 +2598,133 @@ var init_workorder_reporter = __esm({
2060
2598
  });
2061
2599
 
2062
2600
  // src/reporters/token-reporter.ts
2601
+ function renderTokenReportMarkdown(summary) {
2602
+ const lines = [
2603
+ "# AI Token Usage Report",
2604
+ "",
2605
+ `- Total requests: ${summary.totalRequests}`,
2606
+ `- Total tokens: ${summary.totalTokens.toLocaleString()}`,
2607
+ `- Prompt tokens: ${summary.totalPromptTokens.toLocaleString()}`,
2608
+ `- Completion tokens: ${summary.totalCompletionTokens.toLocaleString()}`,
2609
+ `- Estimated cost: \xA5${summary.totalEstimatedCost.toFixed(4)}`,
2610
+ `- Average latency: ${summary.avgLatencyMs}ms`
2611
+ ];
2612
+ if (summary.budgetUsedPercent !== null) {
2613
+ lines.push(`- Budget used: ${summary.budgetUsedPercent}%${summary.budgetExceeded ? " **EXCEEDED**" : ""}`);
2614
+ }
2615
+ lines.push("");
2616
+ const cats = Object.entries(summary.byCategory).sort((a, b) => b[1].totalTokens - a[1].totalTokens);
2617
+ if (cats.length > 0) {
2618
+ lines.push(
2619
+ "## By Category",
2620
+ "",
2621
+ "| Category | Requests | Prompt | Completion | Total | Cost |",
2622
+ "|----------|----------|--------|------------|-------|------|"
2623
+ );
2624
+ for (const [cat, d] of cats) {
2625
+ lines.push(`| ${cat} | ${d.requests} | ${d.promptTokens.toLocaleString()} | ${d.completionTokens.toLocaleString()} | ${d.totalTokens.toLocaleString()} | \xA5${d.estimatedCost.toFixed(4)} |`);
2626
+ }
2627
+ lines.push("");
2628
+ }
2629
+ const models = Object.entries(summary.byModel);
2630
+ if (models.length > 0) {
2631
+ lines.push(
2632
+ "## By Model",
2633
+ "",
2634
+ "| Model | Requests | Total Tokens | Cost |",
2635
+ "|-------|----------|-------------|------|"
2636
+ );
2637
+ for (const [model, d] of models) {
2638
+ lines.push(`| ${model} | ${d.requests} | ${d.totalTokens.toLocaleString()} | \xA5${d.estimatedCost.toFixed(4)} |`);
2639
+ }
2640
+ lines.push("");
2641
+ }
2642
+ return lines.join("\n") + "\n";
2643
+ }
2644
+ var TokenTracker;
2063
2645
  var init_token_reporter = __esm({
2064
2646
  "src/reporters/token-reporter.ts"() {
2065
2647
  "use strict";
2066
2648
  init_esm_shims();
2649
+ TokenTracker = class {
2650
+ entries = [];
2651
+ budget = null;
2652
+ setBudget(maxTokens) {
2653
+ this.budget = maxTokens;
2654
+ }
2655
+ record(entry) {
2656
+ this.entries.push(entry);
2657
+ }
2658
+ reset() {
2659
+ this.entries = [];
2660
+ }
2661
+ getSummary() {
2662
+ const totalRequests = this.entries.length;
2663
+ let totalPromptTokens = 0;
2664
+ let totalCompletionTokens = 0;
2665
+ let totalEstimatedCost = 0;
2666
+ let totalLatency = 0;
2667
+ const byCategory = {};
2668
+ const byModel = {};
2669
+ for (const e of this.entries) {
2670
+ totalPromptTokens += e.promptTokens;
2671
+ totalCompletionTokens += e.completionTokens;
2672
+ totalEstimatedCost += e.estimatedCost;
2673
+ totalLatency += e.latencyMs;
2674
+ if (!byCategory[e.category]) {
2675
+ byCategory[e.category] = { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, estimatedCost: 0 };
2676
+ }
2677
+ const cat = byCategory[e.category];
2678
+ cat.requests++;
2679
+ cat.promptTokens += e.promptTokens;
2680
+ cat.completionTokens += e.completionTokens;
2681
+ cat.totalTokens += e.promptTokens + e.completionTokens;
2682
+ cat.estimatedCost += e.estimatedCost;
2683
+ if (!byModel[e.model]) {
2684
+ byModel[e.model] = { requests: 0, totalTokens: 0, estimatedCost: 0 };
2685
+ }
2686
+ const mod = byModel[e.model];
2687
+ mod.requests++;
2688
+ mod.totalTokens += e.promptTokens + e.completionTokens;
2689
+ mod.estimatedCost += e.estimatedCost;
2690
+ }
2691
+ const totalTokens = totalPromptTokens + totalCompletionTokens;
2692
+ const budgetUsedPercent = this.budget && this.budget > 0 ? Math.round(totalTokens / this.budget * 1e4) / 100 : null;
2693
+ return {
2694
+ totalRequests,
2695
+ totalTokens,
2696
+ totalPromptTokens,
2697
+ totalCompletionTokens,
2698
+ totalEstimatedCost,
2699
+ avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
2700
+ byCategory,
2701
+ byModel,
2702
+ budgetUsedPercent,
2703
+ budgetExceeded: budgetUsedPercent !== null && budgetUsedPercent > 100
2704
+ };
2705
+ }
2706
+ };
2067
2707
  }
2068
2708
  });
2069
2709
 
2070
2710
  // src/reporters/index.ts
2711
+ var reporters_exports = {};
2712
+ __export(reporters_exports, {
2713
+ TokenTracker: () => TokenTracker,
2714
+ aggregateLogCompletion: () => aggregateLogCompletion,
2715
+ buildBackendChecklist: () => buildBackendChecklist,
2716
+ buildFailureSummary: () => buildFailureSummary,
2717
+ buildWorkorders: () => buildWorkorders,
2718
+ classifyFailure: () => classifyFailure,
2719
+ generateHtmlReport: () => generateHtmlReport,
2720
+ generateJsonReport: () => generateJsonReport,
2721
+ generateMarkdownReport: () => generateMarkdownReport,
2722
+ generateReports: () => generateReports,
2723
+ parseApiDomain: () => parseApiDomain,
2724
+ renderChecklistMarkdown: () => renderChecklistMarkdown,
2725
+ renderTokenReportMarkdown: () => renderTokenReportMarkdown,
2726
+ renderWorkordersMarkdown: () => renderWorkordersMarkdown
2727
+ });
2071
2728
  function generateJsonReport(result) {
2072
2729
  const serializable = {
2073
2730
  modules: result.modules,
@@ -3549,6 +4206,60 @@ function registerAgentRoutes(app, office) {
3549
4206
  duration: result.duration
3550
4207
  };
3551
4208
  });
4209
+ app.post("/api/run-tests", async (_req, reply) => {
4210
+ if (office.isRunning()) {
4211
+ reply.code(409).send({ error: "A task is already running" });
4212
+ return;
4213
+ }
4214
+ office.runTests().catch(() => {
4215
+ });
4216
+ return { ok: true, message: "Test execution started" };
4217
+ });
4218
+ app.get("/api/test-results", async () => {
4219
+ const metrics = office.getLastExecutionMetrics();
4220
+ if (!metrics) return { ok: false, message: "No tests have been run yet" };
4221
+ const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4222
+ return { ok: true, metrics, total };
4223
+ });
4224
+ app.post("/api/reports/generate", async (_req, reply) => {
4225
+ if (office.isRunning()) {
4226
+ reply.code(409).send({ error: "A task is already running" });
4227
+ return;
4228
+ }
4229
+ office.generateReport().catch(() => {
4230
+ });
4231
+ return { ok: true, message: "Report generation started" };
4232
+ });
4233
+ app.get("/api/reports", async () => {
4234
+ const reports = office.getLastReports();
4235
+ if (reports.length === 0) return { ok: false, message: "No reports generated yet" };
4236
+ return {
4237
+ ok: true,
4238
+ reports: reports.map((r) => ({
4239
+ format: r.format,
4240
+ filename: r.filename,
4241
+ size: r.content.length
4242
+ }))
4243
+ };
4244
+ });
4245
+ app.get("/api/reports/:format", async (req, reply) => {
4246
+ const reports = office.getLastReports();
4247
+ const report2 = reports.find((r) => r.format === req.params.format);
4248
+ if (!report2) {
4249
+ reply.code(404).send({ error: `No ${req.params.format} report found` });
4250
+ return;
4251
+ }
4252
+ const contentType = req.params.format === "html" ? "text/html" : req.params.format === "json" ? "application/json" : "text/markdown";
4253
+ reply.type(contentType).send(report2.content);
4254
+ });
4255
+ app.get("/api/ci/template", async (req) => {
4256
+ const provider = req.query.provider || "github";
4257
+ const { generateGitHubActionsTemplate: generateGitHubActionsTemplate2, generateGitLabCITemplate: generateGitLabCITemplate2 } = await Promise.resolve().then(() => (init_ci(), ci_exports));
4258
+ if (provider === "gitlab") {
4259
+ return { ok: true, provider: "gitlab", template: generateGitLabCITemplate2() };
4260
+ }
4261
+ return { ok: true, provider: "github", template: generateGitHubActionsTemplate2() };
4262
+ });
3552
4263
  }
3553
4264
  var init_agents = __esm({
3554
4265
  "src/server/routes/agents.ts"() {
@@ -3580,6 +4291,8 @@ var init_croc_office = __esm({
3580
4291
  running = false;
3581
4292
  lastPipelineResult = null;
3582
4293
  lastGeneratedFiles = [];
4294
+ lastExecutionMetrics = null;
4295
+ lastReports = [];
3583
4296
  constructor(config, cwd) {
3584
4297
  this.config = config;
3585
4298
  this.cwd = cwd;
@@ -3761,6 +4474,128 @@ var init_croc_office = __esm({
3761
4474
  getGeneratedFiles() {
3762
4475
  return this.lastGeneratedFiles;
3763
4476
  }
4477
+ /** Get last execution metrics */
4478
+ getLastExecutionMetrics() {
4479
+ return this.lastExecutionMetrics;
4480
+ }
4481
+ /** Get last generated reports */
4482
+ getLastReports() {
4483
+ return this.lastReports;
4484
+ }
4485
+ /** Run generated tests with Playwright */
4486
+ async runTests() {
4487
+ if (this.running) return { ok: false, task: "execute", duration: 0, error: "Another task is running" };
4488
+ if (this.lastGeneratedFiles.length === 0) {
4489
+ return { ok: false, task: "execute", duration: 0, error: "No test files \u2014 run Pipeline first" };
4490
+ }
4491
+ this.running = true;
4492
+ const start = Date.now();
4493
+ try {
4494
+ const { resolve: resolvePath } = await import("path");
4495
+ const { execSync } = await import("child_process");
4496
+ const { existsSync: existsSync16 } = await import("fs");
4497
+ const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync16(f));
4498
+ if (testFiles.length === 0) {
4499
+ this.log("\u26A0\uFE0F No test files found on disk", "warn");
4500
+ return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
4501
+ }
4502
+ this.updateAgent("tester-croc", { status: "working", currentTask: `Running ${testFiles.length} test files...`, progress: 0 });
4503
+ this.log(`\u{1F9EA} \u6D4B\u8BD5\u9CC4 is running ${testFiles.length} Playwright tests...`);
4504
+ this.updateAgent("healer-croc", { status: "thinking", currentTask: "Monitoring test run...", progress: 0 });
4505
+ let stdout2 = "", stderr = "";
4506
+ try {
4507
+ const result = execSync(
4508
+ `npx playwright test ${testFiles.map((f) => `"${f}"`).join(" ")} --reporter=line 2>&1`,
4509
+ { cwd: this.cwd, encoding: "utf-8", timeout: 3e5, stdio: "pipe" }
4510
+ );
4511
+ stdout2 = result;
4512
+ } catch (err) {
4513
+ const execErr = err;
4514
+ stdout2 = execErr.stdout || "";
4515
+ stderr = execErr.stderr || "";
4516
+ }
4517
+ const output = stdout2 + "\n" + stderr;
4518
+ const metrics = { passed: 0, failed: 0, skipped: 0, timedOut: 0 };
4519
+ const passedMatch = output.match(/(\d+)\s+passed/);
4520
+ const failedMatch = output.match(/(\d+)\s+failed/);
4521
+ const skippedMatch = output.match(/(\d+)\s+skipped/);
4522
+ const timedOutMatch = output.match(/(\d+)\s+timed?\s*out/i);
4523
+ if (passedMatch) metrics.passed = parseInt(passedMatch[1], 10);
4524
+ if (failedMatch) metrics.failed = parseInt(failedMatch[1], 10);
4525
+ if (skippedMatch) metrics.skipped = parseInt(skippedMatch[1], 10);
4526
+ if (timedOutMatch) metrics.timedOut = parseInt(timedOutMatch[1], 10);
4527
+ this.lastExecutionMetrics = metrics;
4528
+ const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4529
+ if (metrics.failed > 0) {
4530
+ this.updateAgent("tester-croc", { status: "error", currentTask: `${metrics.failed} tests failed`, progress: 100 });
4531
+ this.updateAgent("healer-croc", { status: "working", currentTask: `Analyzing ${metrics.failed} failures...`, progress: 50 });
4532
+ this.log(`\u274C Tests: ${metrics.passed} passed, ${metrics.failed} failed, ${metrics.skipped} skipped`, "warn");
4533
+ const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
4534
+ const failLines = output.split("\n").filter((l) => /fail|error|timeout/i.test(l)).slice(0, 5);
4535
+ for (const line of failLines) {
4536
+ const cat = categorizeFailure2(line);
4537
+ this.log(` \u{1F50D} ${cat.category} (${Math.round(cat.confidence * 100)}%): ${line.substring(0, 100)}`, "warn");
4538
+ }
4539
+ this.updateAgent("healer-croc", { status: "done", currentTask: "Failure analysis done", progress: 100 });
4540
+ } else {
4541
+ this.updateAgent("tester-croc", { status: "done", currentTask: `All ${metrics.passed} tests passed!`, progress: 100 });
4542
+ this.updateAgent("healer-croc", { status: "done", currentTask: "No failures", progress: 100 });
4543
+ this.log(`\u2705 All ${metrics.passed} tests passed!`);
4544
+ }
4545
+ this.updateNodeStatus("controller", metrics.failed > 0 ? "failed" : "passed");
4546
+ this.broadcast("test:complete", { metrics, total });
4547
+ const duration = Date.now() - start;
4548
+ this.log(`\u{1F9EA} Test execution complete in ${duration}ms`);
4549
+ return { ok: metrics.failed === 0, task: "execute", duration, details: metrics };
4550
+ } catch (err) {
4551
+ this.updateAgent("tester-croc", { status: "error", currentTask: String(err) });
4552
+ this.log(`\u274C Test execution failed: ${err}`, "error");
4553
+ return { ok: false, task: "execute", duration: Date.now() - start, error: String(err) };
4554
+ } finally {
4555
+ this.running = false;
4556
+ }
4557
+ }
4558
+ /** Generate reports (HTML/JSON/Markdown) */
4559
+ async generateReport() {
4560
+ if (this.running) return { ok: false, task: "report", duration: 0, error: "Another task is running" };
4561
+ if (!this.lastPipelineResult) {
4562
+ return { ok: false, task: "report", duration: 0, error: "No pipeline result \u2014 run Pipeline first" };
4563
+ }
4564
+ this.running = true;
4565
+ const start = Date.now();
4566
+ try {
4567
+ this.updateAgent("reporter-croc", { status: "working", currentTask: "Generating reports...", progress: 0 });
4568
+ this.log("\u{1F4CA} \u6C47\u62A5\u9CC4 is generating reports...");
4569
+ const { generateReports: generateReports2 } = await Promise.resolve().then(() => (init_reporters(), reporters_exports));
4570
+ const formats = ["html", "json", "markdown"];
4571
+ const reports = generateReports2(this.lastPipelineResult, formats);
4572
+ this.lastReports = reports;
4573
+ const { resolve: resolvePath } = await import("path");
4574
+ const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync10 } = await import("fs");
4575
+ const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
4576
+ mkdirSync10(outDir, { recursive: true });
4577
+ for (const report2 of reports) {
4578
+ const fullPath = resolvePath(outDir, report2.filename);
4579
+ writeFileSync10(fullPath, report2.content, "utf-8");
4580
+ this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
4581
+ }
4582
+ this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
4583
+ this.broadcast("reports:generated", reports.map((r) => ({
4584
+ format: r.format,
4585
+ filename: r.filename,
4586
+ size: r.content.length
4587
+ })));
4588
+ const duration = Date.now() - start;
4589
+ this.log(`\u2705 Reports generated in ${duration}ms`);
4590
+ return { ok: true, task: "report", duration, details: { count: reports.length } };
4591
+ } catch (err) {
4592
+ this.updateAgent("reporter-croc", { status: "error", currentTask: String(err) });
4593
+ this.log(`\u274C Report generation failed: ${err}`, "error");
4594
+ return { ok: false, task: "report", duration: Date.now() - start, error: String(err) };
4595
+ } finally {
4596
+ this.running = false;
4597
+ }
4598
+ }
3764
4599
  // ============ Graph Helpers ============
3765
4600
  updateNodeStatus(type, status) {
3766
4601
  if (!this.cachedGraph) return;
@@ -4236,7 +5071,7 @@ program.command("heal").description("Run self-healing loop on failed tests").opt
4236
5071
  await heal2(opts);
4237
5072
  });
4238
5073
  program.command("ci").description("Generate CI/CD pipeline template").option("-p, --platform <name>", "CI platform (github, gitlab)", "github").option("--self-heal", "Include self-healing step").option("--node <versions>", "Node.js versions (comma-separated)", "20.x").action(async (opts) => {
4239
- const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports));
5074
+ const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports2));
4240
5075
  await ci2(opts);
4241
5076
  });
4242
5077
  program.command("report").description("Generate pipeline report (HTML/JSON/Markdown)").option("-f, --format <formats>", "Report formats (comma-separated)", "html").option("-o, --output <dir>", "Output directory").action(async (opts) => {