sela-core 0.1.0-alpha.35 → 0.1.0-alpha.37
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/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +21 -1
- package/dist/cli/commands/shadowRegression.d.ts +21 -0
- package/dist/cli/commands/shadowRegression.d.ts.map +1 -0
- package/dist/cli/commands/shadowRegression.js +193 -0
- package/dist/cli/conventionsInit.d.ts +51 -0
- package/dist/cli/conventionsInit.d.ts.map +1 -0
- package/dist/cli/conventionsInit.js +326 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/ui/ConventionsTable.d.ts +7 -0
- package/dist/cli/ui/ConventionsTable.d.ts.map +1 -0
- package/dist/cli/ui/ConventionsTable.js +32 -0
- package/dist/config/ConfigLoader.d.ts +8 -1
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +28 -0
- package/dist/config/Conventions.d.ts +131 -0
- package/dist/config/Conventions.d.ts.map +1 -0
- package/dist/config/Conventions.js +268 -0
- package/dist/config/DryRunGuard.d.ts +15 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -1
- package/dist/config/DryRunGuard.js +30 -0
- package/dist/config/SelaConfig.d.ts +127 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +68 -17
- package/dist/engine/SelaEngine.d.ts +3 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +333 -55
- package/dist/errors/SelaError.d.ts +3 -0
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +3 -0
- package/dist/errors/SelaHealingRefusalError.d.ts +17 -0
- package/dist/errors/SelaHealingRefusalError.d.ts.map +1 -0
- package/dist/errors/SelaHealingRefusalError.js +42 -0
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +101 -14
- package/dist/reporter/SelaReporter.d.ts +16 -1
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +40 -4
- package/dist/services/ASTSourceUpdater.d.ts +63 -1
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +666 -65
- package/dist/services/ArgumentTypeAnalyzer.d.ts +1 -1
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -1
- package/dist/services/ArgumentTypeAnalyzer.js +1 -1
- package/dist/services/ConventionsScanner.d.ts +28 -0
- package/dist/services/ConventionsScanner.d.ts.map +1 -0
- package/dist/services/ConventionsScanner.js +441 -0
- package/dist/services/CrossFileHealer.d.ts +14 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -1
- package/dist/services/CrossFileHealer.js +152 -2
- package/dist/services/DecisionEngine.d.ts +17 -1
- package/dist/services/DecisionEngine.d.ts.map +1 -1
- package/dist/services/DecisionEngine.js +152 -1
- package/dist/services/HealFieldParser.d.ts +31 -0
- package/dist/services/HealFieldParser.d.ts.map +1 -0
- package/dist/services/HealFieldParser.js +153 -0
- package/dist/services/HealReportService.d.ts +71 -3
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +307 -17
- package/dist/services/HealingCacheService.d.ts +7 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -1
- package/dist/services/HealingCacheService.js +1 -0
- package/dist/services/InboundArgumentGate.d.ts +18 -0
- package/dist/services/InboundArgumentGate.d.ts.map +1 -0
- package/dist/services/InboundArgumentGate.js +67 -0
- package/dist/services/InitializerUpdater.d.ts +12 -1
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +45 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +41 -82
- package/dist/services/InteractiveReview.d.ts +20 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +22 -1
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +145 -13
- package/dist/services/LocatorArgModel.d.ts +23 -0
- package/dist/services/LocatorArgModel.d.ts.map +1 -0
- package/dist/services/LocatorArgModel.js +137 -0
- package/dist/services/MutationApplier.d.ts.map +1 -1
- package/dist/services/MutationApplier.js +7 -0
- package/dist/services/OrdinalTracker.d.ts +24 -0
- package/dist/services/OrdinalTracker.d.ts.map +1 -0
- package/dist/services/OrdinalTracker.js +42 -0
- package/dist/services/RegressionManifestService.d.ts +67 -0
- package/dist/services/RegressionManifestService.d.ts.map +1 -0
- package/dist/services/RegressionManifestService.js +189 -0
- package/dist/services/ReportGenerator.d.ts +20 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +20 -3
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +12 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +22 -1
- package/dist/services/SemanticAnchor.d.ts +67 -0
- package/dist/services/SemanticAnchor.d.ts.map +1 -0
- package/dist/services/SemanticAnchor.js +160 -0
- package/dist/services/SharedLocatorGuard.d.ts +36 -0
- package/dist/services/SharedLocatorGuard.d.ts.map +1 -0
- package/dist/services/SharedLocatorGuard.js +360 -0
- package/dist/services/SnapshotService.d.ts +1 -0
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +15 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +10 -5
- package/dist/services/TraceBackEngine.d.ts +47 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -1
- package/dist/services/TraceBackEngine.js +187 -4
- package/dist/services/WorkspaceSnapshotService.d.ts +27 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -1
- package/dist/services/WorkspaceSnapshotService.js +23 -0
- package/dist/services/confidence/BusinessContextParser.d.ts +31 -0
- package/dist/services/confidence/BusinessContextParser.d.ts.map +1 -0
- package/dist/services/confidence/BusinessContextParser.js +124 -0
- package/dist/services/confidence/ConfidenceVectorEngine.d.ts +5 -0
- package/dist/services/confidence/ConfidenceVectorEngine.d.ts.map +1 -0
- package/dist/services/confidence/ConfidenceVectorEngine.js +187 -0
- package/dist/services/confidence/StructuralScoringUtils.d.ts +27 -0
- package/dist/services/confidence/StructuralScoringUtils.d.ts.map +1 -0
- package/dist/services/confidence/StructuralScoringUtils.js +127 -0
- package/dist/services/confidence/index.d.ts +6 -0
- package/dist/services/confidence/index.d.ts.map +1 -0
- package/dist/services/confidence/index.js +12 -0
- package/dist/services/confidence/types.d.ts +64 -0
- package/dist/services/confidence/types.d.ts.map +1 -0
- package/dist/services/confidence/types.js +5 -0
- package/dist/services/interceptors/FileWriteInterceptor.d.ts +29 -0
- package/dist/services/interceptors/FileWriteInterceptor.d.ts.map +1 -0
- package/dist/services/interceptors/FileWriteInterceptor.js +83 -0
- package/dist/services/safety/PipelineContext.d.ts +16 -0
- package/dist/services/safety/PipelineContext.d.ts.map +1 -1
- package/dist/services/safety/PipelineContext.js +6 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -1
- package/dist/services/safety/rules/ConfidenceTierRule.js +12 -1
- package/dist/services/safety/rules/ConfidenceVectorRule.d.ts +15 -0
- package/dist/services/safety/rules/ConfidenceVectorRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ConfidenceVectorRule.js +109 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -1
- package/dist/services/safety/rules/IntentAuditorRule.js +5 -1
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -1
- package/dist/services/safety/rules/StructuralOnlyRule.js +9 -4
- package/dist/services/safety/rules/index.d.ts.map +1 -1
- package/dist/services/safety/rules/index.js +9 -7
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +62 -2
- package/dist/utils/IsolatedDiff.d.ts +8 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -1
- package/dist/utils/IsolatedDiff.js +8 -0
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.js +4 -4
- package/package.json +79 -72
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
export interface InitOptions {
|
|
3
|
+
updateImports?: boolean;
|
|
4
|
+
/** Skip the conventions auto-discovery wizard. */
|
|
5
|
+
conventions?: boolean;
|
|
6
|
+
/** Enable the optional LLM polish step (Commander: --no-llm sets false). */
|
|
7
|
+
llm?: boolean;
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
glob?: string;
|
|
11
|
+
maxFiles?: string;
|
|
12
|
+
fileTimeout?: string;
|
|
13
|
+
}
|
|
2
14
|
export declare function registerInit(program: Command): void;
|
|
3
15
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoBnD"}
|
|
@@ -37,6 +37,7 @@ exports.registerInit = registerInit;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const logger_js_1 = require("../../utils/logger.js");
|
|
40
|
+
const conventionsInit_js_1 = require("../conventionsInit.js");
|
|
40
41
|
const PACKAGE_NAME = "sela-core";
|
|
41
42
|
const CONFIG_TEMPLATE_TS = `import { defineConfig } from '${PACKAGE_NAME}';
|
|
42
43
|
|
|
@@ -60,8 +61,15 @@ const REQUIRED_ENV_VARS = ["ANTHROPIC_API_KEY"];
|
|
|
60
61
|
function registerInit(program) {
|
|
61
62
|
program
|
|
62
63
|
.command("init")
|
|
63
|
-
.description("Initialize Sela
|
|
64
|
+
.description("Initialize Sela and auto-discover your testing conventions")
|
|
64
65
|
.option("--no-update-imports", "Skip swapping @playwright/test imports to sela-core")
|
|
66
|
+
.option("--no-conventions", "Skip the conventions auto-discovery wizard")
|
|
67
|
+
.option("--no-llm", "Skip the optional Claude refinement step")
|
|
68
|
+
.option("-y, --yes", "Accept defaults (smart discovery + write)")
|
|
69
|
+
.option("--dry-run", "Print conventions to stdout without writing")
|
|
70
|
+
.option("--glob <pattern>", "Scan files matching this glob (non-interactive)")
|
|
71
|
+
.option("--max-files <n>", "Hard cap on files scanned")
|
|
72
|
+
.option("--file-timeout <ms>", "Per-file analysis budget (ms)")
|
|
65
73
|
.action(async (opts) => {
|
|
66
74
|
await runInit(opts);
|
|
67
75
|
});
|
|
@@ -106,6 +114,18 @@ async function runInit(opts) {
|
|
|
106
114
|
}
|
|
107
115
|
logger_js_1.logger.info(`\nSpec files should import from:`);
|
|
108
116
|
logger_js_1.logger.info(` import { test, expect } from '${PACKAGE_NAME}'\n`);
|
|
117
|
+
// ── Conventions auto-discovery (default ON; opt out --no-conventions) ──
|
|
118
|
+
if (opts.conventions !== false) {
|
|
119
|
+
logger_js_1.logger.info(`Discovering testing conventions…`);
|
|
120
|
+
await (0, conventionsInit_js_1.runConventionsWizard)(cwd, {
|
|
121
|
+
llm: opts.llm,
|
|
122
|
+
yes: opts.yes,
|
|
123
|
+
dryRun: opts.dryRun,
|
|
124
|
+
glob: opts.glob,
|
|
125
|
+
maxFiles: opts.maxFiles ? Number(opts.maxFiles) : undefined,
|
|
126
|
+
fileTimeoutMs: opts.fileTimeout ? Number(opts.fileTimeout) : undefined,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
109
129
|
}
|
|
110
130
|
function loadDotEnv(cwd) {
|
|
111
131
|
const envPath = path.join(cwd, ".env");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { RegressionResult } from "../../services/ReportGenerator.js";
|
|
3
|
+
/**
|
|
4
|
+
* `sela shadow-regression --manifest <path>`
|
|
5
|
+
*
|
|
6
|
+
* Job 2 of the stateless shadow-regression flow. Reads the manifest emitted by
|
|
7
|
+
* Job 1, re-runs the affected specs with the proposed shared-locator heal hot-
|
|
8
|
+
* swapped into memory (SELA_REGRESSION_ACTIVE), parses Playwright's JSON
|
|
9
|
+
* report, and merges a PASSED/FAILED verdict into the existing sela-report.
|
|
10
|
+
* Zero source-code mutation — the heal lives only in execution memory.
|
|
11
|
+
*
|
|
12
|
+
* Exit code: 1 when the regression FAILS (downstream side-effects), else 0.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerShadowRegression(program: Command): void;
|
|
15
|
+
/**
|
|
16
|
+
* Flatten a Playwright JSON report into RegressionResult[], walking nested
|
|
17
|
+
* suites. A spec is FAILED when `ok` is false; the first failing result's error
|
|
18
|
+
* (message + stack, ANSI-stripped) is attached for the report drilldown.
|
|
19
|
+
*/
|
|
20
|
+
export declare function collectResults(report: any): RegressionResult[];
|
|
21
|
+
//# sourceMappingURL=shadowRegression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadowRegression.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/shadowRegression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOzC,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,mCAAmC,CAAC;AAe3C;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgI/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,GAAG,gBAAgB,EAAE,CAqC9D"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerShadowRegression = registerShadowRegression;
|
|
37
|
+
exports.collectResults = collectResults;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const RegressionManifestService_js_1 = require("../../services/RegressionManifestService.js");
|
|
43
|
+
const HealReportService_js_1 = require("../../services/HealReportService.js");
|
|
44
|
+
// eslint-disable-next-line no-control-regex
|
|
45
|
+
const ANSI = /\[[0-9;]*m/g;
|
|
46
|
+
function stripAnsi(s) {
|
|
47
|
+
return s.replace(ANSI, "");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `sela shadow-regression --manifest <path>`
|
|
51
|
+
*
|
|
52
|
+
* Job 2 of the stateless shadow-regression flow. Reads the manifest emitted by
|
|
53
|
+
* Job 1, re-runs the affected specs with the proposed shared-locator heal hot-
|
|
54
|
+
* swapped into memory (SELA_REGRESSION_ACTIVE), parses Playwright's JSON
|
|
55
|
+
* report, and merges a PASSED/FAILED verdict into the existing sela-report.
|
|
56
|
+
* Zero source-code mutation — the heal lives only in execution memory.
|
|
57
|
+
*
|
|
58
|
+
* Exit code: 1 when the regression FAILS (downstream side-effects), else 0.
|
|
59
|
+
*/
|
|
60
|
+
function registerShadowRegression(program) {
|
|
61
|
+
program
|
|
62
|
+
.command("shadow-regression")
|
|
63
|
+
.description("Verify a bypassed shared-locator heal against its downstream blast radius " +
|
|
64
|
+
"(stateless, in-memory, zero disk mutation)")
|
|
65
|
+
.requiredOption("--manifest <path>", "Path to sela-regression-manifest.json (emitted by the primary run)")
|
|
66
|
+
.option("--dir <path>", "Project dir holding sela-report.json (merge target)", process.cwd())
|
|
67
|
+
.option("--config <path>", "Playwright config to pass through to the run")
|
|
68
|
+
.option("--report <path>", "Override path to the base sela-report.json to merge into")
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const targetDir = path.resolve(opts.dir ?? process.cwd());
|
|
71
|
+
const manifestAbs = path.isAbsolute(opts.manifest)
|
|
72
|
+
? opts.manifest
|
|
73
|
+
: path.resolve(process.cwd(), opts.manifest);
|
|
74
|
+
// 1. Load + validate the manifest (throws loudly on bad input).
|
|
75
|
+
let manifest;
|
|
76
|
+
try {
|
|
77
|
+
manifest = RegressionManifestService_js_1.RegressionManifestService.readManifest(manifestAbs);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
process.stderr.write(`[sela] ${err.message}\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (manifest.records.length === 0) {
|
|
85
|
+
process.stdout.write("[sela] Manifest has no bypassed locators — nothing to verify.\n");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// 2. Union of affected spec files across all records (dedup).
|
|
89
|
+
const affectedFiles = Array.from(new Set(manifest.records.flatMap((r) => r.affectedFiles)));
|
|
90
|
+
if (affectedFiles.length === 0) {
|
|
91
|
+
process.stdout.write("[sela] No affected files in manifest — nothing to re-run.\n");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// 3. Run Playwright over the affected files with the JSON reporter and
|
|
95
|
+
// the injection env active. JSON goes to a temp file via
|
|
96
|
+
// PLAYWRIGHT_JSON_OUTPUT_NAME so stdout stays clean.
|
|
97
|
+
const tmpJson = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "sela-shadow-")), "pw-report.json");
|
|
98
|
+
const pwArgs = ["playwright", "test", ...affectedFiles, "--reporter=json"];
|
|
99
|
+
if (opts.config)
|
|
100
|
+
pwArgs.push(`--config=${opts.config}`);
|
|
101
|
+
process.stdout.write(`[sela] Shadow regression: re-running ${affectedFiles.length} affected ` +
|
|
102
|
+
`spec file(s) with ${manifest.records.length} injected heal(s)…\n`);
|
|
103
|
+
const run = (0, child_process_1.spawnSync)("npx", pwArgs, {
|
|
104
|
+
cwd: targetDir,
|
|
105
|
+
shell: true,
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
SELA_REGRESSION_ACTIVE: "true",
|
|
109
|
+
SELA_REGRESSION_MANIFEST: manifestAbs,
|
|
110
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: tmpJson,
|
|
111
|
+
},
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
114
|
+
});
|
|
115
|
+
// 4. Parse the JSON report → RegressionResult[].
|
|
116
|
+
let pwReport;
|
|
117
|
+
try {
|
|
118
|
+
pwReport = JSON.parse(fs.readFileSync(tmpJson, "utf8"));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
process.stderr.write(`[sela] Could not read Playwright JSON report (${tmpJson}): ${err.message}\n`);
|
|
122
|
+
if (run.stderr)
|
|
123
|
+
process.stderr.write(run.stderr.toString());
|
|
124
|
+
process.exit(1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const regressionTests = collectResults(pwReport);
|
|
128
|
+
const regressionState = regressionTests.some((t) => t.status === "FAILED")
|
|
129
|
+
? "FAILURE"
|
|
130
|
+
: "SUCCESS";
|
|
131
|
+
// 5. Merge the verdict into the report + regenerate the dashboard.
|
|
132
|
+
let htmlPath;
|
|
133
|
+
try {
|
|
134
|
+
htmlPath = (0, HealReportService_js_1.mergeRegressionIntoReport)(targetDir, { regressionState, regressionTests }, { reportJsonPath: opts.report });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
process.stderr.write(`[sela] Report merge failed: ${err.message}\n`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const passN = regressionTests.filter((t) => t.status === "PASSED").length;
|
|
142
|
+
const failN = regressionTests.filter((t) => t.status === "FAILED").length;
|
|
143
|
+
process.stdout.write(`[sela] Regression ${regressionState}: ${passN} passed · ${failN} failed.\n` +
|
|
144
|
+
` Report updated: ${path.relative(process.cwd(), htmlPath)}\n`);
|
|
145
|
+
// FAILURE = the proposed fix breaks downstream tests → fail the CI job.
|
|
146
|
+
process.exit(regressionState === "FAILURE" ? 1 : 0);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Flatten a Playwright JSON report into RegressionResult[], walking nested
|
|
151
|
+
* suites. A spec is FAILED when `ok` is false; the first failing result's error
|
|
152
|
+
* (message + stack, ANSI-stripped) is attached for the report drilldown.
|
|
153
|
+
*/
|
|
154
|
+
function collectResults(report) {
|
|
155
|
+
const out = [];
|
|
156
|
+
const walk = (suite, fileFallback) => {
|
|
157
|
+
const file = suite?.file ?? fileFallback;
|
|
158
|
+
for (const spec of suite?.specs ?? []) {
|
|
159
|
+
const specFile = spec.file ?? file ?? "";
|
|
160
|
+
const ok = spec.ok !== false;
|
|
161
|
+
let error;
|
|
162
|
+
if (!ok) {
|
|
163
|
+
for (const t of spec.tests ?? []) {
|
|
164
|
+
for (const r of t.results ?? []) {
|
|
165
|
+
if (r.status === "passed" || r.status === "skipped")
|
|
166
|
+
continue;
|
|
167
|
+
const e = (r.errors && r.errors[0]) || r.error;
|
|
168
|
+
if (e) {
|
|
169
|
+
error = {
|
|
170
|
+
message: stripAnsi(String(e.message ?? "")),
|
|
171
|
+
stack: e.stack ? stripAnsi(String(e.stack)) : undefined,
|
|
172
|
+
};
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (error)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
out.push({
|
|
181
|
+
status: ok ? "PASSED" : "FAILED",
|
|
182
|
+
testName: spec.title ?? "",
|
|
183
|
+
filePath: specFile,
|
|
184
|
+
...(error ? { error } : {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const child of suite?.suites ?? [])
|
|
188
|
+
walk(child, file);
|
|
189
|
+
};
|
|
190
|
+
for (const s of report?.suites ?? [])
|
|
191
|
+
walk(s);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { type ScanOptions } from "../services/ConventionsScanner.js";
|
|
3
|
+
import { type Conventions, type ScanStats } from "../config/Conventions.js";
|
|
4
|
+
export type DiscoveryMode = "smart" | "folders" | "glob";
|
|
5
|
+
export interface ConventionsInitOptions {
|
|
6
|
+
/** smart | folders | glob. */
|
|
7
|
+
mode?: DiscoveryMode;
|
|
8
|
+
/** Folders (cwd-relative) when mode === 'folders'. */
|
|
9
|
+
folders?: string[];
|
|
10
|
+
/** Glob pattern when mode === 'glob'. */
|
|
11
|
+
glob?: string;
|
|
12
|
+
fileTimeoutMs?: number;
|
|
13
|
+
maxFiles?: number;
|
|
14
|
+
/** Run the optional LLM polish step. Default: false. */
|
|
15
|
+
llm?: boolean;
|
|
16
|
+
/** Injected Anthropic client (tests). Falls back to a sanitizing client. */
|
|
17
|
+
client?: Anthropic;
|
|
18
|
+
model?: string;
|
|
19
|
+
/** Print JSON to stdout, write nothing. */
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Convert a glob (supports **, *, ?) to an anchored RegExp over posix paths. */
|
|
23
|
+
export declare function globToRegex(glob: string): RegExp;
|
|
24
|
+
/** Resolve the absolute, sorted spec file list for a discovery mode. */
|
|
25
|
+
export declare function resolveSpecFiles(cwd: string, opts: Pick<ConventionsInitOptions, "mode" | "folders" | "glob">): string[];
|
|
26
|
+
export interface GenerationResult {
|
|
27
|
+
conventions: Conventions;
|
|
28
|
+
stats: ScanStats;
|
|
29
|
+
}
|
|
30
|
+
export declare function generateConventions(files: string[], opts?: ConventionsInitOptions & {
|
|
31
|
+
onProgress?: ScanOptions["onProgress"];
|
|
32
|
+
}): Promise<GenerationResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Optional LLM refinement. Sends ONLY the integer stats blob + the draft (no
|
|
35
|
+
* raw source) and asks Claude to correct obvious misjudgements while keeping
|
|
36
|
+
* the exact schema. Fully defensive: any failure returns null → caller keeps
|
|
37
|
+
* the deterministic draft.
|
|
38
|
+
*/
|
|
39
|
+
export declare function polishConventions(draft: Conventions, stats: ScanStats, client: Anthropic, model: string): Promise<Conventions | null>;
|
|
40
|
+
export interface WriteResult {
|
|
41
|
+
path: string;
|
|
42
|
+
json: string;
|
|
43
|
+
written: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function writeConventionsArtifact(cwd: string, conventions: Conventions, opts?: {
|
|
46
|
+
dryRun?: boolean;
|
|
47
|
+
}): WriteResult;
|
|
48
|
+
export declare function runConventionsWizard(cwd: string, opts?: ConventionsInitOptions & {
|
|
49
|
+
yes?: boolean;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
//# sourceMappingURL=conventionsInit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conventionsInit.d.ts","sourceRoot":"","sources":["../../src/cli/conventionsInit.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAC;AAO/C,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EAEf,MAAM,0BAA0B,CAAC;AAelC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,4EAA4E;IAC5E,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAkCD,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0BhD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,GAC9D,MAAM,EAAE,CAcV;AAMD,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,GAAE,sBAAsB,GAAG;IAAE,UAAU,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAO,GAC7E,OAAO,CAAC,gBAAgB,CAAC,CAsB3B;AAQD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA4C7B;AAMD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,WAAW,EACxB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9B,WAAW,CAQb;AAMD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,sBAAsB,GAAG;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CAgHf"}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/cli/conventionsInit.ts
|
|
3
|
+
//
|
|
4
|
+
// `sela init` conventions wizard — file discovery, deterministic generation,
|
|
5
|
+
// optional LLM polish, and artifact write. The pure pieces (resolveSpecFiles,
|
|
6
|
+
// generateConventions, writeConventionsArtifact) are exported separately from
|
|
7
|
+
// the interactive `runConventionsWizard` so they can be unit-tested without a
|
|
8
|
+
// TTY or network.
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.globToRegex = globToRegex;
|
|
44
|
+
exports.resolveSpecFiles = resolveSpecFiles;
|
|
45
|
+
exports.generateConventions = generateConventions;
|
|
46
|
+
exports.polishConventions = polishConventions;
|
|
47
|
+
exports.writeConventionsArtifact = writeConventionsArtifact;
|
|
48
|
+
exports.runConventionsWizard = runConventionsWizard;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
52
|
+
const atomicWrite_js_1 = require("../utils/atomicWrite.js");
|
|
53
|
+
const ProgressReporter_js_1 = require("./ui/ProgressReporter.js");
|
|
54
|
+
const ConventionsTable_js_1 = require("./ui/ConventionsTable.js");
|
|
55
|
+
const enquirer_js_1 = require("../vendor/enquirer.js");
|
|
56
|
+
const SanitizingAnthropic_js_1 = require("../services/SanitizingAnthropic.js");
|
|
57
|
+
const ConventionsScanner_js_1 = require("../services/ConventionsScanner.js");
|
|
58
|
+
const Conventions_js_1 = require("../config/Conventions.js");
|
|
59
|
+
const SPEC_EXTS = [".spec.ts", ".spec.js", ".test.ts", ".test.js"];
|
|
60
|
+
const IGNORE_DIRS = new Set([
|
|
61
|
+
"node_modules",
|
|
62
|
+
"dist",
|
|
63
|
+
".git",
|
|
64
|
+
"sela-snapshots",
|
|
65
|
+
"test-results",
|
|
66
|
+
"playwright-report",
|
|
67
|
+
".sela",
|
|
68
|
+
]);
|
|
69
|
+
const DEFAULT_LLM_MODEL = "claude-sonnet-4-6";
|
|
70
|
+
const CONVENTIONS_REL = path.join(".sela", "conventions.json");
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────
|
|
72
|
+
// FILE DISCOVERY
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────
|
|
74
|
+
function isSpec(name) {
|
|
75
|
+
return SPEC_EXTS.some((e) => name.endsWith(e));
|
|
76
|
+
}
|
|
77
|
+
function walkSpecs(root, matchAll, re, base) {
|
|
78
|
+
const out = [];
|
|
79
|
+
const cwd = base ?? root;
|
|
80
|
+
const recurse = (dir) => {
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const full = path.join(dir, e.name);
|
|
90
|
+
if (e.isDirectory()) {
|
|
91
|
+
if (!IGNORE_DIRS.has(e.name))
|
|
92
|
+
recurse(full);
|
|
93
|
+
}
|
|
94
|
+
else if (matchAll ? isSpec(e.name) : re) {
|
|
95
|
+
const rel = path.relative(cwd, full).replace(/\\/g, "/");
|
|
96
|
+
if (matchAll ? isSpec(e.name) : re.test(rel))
|
|
97
|
+
out.push(full);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
recurse(root);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** Convert a glob (supports **, *, ?) to an anchored RegExp over posix paths. */
|
|
105
|
+
function globToRegex(glob) {
|
|
106
|
+
const g = glob.replace(/\\/g, "/");
|
|
107
|
+
let re = "";
|
|
108
|
+
for (let i = 0; i < g.length; i++) {
|
|
109
|
+
const c = g[i];
|
|
110
|
+
if (c === "*") {
|
|
111
|
+
if (g[i + 1] === "*") {
|
|
112
|
+
if (g[i + 2] === "/") {
|
|
113
|
+
re += "(?:.*/)?";
|
|
114
|
+
i += 2;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
re += ".*";
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
re += "[^/]*";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (c === "?") {
|
|
126
|
+
re += "[^/]";
|
|
127
|
+
}
|
|
128
|
+
else if (".+^${}()|[]\\".includes(c)) {
|
|
129
|
+
re += "\\" + c;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
re += c;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return new RegExp("^" + re + "$");
|
|
136
|
+
}
|
|
137
|
+
/** Resolve the absolute, sorted spec file list for a discovery mode. */
|
|
138
|
+
function resolveSpecFiles(cwd, opts) {
|
|
139
|
+
const mode = opts.mode ?? "smart";
|
|
140
|
+
let files;
|
|
141
|
+
if (mode === "glob" && opts.glob) {
|
|
142
|
+
files = walkSpecs(cwd, false, globToRegex(opts.glob), cwd);
|
|
143
|
+
}
|
|
144
|
+
else if (mode === "folders" && opts.folders?.length) {
|
|
145
|
+
files = opts.folders.flatMap((f) => walkSpecs(path.resolve(cwd, f), true, undefined, cwd));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
files = walkSpecs(cwd, true, undefined, cwd);
|
|
149
|
+
}
|
|
150
|
+
// Dedupe + sort for fully deterministic scan order.
|
|
151
|
+
return [...new Set(files)].sort();
|
|
152
|
+
}
|
|
153
|
+
async function generateConventions(files, opts = {}) {
|
|
154
|
+
const stats = (0, ConventionsScanner_js_1.scan)(files, {
|
|
155
|
+
fileTimeoutMs: opts.fileTimeoutMs,
|
|
156
|
+
maxFiles: opts.maxFiles,
|
|
157
|
+
onProgress: opts.onProgress,
|
|
158
|
+
});
|
|
159
|
+
let conventions = (0, ConventionsScanner_js_1.deriveConventions)(stats);
|
|
160
|
+
if (opts.llm) {
|
|
161
|
+
const client = opts.client ?? defaultClient();
|
|
162
|
+
if (client) {
|
|
163
|
+
const polished = await polishConventions(conventions, stats, client, opts.model ?? process.env.SELA_LLM_MODEL ?? DEFAULT_LLM_MODEL);
|
|
164
|
+
if (polished)
|
|
165
|
+
conventions = polished;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { conventions, stats };
|
|
169
|
+
}
|
|
170
|
+
function defaultClient() {
|
|
171
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
172
|
+
if (!apiKey)
|
|
173
|
+
return null;
|
|
174
|
+
return (0, SanitizingAnthropic_js_1.createSanitizingAnthropic)({ apiKey });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Optional LLM refinement. Sends ONLY the integer stats blob + the draft (no
|
|
178
|
+
* raw source) and asks Claude to correct obvious misjudgements while keeping
|
|
179
|
+
* the exact schema. Fully defensive: any failure returns null → caller keeps
|
|
180
|
+
* the deterministic draft.
|
|
181
|
+
*/
|
|
182
|
+
async function polishConventions(draft, stats, client, model) {
|
|
183
|
+
const payload = {
|
|
184
|
+
strategyCounts: stats.strategyCounts,
|
|
185
|
+
archCounts: stats.archCounts,
|
|
186
|
+
caseCounts: stats.caseCounts,
|
|
187
|
+
testIdAttrCounts: stats.testIdAttrCounts,
|
|
188
|
+
sourceModuleCounts: stats.sourceModuleCounts,
|
|
189
|
+
draft: {
|
|
190
|
+
strategies: draft.strategies,
|
|
191
|
+
architecture: draft.architecture,
|
|
192
|
+
naming: draft.naming,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const system = "You refine a Playwright testing-conventions blueprint. You are given " +
|
|
196
|
+
"integer usage counts and a deterministic draft. Correct only clear " +
|
|
197
|
+
"misjudgements. Keep the EXACT JSON schema of `draft` (strategies, " +
|
|
198
|
+
"architecture, naming). Respond ONLY with that raw JSON object.";
|
|
199
|
+
try {
|
|
200
|
+
const msg = await client.messages.create({
|
|
201
|
+
model,
|
|
202
|
+
max_tokens: 512,
|
|
203
|
+
system,
|
|
204
|
+
messages: [{ role: "user", content: JSON.stringify(payload) }],
|
|
205
|
+
});
|
|
206
|
+
const text = msg.content[0]?.type === "text" ? msg.content[0].text : "";
|
|
207
|
+
const json = text.replace(/```json/g, "").replace(/```/g, "").trim();
|
|
208
|
+
const first = json.indexOf("{");
|
|
209
|
+
const last = json.lastIndexOf("}");
|
|
210
|
+
if (first === -1 || last === -1)
|
|
211
|
+
return null;
|
|
212
|
+
const parsed = JSON.parse(json.slice(first, last + 1));
|
|
213
|
+
const merged = {
|
|
214
|
+
...draft,
|
|
215
|
+
strategies: parsed.strategies ?? draft.strategies,
|
|
216
|
+
architecture: parsed.architecture ?? draft.architecture,
|
|
217
|
+
naming: parsed.naming ?? draft.naming,
|
|
218
|
+
_meta: { ...draft._meta, source: "llm-polished" },
|
|
219
|
+
};
|
|
220
|
+
if (!(0, Conventions_js_1.validateConventions)(merged).valid)
|
|
221
|
+
return null;
|
|
222
|
+
return merged;
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
logger_js_1.logger.debug(`Conventions polish skipped: ${err.message}`);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function writeConventionsArtifact(cwd, conventions, opts = {}) {
|
|
230
|
+
const target = path.join(cwd, CONVENTIONS_REL);
|
|
231
|
+
const json = JSON.stringify(conventions, null, 2) + "\n";
|
|
232
|
+
if (opts.dryRun)
|
|
233
|
+
return { path: target, json, written: false };
|
|
234
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
235
|
+
(0, atomicWrite_js_1.atomicWriteFile)(target, json);
|
|
236
|
+
return { path: target, json, written: true };
|
|
237
|
+
}
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────
|
|
239
|
+
// INTERACTIVE WIZARD
|
|
240
|
+
// ─────────────────────────────────────────────────────────────────
|
|
241
|
+
async function runConventionsWizard(cwd, opts = {}) {
|
|
242
|
+
const interactive = !opts.yes && !opts.glob && !opts.dryRun && Boolean(process.stdout.isTTY);
|
|
243
|
+
// ── Discovery selection ───────────────────────────────────────
|
|
244
|
+
let mode = opts.mode ?? (opts.glob ? "glob" : "smart");
|
|
245
|
+
let glob = opts.glob;
|
|
246
|
+
let folders = opts.folders;
|
|
247
|
+
if (interactive && !opts.mode) {
|
|
248
|
+
const choice = await new enquirer_js_1.Select({
|
|
249
|
+
message: "How should Sela discover your test files?",
|
|
250
|
+
choices: [
|
|
251
|
+
{ name: "smart", message: "Smart discovery — scan **/*.spec.{ts,js}" },
|
|
252
|
+
{ name: "folders", message: "Pick folders — choose test directories" },
|
|
253
|
+
{ name: "glob", message: "Custom glob — type your own pattern" },
|
|
254
|
+
],
|
|
255
|
+
}).run();
|
|
256
|
+
mode = choice;
|
|
257
|
+
if (mode === "glob") {
|
|
258
|
+
glob = await new enquirer_js_1.Input({
|
|
259
|
+
message: "Glob pattern",
|
|
260
|
+
initial: "tests/**/*.spec.ts",
|
|
261
|
+
}).run();
|
|
262
|
+
}
|
|
263
|
+
else if (mode === "folders") {
|
|
264
|
+
const raw = await new enquirer_js_1.Input({
|
|
265
|
+
message: "Folders (comma-separated)",
|
|
266
|
+
initial: "tests",
|
|
267
|
+
}).run();
|
|
268
|
+
folders = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const files = resolveSpecFiles(cwd, { mode, glob, folders });
|
|
272
|
+
if (files.length === 0) {
|
|
273
|
+
logger_js_1.logger.warn("No test files matched — nothing to analyze.");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// ── Scan (streamed spinner) ───────────────────────────────────
|
|
277
|
+
const reporter = new ProgressReporter_js_1.ProgressReporter(!interactive);
|
|
278
|
+
const { conventions, stats } = await reporter.run(`Analyzing ${files.length} file(s)…`, () => generateConventions(files, {
|
|
279
|
+
...opts,
|
|
280
|
+
llm: false, // polish handled separately below (confirmable)
|
|
281
|
+
}));
|
|
282
|
+
logger_js_1.logger.success(`Analyzed ${stats.locatorsAnalyzed} locator(s) across ${stats.filesScanned} file(s)`);
|
|
283
|
+
if (stats.skippedFiles.length > 0) {
|
|
284
|
+
logger_js_1.logger.warn(`Skipped ${stats.skippedFiles.length} file(s) (exceeded ${opts.fileTimeoutMs ?? 500}ms) — conventions may be partial`);
|
|
285
|
+
}
|
|
286
|
+
// ── Optional LLM polish ───────────────────────────────────────
|
|
287
|
+
let final = conventions;
|
|
288
|
+
const canPolish = opts.llm !== false && Boolean(process.env.ANTHROPIC_API_KEY);
|
|
289
|
+
const wantPolish = interactive
|
|
290
|
+
? canPolish &&
|
|
291
|
+
(await new enquirer_js_1.Confirm({
|
|
292
|
+
message: "Refine with Claude for edge-cases?",
|
|
293
|
+
initial: false,
|
|
294
|
+
}).run())
|
|
295
|
+
: Boolean(opts.llm) && canPolish;
|
|
296
|
+
if (wantPolish) {
|
|
297
|
+
const client = opts.client ?? defaultClient();
|
|
298
|
+
if (client) {
|
|
299
|
+
const polished = await reporter.run("Refining with Claude…", () => polishConventions(final, stats, client, opts.model ?? process.env.SELA_LLM_MODEL ?? DEFAULT_LLM_MODEL));
|
|
300
|
+
if (polished)
|
|
301
|
+
final = polished;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// ── Summary card ──────────────────────────────────────────────
|
|
305
|
+
logger_js_1.logger.info("\n" + (0, ConventionsTable_js_1.renderConventionsTable)(final));
|
|
306
|
+
// ── Dry-run prints JSON to stdout, writes nothing ─────────────
|
|
307
|
+
if (opts.dryRun) {
|
|
308
|
+
const { json } = writeConventionsArtifact(cwd, final, { dryRun: true });
|
|
309
|
+
process.stdout.write(json);
|
|
310
|
+
logger_js_1.logger.info("(dry-run — no file written)");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// ── Confirm + write ───────────────────────────────────────────
|
|
314
|
+
const confirmWrite = opts.yes ||
|
|
315
|
+
!interactive ||
|
|
316
|
+
(await new enquirer_js_1.Confirm({
|
|
317
|
+
message: "Write .sela/conventions.json?",
|
|
318
|
+
initial: true,
|
|
319
|
+
}).run());
|
|
320
|
+
if (!confirmWrite) {
|
|
321
|
+
logger_js_1.logger.info("Skipped writing conventions.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const { path: written } = writeConventionsArtifact(cwd, final);
|
|
325
|
+
logger_js_1.logger.success(`Wrote ${path.relative(cwd, written)} ${"— commit this file to git"}`);
|
|
326
|
+
}
|