@skyramp/mcp 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/prompts/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +25 -8
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +87 -9
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/reportVerification.d.ts +9 -0
- package/build/utils/reportVerification.js +39 -6
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { readFile, stat } from "fs/promises";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
5
|
+
import { identifierRe } from "./lint-types.js";
|
|
6
|
+
import { computeAssertionMetrics, detectAssertionLanguage, subjectOf, } from "./metrics.js";
|
|
7
|
+
/**
|
|
8
|
+
* Read-only scan of the spec's locally imported AND CALLED modules (the
|
|
9
|
+
* modularized skyramp-utils/helper files), used by the removal gate: a
|
|
10
|
+
* baseline subject missing from the spec is not "removed" if an imported
|
|
11
|
+
* helper still asserts it.
|
|
12
|
+
*
|
|
13
|
+
* Two deliberate restrictions:
|
|
14
|
+
* - One-directional — helper assertions can only EXCUSE removals, never add
|
|
15
|
+
* to the spec's count or strength score: a shared utils file is imported by
|
|
16
|
+
* many specs, and crediting its assertions to each one would inflate every
|
|
17
|
+
* baseline comparison.
|
|
18
|
+
* - CALLED, not merely imported — an import statement plus a dead helper file
|
|
19
|
+
* would otherwise turn a correctly-blocked gutting into a PASS (same
|
|
20
|
+
* call-position requirement as the reuse gate's call-site matching). Only
|
|
21
|
+
* helpers whose imported bindings appear at a call position in the spec
|
|
22
|
+
* contribute subjects.
|
|
23
|
+
*
|
|
24
|
+
* Helpers are never modified.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_HELPER_FILES = 5;
|
|
27
|
+
const MAX_HELPER_BYTES = 256 * 1024;
|
|
28
|
+
// Import forms that bind local names to a RELATIVE module. Dynamic import()
|
|
29
|
+
// is deliberately absent: it binds no statically-trackable name, and dropping
|
|
30
|
+
// it errs strict (the removal stays a removal).
|
|
31
|
+
const JS_IMPORT_RES = [
|
|
32
|
+
// import defaultName from './x' | import * as ns from './x' |
|
|
33
|
+
// import { a, b as c } from './x' | import d, { a } from './x'
|
|
34
|
+
/import\s+([^'"()]+?)\s*from\s*['"](\.{1,2}\/[^'"]+)['"]/g,
|
|
35
|
+
// const d = require('./x') | const { a, b: c } = require('./x')
|
|
36
|
+
/(?:const|let|var)\s+([^=]+?)\s*=\s*require\s*\(\s*['"](\.{1,2}\/[^'"]+)['"]\s*\)/g,
|
|
37
|
+
];
|
|
38
|
+
const PY_IMPORT_RES = [
|
|
39
|
+
// from .x import a, b as c | from x import a
|
|
40
|
+
/^[ \t]*from\s+(\.*[\w.]+)\s+import\s+([^\n#]+)/gm,
|
|
41
|
+
// import x (module itself becomes the callable namespace)
|
|
42
|
+
/^[ \t]*import\s+([\w.]+)(?:\s+as\s+([\w]+))?/gm,
|
|
43
|
+
];
|
|
44
|
+
const JS_RESOLVE_SUFFIXES = [
|
|
45
|
+
"",
|
|
46
|
+
".ts",
|
|
47
|
+
".tsx",
|
|
48
|
+
".js",
|
|
49
|
+
".jsx",
|
|
50
|
+
".mjs",
|
|
51
|
+
".cjs",
|
|
52
|
+
".mts",
|
|
53
|
+
".cts",
|
|
54
|
+
"/index.ts",
|
|
55
|
+
"/index.js",
|
|
56
|
+
];
|
|
57
|
+
function pyCandidates(specDir, specifier) {
|
|
58
|
+
const moduleName = specifier.replace(/^\.+/, "").split(".")[0];
|
|
59
|
+
return moduleName ? [path.resolve(specDir, `${moduleName}.py`)] : [];
|
|
60
|
+
}
|
|
61
|
+
/** Local names bound by a JS import/require clause ("d, { a, b as c }"). */
|
|
62
|
+
function jsBoundNames(clause) {
|
|
63
|
+
const names = [];
|
|
64
|
+
const star = /\*\s*as\s+([\w$]+)/.exec(clause);
|
|
65
|
+
if (star)
|
|
66
|
+
names.push(star[1]);
|
|
67
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
68
|
+
if (braces) {
|
|
69
|
+
for (const part of braces[1].split(",")) {
|
|
70
|
+
const name = (part.split(/\s+as\s+|\s*:\s*/).pop() ?? "").trim();
|
|
71
|
+
if (/^[\w$]+$/.test(name))
|
|
72
|
+
names.push(name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const bare = clause
|
|
76
|
+
.replace(/\{[^}]*\}/, "")
|
|
77
|
+
.replace(/\*\s*as\s+[\w$]+/, "")
|
|
78
|
+
.replace(/,/g, " ")
|
|
79
|
+
.trim();
|
|
80
|
+
if (/^[\w$]+$/.test(bare))
|
|
81
|
+
names.push(bare);
|
|
82
|
+
return names;
|
|
83
|
+
}
|
|
84
|
+
/** Relative/local imports with the names they bind. */
|
|
85
|
+
function localImports(content, language) {
|
|
86
|
+
const commentless = strippedSources(content, language).commentless;
|
|
87
|
+
const imports = [];
|
|
88
|
+
if (language === "python") {
|
|
89
|
+
const [fromRe, importRe] = PY_IMPORT_RES;
|
|
90
|
+
fromRe.lastIndex = 0;
|
|
91
|
+
let m;
|
|
92
|
+
while ((m = fromRe.exec(commentless)) !== null) {
|
|
93
|
+
const names = m[2]
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((part) => (part.split(/\s+as\s+/).pop() ?? "").trim())
|
|
96
|
+
.filter((n) => /^\w+$/.test(n));
|
|
97
|
+
imports.push({ specifier: m[1], localNames: names });
|
|
98
|
+
}
|
|
99
|
+
importRe.lastIndex = 0;
|
|
100
|
+
while ((m = importRe.exec(commentless)) !== null) {
|
|
101
|
+
imports.push({ specifier: m[1], localNames: [m[2] ?? m[1]] });
|
|
102
|
+
}
|
|
103
|
+
return imports;
|
|
104
|
+
}
|
|
105
|
+
for (const re of JS_IMPORT_RES) {
|
|
106
|
+
re.lastIndex = 0;
|
|
107
|
+
let m;
|
|
108
|
+
while ((m = re.exec(commentless)) !== null) {
|
|
109
|
+
imports.push({ specifier: m[2], localNames: jsBoundNames(m[1]) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return imports;
|
|
113
|
+
}
|
|
114
|
+
/** True when any of the bound names appears at a CALL position in the spec —
|
|
115
|
+
* `name(...)` or `name.member(...)` — on comment/string-stripped source. */
|
|
116
|
+
function anyNameCalled(strippedSpec, names) {
|
|
117
|
+
// identifierRe gives the identifier boundary; the extra `(?<!\.)` keeps a
|
|
118
|
+
// same-named METHOD on another object (`other.name(...)`) from counting;
|
|
119
|
+
// the suffix requires a CALL position — `name(...)` or `name.member(...)`.
|
|
120
|
+
return names.some((name) => new RegExp(`(?<!\\.)${identifierRe(name).source}\\s*(?:\\(|\\.\\s*[\\w$]+\\s*\\()`).test(strippedSpec));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Assertion SUBJECTS found in the spec's locally imported, actually-called
|
|
124
|
+
* helper files. Strict-by-default failure direction: an unreadable/oversized/
|
|
125
|
+
* uncalled helper is skipped, so its subjects don't excuse anything.
|
|
126
|
+
*/
|
|
127
|
+
export async function importedHelperSubjects(testFile, content) {
|
|
128
|
+
const subjects = new Set();
|
|
129
|
+
const language = detectAssertionLanguage(testFile);
|
|
130
|
+
// Not a no-op: an empty subject set means NO removal gets excused by a
|
|
131
|
+
// helper — the strict direction. Unknown languages have no import scanner;
|
|
132
|
+
// java is count+hash gated only (no fingerprint subjects exist to excuse —
|
|
133
|
+
// same documented limit as contract-lints.ts).
|
|
134
|
+
if (language === undefined || language === "java")
|
|
135
|
+
return subjects;
|
|
136
|
+
const strippedSpec = strippedSources(content, language).stripped;
|
|
137
|
+
const specDir = path.dirname(testFile);
|
|
138
|
+
let scanned = 0;
|
|
139
|
+
for (const { specifier, localNames } of localImports(content, language)) {
|
|
140
|
+
if (scanned >= MAX_HELPER_FILES) {
|
|
141
|
+
logger.warning("Assertion verification scanned the maximum number of imported helper files — remaining imports ignored", { testFile, max: MAX_HELPER_FILES });
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
// An import whose bindings are never invoked cannot vouch for coverage —
|
|
145
|
+
// a dead helper file must not excuse a gutted spec.
|
|
146
|
+
if (localNames.length === 0 || !anyNameCalled(strippedSpec, localNames)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const candidates = language === "python"
|
|
150
|
+
? pyCandidates(specDir, specifier)
|
|
151
|
+
: JS_RESOLVE_SUFFIXES.map((s) => path.resolve(specDir, specifier) + s);
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
try {
|
|
154
|
+
const info = await stat(candidate);
|
|
155
|
+
// A directory hit (extensionless specifier) must not shadow the real
|
|
156
|
+
// module file — keep trying the remaining suffix candidates.
|
|
157
|
+
if (!info.isFile() || info.size > MAX_HELPER_BYTES)
|
|
158
|
+
continue;
|
|
159
|
+
const helperLanguage = detectAssertionLanguage(candidate);
|
|
160
|
+
if (helperLanguage === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
const helperContent = await readFile(candidate, "utf8");
|
|
163
|
+
for (const fp of computeAssertionMetrics(helperContent, helperLanguage)
|
|
164
|
+
.fingerprints) {
|
|
165
|
+
const subject = subjectOf(fp);
|
|
166
|
+
if (subject !== undefined)
|
|
167
|
+
subjects.add(subject);
|
|
168
|
+
}
|
|
169
|
+
scanned++;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Candidate doesn't exist / unreadable — try the next suffix.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return subjects;
|
|
178
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { detectAssertionLanguage, computeAssertionMetrics } from "./metrics.js";
|
|
2
|
+
export { verifyAssertionEnhancement, sha256Of, type AssertionBaseline, type AssertionEnhanceType, type AssertionVerifyResult, } from "./verify.js";
|
|
3
|
+
export { formatAssertionVerifyReport } from "./format.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Barrel: only what crosses the package boundary (assertion-state.ts and
|
|
2
|
+
// enhanceAssertionsTool.ts). Internal helpers and the per-test-type lint
|
|
3
|
+
// modules are imported directly by their sibling files and by tests — listing
|
|
4
|
+
// them here made every internal name look like public API.
|
|
5
|
+
export { detectAssertionLanguage, computeAssertionMetrics } from "./metrics.js";
|
|
6
|
+
export { verifyAssertionEnhancement, sha256Of, } from "./verify.js";
|
|
7
|
+
export { formatAssertionVerifyReport } from "./format.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
2
|
+
import { lineOfOffset, } from "./lint-types.js";
|
|
3
|
+
import { lintApiShared } from "./api-shared-lints.js";
|
|
4
|
+
/**
|
|
5
|
+
* Integration-test lints: the shared API checks plus the machine-checkable
|
|
6
|
+
* slice of the D5 `chaining` dim. Whether chained values are the RIGHT ones is
|
|
7
|
+
* judge territory; a literal UUID embedded in a request is not — an ID that
|
|
8
|
+
* exists before the run started cannot have come from a prior response.
|
|
9
|
+
*/
|
|
10
|
+
const UUID_IN_REQUEST_RE = /(?:sendRequest|send_request)\s*\([^)]*['"`][^'"`]*\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
11
|
+
/** A hardcoded UUID inside a request call — chained IDs should come from a
|
|
12
|
+
* prior step's response. Warn-only: fixtures may legitimately pin seeded IDs. */
|
|
13
|
+
function lintHardcodedRequestIds(commentless) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
UUID_IN_REQUEST_RE.lastIndex = 0;
|
|
16
|
+
let m;
|
|
17
|
+
while ((m = UUID_IN_REQUEST_RE.exec(commentless)) !== null) {
|
|
18
|
+
findings.push({
|
|
19
|
+
rule: "hardcoded-request-id",
|
|
20
|
+
severity: "warn",
|
|
21
|
+
line: lineOfOffset(commentless, m.index),
|
|
22
|
+
message: "A request embeds a hardcoded UUID.",
|
|
23
|
+
remediation: "Extract the ID from the prior step's response (SDK helper) instead of hardcoding it, unless it is intentionally seeded fixture data.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return findings;
|
|
27
|
+
}
|
|
28
|
+
export function lintIntegrationTest(raw, language, opts) {
|
|
29
|
+
if (language === "java")
|
|
30
|
+
return [];
|
|
31
|
+
const findings = lintApiShared(raw, language, opts);
|
|
32
|
+
if (opts?.scopeLines === undefined) {
|
|
33
|
+
findings.push(...lintHardcodedRequestIds(strippedSources(raw, language).commentless));
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function blankJsRegexLiterals(src: string): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offset-preserving blanking of JS/TS regex literals. `pom-scope/strip.ts`
|
|
3
|
+
* knows nothing about regex literals, so an apostrophe inside one (e.g.
|
|
4
|
+
* `getByText(/Don't have an account/)`) opens a phantom string span that
|
|
5
|
+
* swallows real code through the next quote — corrupting metrics and lints on
|
|
6
|
+
* perfectly valid specs. Blanking regex interiors first removes the hazard.
|
|
7
|
+
*
|
|
8
|
+
* Heuristic regex-start detection (`/` is a regex only where an expression may
|
|
9
|
+
* begin): the previous non-space character is an operator/opener/keyword end,
|
|
10
|
+
* not an identifier/literal end. Division therefore stays untouched. Only used
|
|
11
|
+
* as a pre-pass for scanners that never need regex CONTENT — lints that match
|
|
12
|
+
* regex text (permissive-status) run on comment-stripped source instead.
|
|
13
|
+
*/
|
|
14
|
+
const REGEX_MAY_FOLLOW = new Set([
|
|
15
|
+
"(",
|
|
16
|
+
",",
|
|
17
|
+
"=",
|
|
18
|
+
":",
|
|
19
|
+
"[",
|
|
20
|
+
"!",
|
|
21
|
+
"&",
|
|
22
|
+
"|",
|
|
23
|
+
"?",
|
|
24
|
+
"{",
|
|
25
|
+
";",
|
|
26
|
+
"+",
|
|
27
|
+
"-",
|
|
28
|
+
"*",
|
|
29
|
+
"%",
|
|
30
|
+
"<",
|
|
31
|
+
">",
|
|
32
|
+
"~",
|
|
33
|
+
"^",
|
|
34
|
+
]);
|
|
35
|
+
// Whole-token match only: an identifier merely ENDING in a keyword (`min`,
|
|
36
|
+
// `margin`, `todo`, `login`) is an ordinary value and a `/` after it is
|
|
37
|
+
// division — suffix-matching here silently blanked real code.
|
|
38
|
+
const KEYWORDS_BEFORE_REGEX = new Set([
|
|
39
|
+
"return",
|
|
40
|
+
"typeof",
|
|
41
|
+
"case",
|
|
42
|
+
"in",
|
|
43
|
+
"of",
|
|
44
|
+
"instanceof",
|
|
45
|
+
"new",
|
|
46
|
+
"do",
|
|
47
|
+
"else",
|
|
48
|
+
"void",
|
|
49
|
+
"delete",
|
|
50
|
+
"throw",
|
|
51
|
+
"await",
|
|
52
|
+
"yield",
|
|
53
|
+
]);
|
|
54
|
+
export function blankJsRegexLiterals(src) {
|
|
55
|
+
const out = src.split("");
|
|
56
|
+
let i = 0;
|
|
57
|
+
const n = src.length;
|
|
58
|
+
let inLineComment = false;
|
|
59
|
+
let inBlockComment = false;
|
|
60
|
+
let stringQuote;
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const c = src[i];
|
|
63
|
+
if (inLineComment) {
|
|
64
|
+
if (c === "\n")
|
|
65
|
+
inLineComment = false;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (inBlockComment) {
|
|
70
|
+
if (c === "*" && src[i + 1] === "/") {
|
|
71
|
+
inBlockComment = false;
|
|
72
|
+
i += 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (stringQuote !== undefined) {
|
|
79
|
+
if (c === "\\") {
|
|
80
|
+
i += 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === stringQuote || (c === "\n" && stringQuote !== "`")) {
|
|
84
|
+
stringQuote = undefined;
|
|
85
|
+
}
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
90
|
+
inLineComment = true;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
95
|
+
inBlockComment = true;
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
100
|
+
stringQuote = c;
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === "/") {
|
|
105
|
+
// Decide regex vs division from the previous non-whitespace token.
|
|
106
|
+
let j = i - 1;
|
|
107
|
+
while (j >= 0 && /\s/.test(src[j]))
|
|
108
|
+
j--;
|
|
109
|
+
const prev = j >= 0 ? src[j] : "";
|
|
110
|
+
// The COMPLETE identifier before the slash (scan across [\w$]) — only an
|
|
111
|
+
// exact keyword counts; after any other identifier, `/` is division.
|
|
112
|
+
let tokenStart = j;
|
|
113
|
+
while (tokenStart >= 0 && /[\w$]/.test(src[tokenStart]))
|
|
114
|
+
tokenStart--;
|
|
115
|
+
const prevToken = /[\w$]/.test(prev)
|
|
116
|
+
? src.slice(tokenStart + 1, j + 1)
|
|
117
|
+
: "";
|
|
118
|
+
const regexStart = j < 0 ||
|
|
119
|
+
REGEX_MAY_FOLLOW.has(prev) ||
|
|
120
|
+
KEYWORDS_BEFORE_REGEX.has(prevToken);
|
|
121
|
+
if (regexStart) {
|
|
122
|
+
// Scan to the closing unescaped `/` (a `/` inside [...] doesn't close).
|
|
123
|
+
let k = i + 1;
|
|
124
|
+
let inClass = false;
|
|
125
|
+
let closed = false;
|
|
126
|
+
while (k < n && src[k] !== "\n") {
|
|
127
|
+
const ck = src[k];
|
|
128
|
+
if (ck === "\\") {
|
|
129
|
+
k += 2;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ck === "[")
|
|
133
|
+
inClass = true;
|
|
134
|
+
else if (ck === "]")
|
|
135
|
+
inClass = false;
|
|
136
|
+
else if (ck === "/" && !inClass) {
|
|
137
|
+
closed = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
k++;
|
|
141
|
+
}
|
|
142
|
+
if (closed) {
|
|
143
|
+
for (let b = i; b <= k; b++)
|
|
144
|
+
out[b] = " ";
|
|
145
|
+
i = k + 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
return out.join("");
|
|
153
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and text helpers for the per-test-type deterministic lints
|
|
3
|
+
* (ui-lints.ts, integration-lints.ts, contract-lints.ts). Each lint mirrors a
|
|
4
|
+
* dim of the eval D5 assertion-depth rubric; only near-zero-false-positive
|
|
5
|
+
* rules are `hard` (they block the verify pass), everything a legitimate spec
|
|
6
|
+
* could trip ships as `warn` until run evidence justifies promotion.
|
|
7
|
+
*/
|
|
8
|
+
export interface LintFinding {
|
|
9
|
+
rule: string;
|
|
10
|
+
severity: "hard" | "warn";
|
|
11
|
+
/** 1-indexed line, when the finding anchors to one. */
|
|
12
|
+
line?: number;
|
|
13
|
+
message: string;
|
|
14
|
+
remediation: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LintOptions {
|
|
17
|
+
/** Maintenance scope: line numbers of assertion sites ADDED since the baseline.
|
|
18
|
+
* When set, site-scoped lints only report findings on these lines and
|
|
19
|
+
* structural (whole-file) lints — including the import lint — are skipped:
|
|
20
|
+
* they would flag pre-existing customer code the agent was told not to touch. */
|
|
21
|
+
scopeLines?: Set<number>;
|
|
22
|
+
}
|
|
23
|
+
export declare function lineOfOffset(src: string, offset: number): number;
|
|
24
|
+
/** `\b` breaks on `$`-names, so identifier boundaries use lookarounds. Single
|
|
25
|
+
* home for the pattern — it is subtle enough that hand-inlined copies drifted
|
|
26
|
+
* (found by the /simplify pass: six sites, one missing the f-string branch). */
|
|
27
|
+
export declare function identifierRe(name: string, flags?: string): RegExp;
|
|
28
|
+
/** A use of `name` inside a string interpolation — js/ts `${...}` or python
|
|
29
|
+
* f-string `{...}`. Interpolations are blanked wholesale by the string
|
|
30
|
+
* strippers, so callers test this against COMMENT-stripped source. */
|
|
31
|
+
export declare function interpolationRe(name: string, language: string): RegExp;
|
|
32
|
+
export declare function balancedCloseIndex(stripped: string, openIdx: number): number;
|
|
33
|
+
export declare function inScope(line: number, opts?: LintOptions): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { escapeRegExp } from "../regex.js";
|
|
2
|
+
// One-entry memo of a source's newline offsets: every scanner asks for line
|
|
3
|
+
// numbers of many offsets in the SAME string (assertion sites, lint findings),
|
|
4
|
+
// so counting newlines from index 0 per call was O(n × sites). The memo is
|
|
5
|
+
// per-string; a new source replaces it.
|
|
6
|
+
let newlineIndexSrc;
|
|
7
|
+
let newlineIndex = [];
|
|
8
|
+
export function lineOfOffset(src, offset) {
|
|
9
|
+
if (src !== newlineIndexSrc) {
|
|
10
|
+
newlineIndexSrc = src;
|
|
11
|
+
newlineIndex = [];
|
|
12
|
+
for (let i = 0; i < src.length; i++) {
|
|
13
|
+
if (src[i] === "\n")
|
|
14
|
+
newlineIndex.push(i);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Binary search: number of newlines strictly before `offset`, plus 1.
|
|
18
|
+
let lo = 0;
|
|
19
|
+
let hi = newlineIndex.length;
|
|
20
|
+
while (lo < hi) {
|
|
21
|
+
const mid = (lo + hi) >> 1;
|
|
22
|
+
if (newlineIndex[mid] < offset)
|
|
23
|
+
lo = mid + 1;
|
|
24
|
+
else
|
|
25
|
+
hi = mid;
|
|
26
|
+
}
|
|
27
|
+
return lo + 1;
|
|
28
|
+
}
|
|
29
|
+
/** `\b` breaks on `$`-names, so identifier boundaries use lookarounds. Single
|
|
30
|
+
* home for the pattern — it is subtle enough that hand-inlined copies drifted
|
|
31
|
+
* (found by the /simplify pass: six sites, one missing the f-string branch). */
|
|
32
|
+
export function identifierRe(name, flags = "") {
|
|
33
|
+
return new RegExp(`(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`, flags);
|
|
34
|
+
}
|
|
35
|
+
/** A use of `name` inside a string interpolation — js/ts `${...}` or python
|
|
36
|
+
* f-string `{...}`. Interpolations are blanked wholesale by the string
|
|
37
|
+
* strippers, so callers test this against COMMENT-stripped source. */
|
|
38
|
+
export function interpolationRe(name, language) {
|
|
39
|
+
const open = language === "python" ? "\\{" : "\\$\\{";
|
|
40
|
+
return new RegExp(`${open}[^}]*(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`);
|
|
41
|
+
}
|
|
42
|
+
export function balancedCloseIndex(stripped, openIdx) {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (let i = openIdx; i < stripped.length; i++) {
|
|
45
|
+
if (stripped[i] === "(")
|
|
46
|
+
depth++;
|
|
47
|
+
else if (stripped[i] === ")") {
|
|
48
|
+
depth--;
|
|
49
|
+
if (depth === 0)
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
export function inScope(line, opts) {
|
|
56
|
+
return opts?.scopeLines === undefined || opts.scopeLines.has(line);
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AssertionLanguage } from "./metrics.js";
|
|
2
|
+
/**
|
|
3
|
+
* The documented-decline escape hatch for the assertion verify pass, mirroring
|
|
4
|
+
* the `// kept inline:` marker discipline (reuse-outcome.ts): a spec whose
|
|
5
|
+
* generated assertions genuinely need no strengthening carries
|
|
6
|
+
*
|
|
7
|
+
* // assertions complete: <test-file basename> — <reason> (TS/JS/Java)
|
|
8
|
+
* # assertions complete: <test-file basename> — <reason> (Python)
|
|
9
|
+
*
|
|
10
|
+
* It clears ONLY the strength-family gates (strength-must-increase and
|
|
11
|
+
* weak-additions-only) — never a removal, a hard lint, or the hash gate: a
|
|
12
|
+
* marker that excused removals would excuse gutting the file. The reason is
|
|
13
|
+
* PR-reviewable in the diff.
|
|
14
|
+
*/
|
|
15
|
+
/** The comment token that makes the marker a REAL comment in this language —
|
|
16
|
+
* a `//` marker in a .py file is a syntax error, so accepting it would go
|
|
17
|
+
* green on a file that no longer parses. */
|
|
18
|
+
export declare function markerCommentToken(language: AssertionLanguage): string;
|
|
19
|
+
/** The well-formed marker naming this test file, or undefined. */
|
|
20
|
+
export declare function findAssertionsCompleteMarker(content: string, testFile: string, language: AssertionLanguage): {
|
|
21
|
+
reason: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
/** Lines announcing an `assertions complete` decline that markerRe cannot read
|
|
24
|
+
* (missing reason, unspaced hyphen, unedited placeholder, wrong comment token
|
|
25
|
+
* for the language). Reported as their own verify failure with the corrected
|
|
26
|
+
* format, so the decline is never lost. */
|
|
27
|
+
export declare function findUnparsableAssertionsComplete(content: string, language: AssertionLanguage): string[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
3
|
+
/**
|
|
4
|
+
* The documented-decline escape hatch for the assertion verify pass, mirroring
|
|
5
|
+
* the `// kept inline:` marker discipline (reuse-outcome.ts): a spec whose
|
|
6
|
+
* generated assertions genuinely need no strengthening carries
|
|
7
|
+
*
|
|
8
|
+
* // assertions complete: <test-file basename> — <reason> (TS/JS/Java)
|
|
9
|
+
* # assertions complete: <test-file basename> — <reason> (Python)
|
|
10
|
+
*
|
|
11
|
+
* It clears ONLY the strength-family gates (strength-must-increase and
|
|
12
|
+
* weak-additions-only) — never a removal, a hard lint, or the hash gate: a
|
|
13
|
+
* marker that excused removals would excuse gutting the file. The reason is
|
|
14
|
+
* PR-reviewable in the diff.
|
|
15
|
+
*/
|
|
16
|
+
/** The comment token that makes the marker a REAL comment in this language —
|
|
17
|
+
* a `//` marker in a .py file is a syntax error, so accepting it would go
|
|
18
|
+
* green on a file that no longer parses. */
|
|
19
|
+
export function markerCommentToken(language) {
|
|
20
|
+
return language === "python" ? "#" : "//";
|
|
21
|
+
}
|
|
22
|
+
/** Separator must be an em/en dash or a *spaced* hyphen — same decidability
|
|
23
|
+
* reasoning as KEPT_INLINE_RE. `(?!\s*<reason)` rejects the paste-ready
|
|
24
|
+
* placeholder from the failure text pasted unedited. */
|
|
25
|
+
function markerRe(language) {
|
|
26
|
+
return new RegExp(`${markerCommentToken(language)}\\s*assertions complete:\\s*(\\S.*?)\\s*(?:[—–]+|\\s+-+\\s+)(?!\\s*<reason)\\s*(\\S.*?)\\s*$`);
|
|
27
|
+
}
|
|
28
|
+
/** Looser than markerRe so a malformed marker is detectable, never silently
|
|
29
|
+
* lost — including a marker written with the WRONG language's comment token. */
|
|
30
|
+
const MARKER_LOOSE_RE = /(?:\/\/|#)\s*assertions complete:/;
|
|
31
|
+
/** Comment lines only — a marker quoted inside a string literal is not a marker. */
|
|
32
|
+
function commentLines(content, language) {
|
|
33
|
+
return strippedSources(content, language).stringless.split("\n");
|
|
34
|
+
}
|
|
35
|
+
/** Basename or extension-less stem — same leniency as `isKeptInlineDocumented`. */
|
|
36
|
+
function namesTestFile(named, testFile) {
|
|
37
|
+
const base = path.basename(testFile);
|
|
38
|
+
const stem = base.slice(0, base.length - path.extname(base).length);
|
|
39
|
+
const cleaned = path.basename(named.trim());
|
|
40
|
+
return cleaned === base || cleaned === stem;
|
|
41
|
+
}
|
|
42
|
+
/** The well-formed marker naming this test file, or undefined. */
|
|
43
|
+
export function findAssertionsCompleteMarker(content, testFile, language) {
|
|
44
|
+
const re = markerRe(language);
|
|
45
|
+
for (const line of commentLines(content, language)) {
|
|
46
|
+
const m = re.exec(line);
|
|
47
|
+
if (m && namesTestFile(m[1], testFile))
|
|
48
|
+
return { reason: m[2].trim() };
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Lines announcing an `assertions complete` decline that markerRe cannot read
|
|
53
|
+
* (missing reason, unspaced hyphen, unedited placeholder, wrong comment token
|
|
54
|
+
* for the language). Reported as their own verify failure with the corrected
|
|
55
|
+
* format, so the decline is never lost. */
|
|
56
|
+
export function findUnparsableAssertionsComplete(content, language) {
|
|
57
|
+
const re = markerRe(language);
|
|
58
|
+
return commentLines(content, language)
|
|
59
|
+
.filter((line) => MARKER_LOOSE_RE.test(line) && !re.test(line))
|
|
60
|
+
.map((line) => line.trim());
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
2
|
+
export type AssertionLanguage = `${ProgrammingLanguage}`;
|
|
3
|
+
/** One assertion call/statement site. Multi-line chains count once. */
|
|
4
|
+
export interface AssertionSite {
|
|
5
|
+
/** 1-indexed line of the site's first character. */
|
|
6
|
+
line: number;
|
|
7
|
+
/** Matcher name for expect-chains (`toHaveText`, `to_be`), undefined for bare asserts. */
|
|
8
|
+
matcher?: string;
|
|
9
|
+
weight: number;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AssertionMetrics {
|
|
13
|
+
count: number;
|
|
14
|
+
strengthScore: number;
|
|
15
|
+
/** Whitespace-collapsed statement texts, 120-char cap, ≤500 — the multiset used
|
|
16
|
+
* to scope maintenance-mode checks to assertions added since the baseline. */
|
|
17
|
+
fingerprints: string[];
|
|
18
|
+
sites: AssertionSite[];
|
|
19
|
+
}
|
|
20
|
+
export declare function detectAssertionLanguage(testFile: string): AssertionLanguage | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The assertion's SUBJECT — what it asserts about — recovered from a fingerprint
|
|
23
|
+
* (`expect(SUBJECT).matcher(...)` arg, or a python assert's left-hand side).
|
|
24
|
+
* Correlating subjects across baseline and current fingerprints is what tells a
|
|
25
|
+
* REPLACED assertion (subject still asserted, usually more strongly) from a
|
|
26
|
+
* REMOVED one (subject lost all coverage). Undefined when unrecoverable (e.g.
|
|
27
|
+
* truncated fingerprint) — callers treat that conservatively.
|
|
28
|
+
*/
|
|
29
|
+
export declare function subjectOf(fingerprint: string): string | undefined;
|
|
30
|
+
export declare function computeAssertionMetrics(content: string, language: AssertionLanguage): AssertionMetrics;
|