@skyramp/mcp 0.3.4 → 0.3.6-rc.1
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/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +88 -33
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +487 -104
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +75 -12
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +149 -23
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +25 -3
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +69 -1
- package/build/utils/AnalysisStateManager.js +69 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportLanguage.d.ts +43 -0
- package/build/utils/reportLanguage.js +125 -0
- package/build/utils/reportVerification.d.ts +74 -4
- package/build/utils/reportVerification.js +259 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- 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 +5 -0
- 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/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* matching (contract tests, or a fallback when names drift between
|
|
15
15
|
* register and generate).
|
|
16
16
|
*/
|
|
17
|
+
import { isParamSegment, isOpaqueIdSegment } from "./routeParsers.js";
|
|
17
18
|
/** Lowercase, collapse non-alphanumeric runs to '-', trim leading/trailing '-'. */
|
|
18
19
|
export function slugifyName(name) {
|
|
19
20
|
return (name ?? "")
|
|
@@ -22,9 +23,22 @@ export function slugifyName(name) {
|
|
|
22
23
|
.replace(/^-+|-+$/g, "");
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
* Normalize a path so param segments collapse:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Normalize a path so param/id segments collapse: `{brace}`, `:colon`,
|
|
27
|
+
* `[bracket]` params in any of routeParsers.ts's syntaxes, plus numeric,
|
|
28
|
+
* UUID, and long-hex id VALUES, all become `:p`. Delegates to
|
|
29
|
+
* routeParsers.ts's `isParamSegment`/`isOpaqueIdSegment` (SKYR-4214 task 5)
|
|
30
|
+
* rather than a second inline copy — this module previously had its own
|
|
31
|
+
* looser hex rule (`/^[0-9a-fA-F-]{16,}$/`, accepting `-`) and no
|
|
32
|
+
* bracket-syntax rule at all, so a Next.js `[id]` segment never collapsed
|
|
33
|
+
* and kept a match key from matching its `{id}`-spelled counterpart (the
|
|
34
|
+
* SKYR-4123 failure mode: a generated test's key stops matching its plan
|
|
35
|
+
* item). The characterization test in
|
|
36
|
+
* `pathSegmentClassification.characterization.test.ts` confirms every
|
|
37
|
+
* previously-agreeing input (UUID, numeric, 16+/24-char hex) still
|
|
38
|
+
* collapses to `:p` after this change — safe within one run because the
|
|
39
|
+
* register-time key and the generation-time query are computed by the same
|
|
40
|
+
* build; a plan file is never persisted across builds. Mirrors
|
|
41
|
+
* discriminators.ts's normalizePath (duplicated rather than imported —
|
|
28
42
|
* that function verifies discriminator claims against a diff, a different
|
|
29
43
|
* concern from plan matching, and keeping them independent avoids coupling
|
|
30
44
|
* this module's stability to phase 1's internal helper).
|
|
@@ -36,13 +50,12 @@ export function normalizeMatchPath(rawPath) {
|
|
|
36
50
|
.map((seg) => {
|
|
37
51
|
if (seg === "")
|
|
38
52
|
return seg;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (/^[0-9a-fA-F-]{16,}$/.test(seg))
|
|
53
|
+
// `isParamSegment` covers the well-formed colon family — `:id`,
|
|
54
|
+
// `:id(\d+)`, `:id?`, `:id*`, `:id+`. The `/^:/` test in front of it is
|
|
55
|
+
// deliberate breadth for the colon segments that regex rejects, such as
|
|
56
|
+
// `:1abc` or `:id-x`: a malformed param must still normalize to `:p`,
|
|
57
|
+
// or a registered `/users/:1abc` stops endpoint-matching `/users/42`.
|
|
58
|
+
if (/^:/.test(seg) || isParamSegment(seg) || isOpaqueIdSegment(seg))
|
|
46
59
|
return ":p";
|
|
47
60
|
return seg.toLowerCase();
|
|
48
61
|
})
|
|
@@ -74,6 +87,9 @@ export function buildApprovedPlanItem(candidate) {
|
|
|
74
87
|
category: candidate.scenario.category,
|
|
75
88
|
source: candidate.source,
|
|
76
89
|
...(candidate.verifiedDiscriminator ? { verifiedDiscriminator: candidate.verifiedDiscriminator } : {}),
|
|
90
|
+
...(candidate.scenario.subjectEndpoints !== undefined
|
|
91
|
+
? { subjectEndpoints: candidate.scenario.subjectEndpoints }
|
|
92
|
+
: {}),
|
|
77
93
|
matchKeys: computeMatchKeys(candidate.scenario),
|
|
78
94
|
};
|
|
79
95
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a plural resource name to its singular form for use in field names
|
|
3
|
+
* and step descriptions.
|
|
4
|
+
*
|
|
5
|
+
* Handles the most common irregular plurals found in REST API paths:
|
|
6
|
+
* -ies → -y (categories → category, companies → company)
|
|
7
|
+
* -ses → -s (statuses → status, classes → class)
|
|
8
|
+
* Falls back to removing the trailing "s" for regular plurals.
|
|
9
|
+
*/
|
|
10
|
+
export declare function singularize(word: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a plural resource name to its singular form for use in field names
|
|
3
|
+
* and step descriptions.
|
|
4
|
+
*
|
|
5
|
+
* Handles the most common irregular plurals found in REST API paths:
|
|
6
|
+
* -ies → -y (categories → category, companies → company)
|
|
7
|
+
* -ses → -s (statuses → status, classes → class)
|
|
8
|
+
* Falls back to removing the trailing "s" for regular plurals.
|
|
9
|
+
*/
|
|
10
|
+
export function singularize(word) {
|
|
11
|
+
if (word.endsWith("ies") && word.length > 3) {
|
|
12
|
+
return word.slice(0, -3) + "y";
|
|
13
|
+
}
|
|
14
|
+
if (word.endsWith("ses") && word.length > 4) {
|
|
15
|
+
return word.slice(0, -2); // statuses → status
|
|
16
|
+
}
|
|
17
|
+
return word.endsWith("s") && word.length > 1 ? word.slice(0, -1) : word;
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReuseMissedEntry } from "../types/ReuseOutcome.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parses the agent-written POM catalog (`.skyramp/skyramp-pom-catalog.md`, path owned
|
|
4
|
+
* by `pom-catalog.ts`) and joins it against the raw locators a delivered spec still
|
|
5
|
+
* carries inline.
|
|
6
|
+
*
|
|
7
|
+
* Why the catalog and not `selectScopedPoms`: that join is a whole-token substring
|
|
8
|
+
* test (`pomContent.includes(specToken)`), which is structurally blind to
|
|
9
|
+
* prefix-parameterized selectors — the exact thing a good page object does. Measured
|
|
10
|
+
* on openobserve run 31662701508 it misses the covering file entirely and returns two
|
|
11
|
+
* unrelated pages; widening it to prefix matching finds the right file but pulls in 32
|
|
12
|
+
* more. The catalog is already scoped and records the wildcard form, so the same match
|
|
13
|
+
* is precise there.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is informational — no caller gates on it — so the bias throughout is
|
|
16
|
+
* to under-report rather than assert. A malformed catalog yields no members, and a
|
|
17
|
+
* member with no recorded selector simply covers nothing.
|
|
18
|
+
*/
|
|
19
|
+
/** One catalogued property or method, with its selectors as wildcard patterns. */
|
|
20
|
+
export interface CatalogMember {
|
|
21
|
+
className: string;
|
|
22
|
+
/** As the catalog names it: `propertyName` or `methodName(a, b)`. */
|
|
23
|
+
member: string;
|
|
24
|
+
kind: "property" | "method";
|
|
25
|
+
/** Basename of the class's `Import:` path — extension-less, see ReuseMissedEntry. */
|
|
26
|
+
pageObject: string;
|
|
27
|
+
/** Wildcard patterns; `*` matches any run of characters. */
|
|
28
|
+
selectors: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Every property and method in the catalog. Returns `[]` for anything that is not a
|
|
32
|
+
* catalog — this runs on a file the agent wrote, so unparseable input is an expected
|
|
33
|
+
* state, not an error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parsePomCatalog(markdown: string): CatalogMember[];
|
|
36
|
+
/**
|
|
37
|
+
* Raw locators the delivered spec still carries, and which of them a catalogued member
|
|
38
|
+
* covers.
|
|
39
|
+
*
|
|
40
|
+
* The spec side is `extractRawLocatorTokens`, a sibling of the POM-detection
|
|
41
|
+
* `extractSelectors` that excludes `getByRole`'s role token and the bare `name:`
|
|
42
|
+
* field — those are detection evidence, not locators, and `name:` in particular can
|
|
43
|
+
* capture a POM method's own data argument (e.g. `fp.createFunction({ name: "myFunction" })`).
|
|
44
|
+
* Both numbers come from one pass so the denominator and the misses cannot disagree.
|
|
45
|
+
*
|
|
46
|
+
* First match wins per locator. A locator covered by two members is still one missed
|
|
47
|
+
* reuse, and naming one of them is enough to send a reviewer to the right file.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findMissedReuse(specContent: string, members: CatalogMember[]): {
|
|
50
|
+
rawLocatorsInline: number;
|
|
51
|
+
missed: ReuseMissedEntry[];
|
|
52
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { extractRawLocatorTokens } from "./pom-scope/selector-extractor.js";
|
|
3
|
+
import { escapeRegExp } from "./regex.js";
|
|
4
|
+
const CLASS_HEADING_RE = /^##\s+(.+?)\s*$/;
|
|
5
|
+
const IMPORT_RE = /^-\s+\*\*Import:\*\*\s+`([^`]+)`/;
|
|
6
|
+
const PROPERTIES_RE = /^-\s+\*\*Properties:\*\*/;
|
|
7
|
+
const METHODS_RE = /^-\s+\*\*Methods:\*\*/;
|
|
8
|
+
const OTHER_FIELD_RE = /^-\s+\*\*/;
|
|
9
|
+
/** A member bullet: indented, backticked name, dash separator, then the description. */
|
|
10
|
+
const MEMBER_RE = /^\s+-\s+`([^`]+)`\s*[—–-]\s*(.*)$/;
|
|
11
|
+
const BACKTICKED_RE = /`([^`]+)`/g;
|
|
12
|
+
/** `[data-test="value"]`, including prefix/suffix matchers — the value is the token. */
|
|
13
|
+
const ATTR_VALUE_RE = /\[[A-Za-z-]+[$^*~|]?="([^"]+)"\]/g;
|
|
14
|
+
/** A bare token that looks like a selector value rather than prose. */
|
|
15
|
+
const BARE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*\*?$/;
|
|
16
|
+
/** Below this, a bare token is too generic to claim coverage of anything. Attribute
|
|
17
|
+
* values are exempt: `[data-test="x"]` is unambiguous evidence regardless of length. */
|
|
18
|
+
const MIN_BARE_TOKEN_LENGTH = 6;
|
|
19
|
+
/** `${...}` and `*` both mean "any run of characters" — normalize to one form. */
|
|
20
|
+
function normalizeWildcards(token) {
|
|
21
|
+
return token.replace(/\$\{[^}]*\}/g, "*");
|
|
22
|
+
}
|
|
23
|
+
/** Selector patterns in a member's DESCRIPTION — the text after the dash, never the
|
|
24
|
+
* member name itself. Scanning the whole bullet would make a property register its own
|
|
25
|
+
* name as a selector (`addFunctionButton` is a valid bare token), so a spec locator that
|
|
26
|
+
* happened to equal a property name would read as covered by that property, which is
|
|
27
|
+
* circular. */
|
|
28
|
+
function selectorsFrom(description) {
|
|
29
|
+
const out = new Set();
|
|
30
|
+
for (const [, backticked] of description.matchAll(BACKTICKED_RE)) {
|
|
31
|
+
const token = normalizeWildcards(backticked);
|
|
32
|
+
let sawAttr = false;
|
|
33
|
+
for (const [, value] of token.matchAll(ATTR_VALUE_RE)) {
|
|
34
|
+
out.add(value);
|
|
35
|
+
sawAttr = true;
|
|
36
|
+
}
|
|
37
|
+
// A bare token is only a selector if the whole backticked run is one — otherwise
|
|
38
|
+
// it is prose, or an attribute selector already harvested above.
|
|
39
|
+
if (!sawAttr && BARE_TOKEN_RE.test(token) && token.length >= MIN_BARE_TOKEN_LENGTH) {
|
|
40
|
+
out.add(token);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...out];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every property and method in the catalog. Returns `[]` for anything that is not a
|
|
47
|
+
* catalog — this runs on a file the agent wrote, so unparseable input is an expected
|
|
48
|
+
* state, not an error.
|
|
49
|
+
*/
|
|
50
|
+
export function parsePomCatalog(markdown) {
|
|
51
|
+
const members = [];
|
|
52
|
+
let className;
|
|
53
|
+
let pageObject = "";
|
|
54
|
+
let section;
|
|
55
|
+
for (const line of markdown.split("\n")) {
|
|
56
|
+
const heading = CLASS_HEADING_RE.exec(line);
|
|
57
|
+
if (heading) {
|
|
58
|
+
// Free-function module headings carry a trailing gloss — `bookingHelpers
|
|
59
|
+
// (module functions, not a class)` (SKYR-4157 finding 4) — strip it so
|
|
60
|
+
// `className` is the bare module/class name, not prose.
|
|
61
|
+
className = heading[1].replace(/\s*\([^)]*\)\s*$/, "");
|
|
62
|
+
pageObject = "";
|
|
63
|
+
section = undefined;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!className)
|
|
67
|
+
continue;
|
|
68
|
+
const imported = IMPORT_RE.exec(line);
|
|
69
|
+
if (imported) {
|
|
70
|
+
// Extension-less by construction: the catalog records an import specifier, and
|
|
71
|
+
// `path.basename` of `../../pages/functionsPages/functionsPage` is what a reader
|
|
72
|
+
// needs to find the file.
|
|
73
|
+
pageObject = path.basename(imported[1]).replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (PROPERTIES_RE.test(line)) {
|
|
77
|
+
section = "property";
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (METHODS_RE.test(line)) {
|
|
81
|
+
section = "method";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Any other `- **Field:**` line (Iframe, depth markers) closes the current section.
|
|
85
|
+
if (OTHER_FIELD_RE.test(line)) {
|
|
86
|
+
section = undefined;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!section)
|
|
90
|
+
continue;
|
|
91
|
+
const member = MEMBER_RE.exec(line);
|
|
92
|
+
if (!member)
|
|
93
|
+
continue;
|
|
94
|
+
members.push({
|
|
95
|
+
className,
|
|
96
|
+
member: member[1],
|
|
97
|
+
kind: section,
|
|
98
|
+
pageObject,
|
|
99
|
+
// Description only — see selectorsFrom on why the member name is excluded.
|
|
100
|
+
selectors: selectorsFrom(member[2]),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return members;
|
|
104
|
+
}
|
|
105
|
+
/** Anchored matcher for one wildcard pattern. */
|
|
106
|
+
function toMatcher(pattern) {
|
|
107
|
+
return new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
|
|
108
|
+
}
|
|
109
|
+
/** The member name without its parameter list, for the `Class.member` label. */
|
|
110
|
+
function bareMemberName(member) {
|
|
111
|
+
return member.replace(/\(.*$/, "");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Raw locators the delivered spec still carries, and which of them a catalogued member
|
|
115
|
+
* covers.
|
|
116
|
+
*
|
|
117
|
+
* The spec side is `extractRawLocatorTokens`, a sibling of the POM-detection
|
|
118
|
+
* `extractSelectors` that excludes `getByRole`'s role token and the bare `name:`
|
|
119
|
+
* field — those are detection evidence, not locators, and `name:` in particular can
|
|
120
|
+
* capture a POM method's own data argument (e.g. `fp.createFunction({ name: "myFunction" })`).
|
|
121
|
+
* Both numbers come from one pass so the denominator and the misses cannot disagree.
|
|
122
|
+
*
|
|
123
|
+
* First match wins per locator. A locator covered by two members is still one missed
|
|
124
|
+
* reuse, and naming one of them is enough to send a reviewer to the right file.
|
|
125
|
+
*/
|
|
126
|
+
export function findMissedReuse(specContent, members) {
|
|
127
|
+
const locators = extractRawLocatorTokens(specContent);
|
|
128
|
+
const compiled = members.map((m) => ({ m, matchers: m.selectors.map(toMatcher) }));
|
|
129
|
+
const missed = [];
|
|
130
|
+
for (const locator of locators) {
|
|
131
|
+
const hit = compiled.find(({ matchers }) => matchers.some((re) => re.test(locator)));
|
|
132
|
+
if (!hit)
|
|
133
|
+
continue;
|
|
134
|
+
missed.push({
|
|
135
|
+
locator,
|
|
136
|
+
pageObject: hit.m.pageObject,
|
|
137
|
+
member: `${hit.m.className}.${bareMemberName(hit.m.member)}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { rawLocatorsInline: locators.length, missed };
|
|
141
|
+
}
|
|
@@ -4,4 +4,16 @@
|
|
|
4
4
|
* these strings to find which page objects cover the test's actions.
|
|
5
5
|
*/
|
|
6
6
|
export declare const DEFAULT_TESTID_ATTRS: string[];
|
|
7
|
+
/** POM-detection token set (SKYR-4157 finding 1: also used for scoring, unchanged
|
|
8
|
+
* behaviour) — includes role names and bare `name:` values as evidence, even
|
|
9
|
+
* though those are not themselves raw locators. Used by the POM scoping path
|
|
10
|
+
* (`pom-scope/index.ts`); do not repurpose for coverage counting. */
|
|
7
11
|
export declare function extractSelectors(specContent: string, extraTestIdAttrs?: string[]): string[];
|
|
12
|
+
/** Genuine raw LOCATOR tokens only: test-id attribute values, `getByTestId`,
|
|
13
|
+
* `getByText`/`getByLabel`/`getByPlaceholder`, and raw `.locator()`/
|
|
14
|
+
* `.frameLocator()` strings. Excludes `getByRole`'s role token and the bare
|
|
15
|
+
* `name:` field — neither is a locator, and `name:` in particular can capture a
|
|
16
|
+
* POM method's own data argument (SKYR-4157 finding 1). Used for coverage
|
|
17
|
+
* counting (`pom-catalog-parse.ts`), where the denominator must be actual
|
|
18
|
+
* locators still inline, not detection evidence. */
|
|
19
|
+
export declare function extractRawLocatorTokens(specContent: string, extraTestIdAttrs?: string[]): string[];
|
|
@@ -16,12 +16,19 @@ const attrPattern = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^
|
|
|
16
16
|
// string is actually a family-attr selector, vs. one that merely CONTAINS the
|
|
17
17
|
// substring (e.g. ".data-testing-notes" contains "data-test" but isn't one).
|
|
18
18
|
const attrAnchor = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^*~|]?=`);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// getByTestId("X"), getByText/Label/Placeholder("T") — these capture genuine raw
|
|
20
|
+
// locator values, shared by both extractSelectors and extractRawLocatorTokens.
|
|
21
|
+
const GET_BY_TEST_ID = /getByTestId\(\s*["']([^"']+)["']/g;
|
|
22
|
+
const GET_BY_TEXT_LABEL_PLACEHOLDER = /getBy(?:Text|Label|Placeholder)\(\s*["']([^"']+)["']/g;
|
|
23
|
+
const LOCATOR_PATTERNS = [GET_BY_TEST_ID, GET_BY_TEXT_LABEL_PLACEHOLDER];
|
|
24
|
+
// getByRole("role", ...) and bare `name: "..."` object keys are useful evidence for
|
|
25
|
+
// POM *detection* (scoring which page objects a test's actions touch), but they are
|
|
26
|
+
// NOT raw locator values: a role name isn't a selector, and `name:` matches ANY bare
|
|
27
|
+
// object key so named — including a POM method's own data argument (SKYR-4157
|
|
28
|
+
// finding 1). Only extractSelectors (the detection path) includes these.
|
|
29
|
+
const GET_BY_ROLE = /getByRole\(\s*["']([^"']+)["']/g;
|
|
30
|
+
const BARE_NAME_FIELD = /name:\s*["']([^"']+)["']/g;
|
|
31
|
+
const OTHER_PATTERNS = [GET_BY_TEST_ID, GET_BY_ROLE, BARE_NAME_FIELD, GET_BY_TEXT_LABEL_PLACEHOLDER];
|
|
25
32
|
// Raw .locator("...")/.frameLocator("...") strings that are NOT test-id-attribute based
|
|
26
33
|
// (e.g. ".step-deduplication", "iframe#asset-list").
|
|
27
34
|
// Quote-type-aware alternates: a double-quoted arg may contain unescaped single quotes
|
|
@@ -35,11 +42,13 @@ function normalize(token) {
|
|
|
35
42
|
function isUseful(token) {
|
|
36
43
|
return token.length >= 3 && !/^\d+$/.test(token);
|
|
37
44
|
}
|
|
38
|
-
|
|
45
|
+
/** Shared core: test-id-attribute values, the given value-bearing patterns, and raw
|
|
46
|
+
* `.locator()`/`.frameLocator()` strings not already covered by an attribute match. */
|
|
47
|
+
function collectTokens(specContent, patterns, extraTestIdAttrs) {
|
|
39
48
|
const testIdAttrs = [...DEFAULT_TESTID_ATTRS, ...extraTestIdAttrs];
|
|
40
49
|
const anchor = attrAnchor(testIdAttrs);
|
|
41
50
|
const tokens = new Set();
|
|
42
|
-
for (const re of [attrPattern(testIdAttrs), ...
|
|
51
|
+
for (const re of [attrPattern(testIdAttrs), ...patterns]) {
|
|
43
52
|
for (const m of specContent.matchAll(re)) {
|
|
44
53
|
const t = normalize(m[1]);
|
|
45
54
|
if (isUseful(t))
|
|
@@ -55,3 +64,20 @@ export function extractSelectors(specContent, extraTestIdAttrs = []) {
|
|
|
55
64
|
}
|
|
56
65
|
return [...tokens];
|
|
57
66
|
}
|
|
67
|
+
/** POM-detection token set (SKYR-4157 finding 1: also used for scoring, unchanged
|
|
68
|
+
* behaviour) — includes role names and bare `name:` values as evidence, even
|
|
69
|
+
* though those are not themselves raw locators. Used by the POM scoping path
|
|
70
|
+
* (`pom-scope/index.ts`); do not repurpose for coverage counting. */
|
|
71
|
+
export function extractSelectors(specContent, extraTestIdAttrs = []) {
|
|
72
|
+
return collectTokens(specContent, OTHER_PATTERNS, extraTestIdAttrs);
|
|
73
|
+
}
|
|
74
|
+
/** Genuine raw LOCATOR tokens only: test-id attribute values, `getByTestId`,
|
|
75
|
+
* `getByText`/`getByLabel`/`getByPlaceholder`, and raw `.locator()`/
|
|
76
|
+
* `.frameLocator()` strings. Excludes `getByRole`'s role token and the bare
|
|
77
|
+
* `name:` field — neither is a locator, and `name:` in particular can capture a
|
|
78
|
+
* POM method's own data argument (SKYR-4157 finding 1). Used for coverage
|
|
79
|
+
* counting (`pom-catalog-parse.ts`), where the denominator must be actual
|
|
80
|
+
* locators still inline, not detection evidence. */
|
|
81
|
+
export function extractRawLocatorTokens(specContent, extraTestIdAttrs = []) {
|
|
82
|
+
return collectTokens(specContent, LOCATOR_PATTERNS, extraTestIdAttrs);
|
|
83
|
+
}
|
|
@@ -7,13 +7,14 @@ export type ReuseViolation = {
|
|
|
7
7
|
export type VerifyResult = {
|
|
8
8
|
ok: boolean;
|
|
9
9
|
violations: ReuseViolation[];
|
|
10
|
-
|
|
10
|
+
unverifiableCalls: string[];
|
|
11
|
+
unrecognizedBindings: string[];
|
|
11
12
|
parseError?: string;
|
|
12
13
|
checkedCalls: number;
|
|
13
|
-
/** Every POM call found in the spec, verifiable or not.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* POM members the delivered spec actually reuses. */
|
|
14
|
+
/** Every POM call found in the spec, verifiable or not. Equals
|
|
15
|
+
* `checkedCalls + unverifiableCalls.length`: `unrecognizedBindings` is kept
|
|
16
|
+
* separate precisely because those are not calls and would overcount. This is
|
|
17
|
+
* the count of POM members the delivered spec actually reuses. */
|
|
17
18
|
reusedCalls: number;
|
|
18
19
|
};
|
|
19
20
|
export declare function verifyReuse(testFile: string, language: string): Promise<VerifyResult>;
|
|
@@ -126,18 +126,19 @@ export async function verifyReuse(testFile, language) {
|
|
|
126
126
|
const calls = extractPomCalls(spec, bindings);
|
|
127
127
|
const bindingsByName = new Map(bindings.map((b) => [b.name, b]));
|
|
128
128
|
const violations = [];
|
|
129
|
-
const
|
|
129
|
+
const unverifiableCalls = [];
|
|
130
|
+
const unrecognizedBindings = [];
|
|
130
131
|
let checkedCalls = 0;
|
|
131
132
|
for (const call of calls) {
|
|
132
133
|
const binding = bindingsByName.get(call.binding);
|
|
133
134
|
if (!binding) {
|
|
134
135
|
// Shouldn't happen -- extractPomCalls only matches against the bindings it was given --
|
|
135
136
|
// but degrade to unverifiable rather than throw if the invariant ever breaks.
|
|
136
|
-
|
|
137
|
+
unverifiableCalls.push([call.binding, ...call.chain].join("."));
|
|
137
138
|
continue;
|
|
138
139
|
}
|
|
139
140
|
if (!call.strict) {
|
|
140
|
-
|
|
141
|
+
unverifiableCalls.push([call.binding, ...call.chain].join("."));
|
|
141
142
|
continue;
|
|
142
143
|
}
|
|
143
144
|
checkedCalls++;
|
|
@@ -156,7 +157,7 @@ export async function verifyReuse(testFile, language) {
|
|
|
156
157
|
// remap/demote loop the agent cannot win (no candidates to remap to → forced demote
|
|
157
158
|
// of possibly-correct reuse). Degrade to unverifiable — the fail-open contract at
|
|
158
159
|
// the reachability level, matching resolve.ts's permissive stance on shapes.
|
|
159
|
-
|
|
160
|
+
unverifiableCalls.push([call.binding, ...call.chain].join("."));
|
|
160
161
|
checkedCalls--;
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
@@ -164,7 +165,7 @@ export async function verifyReuse(testFile, language) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
for (const u of unrecognized)
|
|
167
|
-
|
|
168
|
+
unrecognizedBindings.push(u);
|
|
168
169
|
let parseError;
|
|
169
170
|
if (language === "javascript") {
|
|
170
171
|
try {
|
|
@@ -179,7 +180,8 @@ export async function verifyReuse(testFile, language) {
|
|
|
179
180
|
return {
|
|
180
181
|
ok: violations.length === 0 && parseError === undefined,
|
|
181
182
|
violations,
|
|
182
|
-
|
|
183
|
+
unverifiableCalls,
|
|
184
|
+
unrecognizedBindings,
|
|
183
185
|
parseError,
|
|
184
186
|
checkedCalls,
|
|
185
187
|
reusedCalls: calls.length,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
|
|
3
|
+
*
|
|
4
|
+
* The report language (SKYR-4023) reaches the MCP server only as a testbot
|
|
5
|
+
* prompt/resource arg, and the instruction to write free-text report fields in
|
|
6
|
+
* that language is prose the agent sometimes ignores — the customer-visible
|
|
7
|
+
* failure this module exists to close. The prompt render captures the language
|
|
8
|
+
* here (same server process serves the prompt and later handles the report
|
|
9
|
+
* tool), and the report tool rejects submissions whose free-text fields leaked
|
|
10
|
+
* back into English.
|
|
11
|
+
*
|
|
12
|
+
* Capture is fail-open by design: if the testbot prompt was never served
|
|
13
|
+
* (local/IDE flows, or a run broken enough that the agent has no instructions),
|
|
14
|
+
* the language stays unset and no report is ever falsely rejected.
|
|
15
|
+
*/
|
|
16
|
+
/** Capture the report language at prompt-serve time. Last render wins:
|
|
17
|
+
* `undefined` clears any earlier capture, so a long-lived server (IDE use)
|
|
18
|
+
* that renders a ja prompt and later an English/argless one disarms the
|
|
19
|
+
* guardrail rather than falsely rejecting a legitimately-English report. */
|
|
20
|
+
export declare function setReportLanguage(language: string | undefined): void;
|
|
21
|
+
export declare function getReportLanguage(): string | undefined;
|
|
22
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
23
|
+
export declare function resetReportLanguage(): void;
|
|
24
|
+
export declare function isEnforcedReportLanguage(language: string | undefined): language is string;
|
|
25
|
+
/** English display name for a language code, falling back to the raw code when
|
|
26
|
+
* Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
|
|
27
|
+
* ultimately caller-supplied, and a name lookup must never throw. */
|
|
28
|
+
export declare function reportLanguageDisplayName(language: string): string;
|
|
29
|
+
export interface ReportTextField {
|
|
30
|
+
/** Where the text came from, in the tool's input terms (e.g. "testResults[2].details"). */
|
|
31
|
+
path: string;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
/** Field paths whose text leaked English under an enforced report language.
|
|
35
|
+
* Empty for non-enforced languages — unknown languages are skipped, never guessed. */
|
|
36
|
+
export declare function findLanguageViolations(fields: ReportTextField[], language: string): string[];
|
|
37
|
+
/** Field paths in the deliberately-lenient band: some English prose, but under
|
|
38
|
+
* the rejection threshold (i.e. exactly one stray word at the current
|
|
39
|
+
* MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
|
|
40
|
+
* technical word ("flaky", "timeout") never blocks a report. Counted into the
|
|
41
|
+
* submit-report analytics event so the residual leak rate is measurable
|
|
42
|
+
* before anyone tightens the threshold. */
|
|
43
|
+
export declare function findLanguageNearMisses(fields: ReportTextField[], language: string): string[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
|
|
3
|
+
*
|
|
4
|
+
* The report language (SKYR-4023) reaches the MCP server only as a testbot
|
|
5
|
+
* prompt/resource arg, and the instruction to write free-text report fields in
|
|
6
|
+
* that language is prose the agent sometimes ignores — the customer-visible
|
|
7
|
+
* failure this module exists to close. The prompt render captures the language
|
|
8
|
+
* here (same server process serves the prompt and later handles the report
|
|
9
|
+
* tool), and the report tool rejects submissions whose free-text fields leaked
|
|
10
|
+
* back into English.
|
|
11
|
+
*
|
|
12
|
+
* Capture is fail-open by design: if the testbot prompt was never served
|
|
13
|
+
* (local/IDE flows, or a run broken enough that the agent has no instructions),
|
|
14
|
+
* the language stays unset and no report is ever falsely rejected.
|
|
15
|
+
*/
|
|
16
|
+
let sessionReportLanguage;
|
|
17
|
+
/** Capture the report language at prompt-serve time. Last render wins:
|
|
18
|
+
* `undefined` clears any earlier capture, so a long-lived server (IDE use)
|
|
19
|
+
* that renders a ja prompt and later an English/argless one disarms the
|
|
20
|
+
* guardrail rather than falsely rejecting a legitimately-English report. */
|
|
21
|
+
export function setReportLanguage(language) {
|
|
22
|
+
sessionReportLanguage = language;
|
|
23
|
+
}
|
|
24
|
+
export function getReportLanguage() {
|
|
25
|
+
return sessionReportLanguage;
|
|
26
|
+
}
|
|
27
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
28
|
+
export function resetReportLanguage() {
|
|
29
|
+
sessionReportLanguage = undefined;
|
|
30
|
+
}
|
|
31
|
+
// Languages the guardrail can enforce: detection is script-presence-based, so a
|
|
32
|
+
// language is enforceable only when its expected script is disjoint from Latin.
|
|
33
|
+
// English ('en') is deliberately absent — it is the default, not enforced.
|
|
34
|
+
// A Map, not a plain object: the key is caller-supplied, and object indexing
|
|
35
|
+
// would resolve prototype keys ("toString") to inherited members whose later
|
|
36
|
+
// .test() call crashes the tool.
|
|
37
|
+
const TARGET_SCRIPTS = new Map([
|
|
38
|
+
// Hiragana, Katakana (incl. halfwidth), CJK ideographs (incl. ext A / compat).
|
|
39
|
+
["ja", /[-ヿ㐀-䶿一-鿿豈-ヲ-゚]/],
|
|
40
|
+
]);
|
|
41
|
+
export function isEnforcedReportLanguage(language) {
|
|
42
|
+
return language !== undefined && TARGET_SCRIPTS.has(language);
|
|
43
|
+
}
|
|
44
|
+
/** English display name for a language code, falling back to the raw code when
|
|
45
|
+
* Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
|
|
46
|
+
* ultimately caller-supplied, and a name lookup must never throw. */
|
|
47
|
+
export function reportLanguageDisplayName(language) {
|
|
48
|
+
try {
|
|
49
|
+
return new Intl.DisplayNames(["en"], { type: "language" }).of(language) ?? language;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return language;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Tokens that are legitimate untranslated content in any report language — the
|
|
56
|
+
// prompt's own "do NOT translate" list: enum values, HTTP verbs, test types.
|
|
57
|
+
const UNTRANSLATED_WORDS = new Set([
|
|
58
|
+
// Exact enum values only. NOT "passed"/"failed" — those are ordinary English
|
|
59
|
+
// prose and are exactly what leaks in pytest summaries ("1 passed in 0.88s",
|
|
60
|
+
// "failed: count was 13"; see letsramp/api-insight#215).
|
|
61
|
+
"pass", "fail", "skipped", "error", "unknown",
|
|
62
|
+
"critical", "high", "medium", "low",
|
|
63
|
+
"get", "post", "put", "patch", "delete", "head", "options",
|
|
64
|
+
"ui", "e2e", "api", "contract", "integration", "smoke", "fuzz", "load", "mock",
|
|
65
|
+
"http", "https", "json", "yaml", "ok",
|
|
66
|
+
]);
|
|
67
|
+
/** A field must contain at least this many plain English prose words (after the
|
|
68
|
+
* stripping below) to be flagged — so identifier-and-timing-only details like
|
|
69
|
+
* "10.8s, products_contract_test.py" stay legal, and one stray word ("flaky")
|
|
70
|
+
* never rejects a report. Two is deliberate: the shortest real leak observed
|
|
71
|
+
* ("1 passed in 0.88s", api-insight#215) has exactly two prose words. */
|
|
72
|
+
const MIN_PROSE_WORDS = 2;
|
|
73
|
+
/** Count the plain English prose words in `text`, or return 0 when the target
|
|
74
|
+
* language's script is present (the field is compliant regardless of any
|
|
75
|
+
* English around it). Backticked spans, file names/paths, identifiers
|
|
76
|
+
* (snake_case/camelCase/ALL-CAPS), digit-bearing tokens, and the prompt's
|
|
77
|
+
* untranslated vocabulary are all excluded from the count. */
|
|
78
|
+
function countProseWords(text, script) {
|
|
79
|
+
if (script.test(text))
|
|
80
|
+
return 0;
|
|
81
|
+
const withoutCode = text.replace(/`[^`]*`/g, " ");
|
|
82
|
+
let proseWords = 0;
|
|
83
|
+
for (const raw of withoutCode.split(/\s+/)) {
|
|
84
|
+
const token = raw.replace(/^[^A-Za-z0-9]+/, "").replace(/[^A-Za-z0-9]+$/, "");
|
|
85
|
+
// Pure alphabetic words only: anything with digits or interior punctuation
|
|
86
|
+
// (paths, file names, snake_case, versions, "10.8s") is not prose.
|
|
87
|
+
if (token.length < 2 || !/^[A-Za-z]+$/.test(token))
|
|
88
|
+
continue;
|
|
89
|
+
if (/[a-z][A-Z]/.test(token))
|
|
90
|
+
continue; // camelCase identifier
|
|
91
|
+
if (token === token.toUpperCase())
|
|
92
|
+
continue; // acronym / enum shout (GET, FAILED)
|
|
93
|
+
if (UNTRANSLATED_WORDS.has(token.toLowerCase()))
|
|
94
|
+
continue;
|
|
95
|
+
proseWords++;
|
|
96
|
+
}
|
|
97
|
+
return proseWords;
|
|
98
|
+
}
|
|
99
|
+
/** Field paths whose text leaked English under an enforced report language.
|
|
100
|
+
* Empty for non-enforced languages — unknown languages are skipped, never guessed. */
|
|
101
|
+
export function findLanguageViolations(fields, language) {
|
|
102
|
+
const script = TARGET_SCRIPTS.get(language);
|
|
103
|
+
if (!script)
|
|
104
|
+
return [];
|
|
105
|
+
return fields
|
|
106
|
+
.filter((f) => countProseWords(f.text, script) >= MIN_PROSE_WORDS)
|
|
107
|
+
.map((f) => f.path);
|
|
108
|
+
}
|
|
109
|
+
/** Field paths in the deliberately-lenient band: some English prose, but under
|
|
110
|
+
* the rejection threshold (i.e. exactly one stray word at the current
|
|
111
|
+
* MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
|
|
112
|
+
* technical word ("flaky", "timeout") never blocks a report. Counted into the
|
|
113
|
+
* submit-report analytics event so the residual leak rate is measurable
|
|
114
|
+
* before anyone tightens the threshold. */
|
|
115
|
+
export function findLanguageNearMisses(fields, language) {
|
|
116
|
+
const script = TARGET_SCRIPTS.get(language);
|
|
117
|
+
if (!script)
|
|
118
|
+
return [];
|
|
119
|
+
return fields
|
|
120
|
+
.filter((f) => {
|
|
121
|
+
const words = countProseWords(f.text, script);
|
|
122
|
+
return words > 0 && words < MIN_PROSE_WORDS;
|
|
123
|
+
})
|
|
124
|
+
.map((f) => f.path);
|
|
125
|
+
}
|