@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.
Files changed (112) hide show
  1. package/build/prompts/code-reuse.js +17 -2
  2. package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
  3. package/build/prompts/modularization/ui-test-modularization.js +9 -6
  4. package/build/prompts/pom-aware-code-reuse.js +1 -1
  5. package/build/prompts/shared-helper-policy.js +5 -5
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
  7. package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
  8. package/build/prompts/test-recommendation/recommendationSections.js +3 -3
  9. package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
  10. package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
  12. package/build/prompts/testbot/testbot-prompts.js +25 -8
  13. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
  14. package/build/recommendation/budgeters/shared.d.ts +21 -8
  15. package/build/recommendation/budgeters/shared.js +134 -56
  16. package/build/recommendation/planRanker.d.ts +5 -3
  17. package/build/recommendation/planRanker.js +1 -1
  18. package/build/services/TestGenerationService.js +15 -1
  19. package/build/tools/code-refactor/assertion-state.d.ts +91 -0
  20. package/build/tools/code-refactor/assertion-state.js +375 -0
  21. package/build/tools/code-refactor/codeReuseTool.js +6 -4
  22. package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
  23. package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
  24. package/build/tools/code-refactor/retrofit-state.js +162 -0
  25. package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
  26. package/build/tools/code-refactor/reuse-state.d.ts +9 -0
  27. package/build/tools/code-refactor/reuse-state.js +42 -4
  28. package/build/tools/code-refactor/utils-verify-gates.js +69 -15
  29. package/build/tools/executeSkyrampTestTool.js +19 -14
  30. package/build/tools/submitReportTool.js +87 -9
  31. package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
  32. package/build/tools/test-management/registerTestPlanTool.js +29 -8
  33. package/build/types/AssertionOutcome.d.ts +68 -0
  34. package/build/types/AssertionOutcome.js +1 -0
  35. package/build/types/RepositoryAnalysis.d.ts +10 -10
  36. package/build/types/ReuseOutcome.d.ts +16 -0
  37. package/build/types/TestRecommendation.d.ts +11 -1
  38. package/build/types/TestRecommendation.js +34 -0
  39. package/build/types/TestTypes.d.ts +4 -0
  40. package/build/types/TestTypes.js +8 -0
  41. package/build/types/TestbotReport.d.ts +13 -0
  42. package/build/types/index.d.ts +1 -1
  43. package/build/utils/AnalysisStateManager.d.ts +20 -7
  44. package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
  45. package/build/utils/assertion-verify/api-shared-lints.js +315 -0
  46. package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
  47. package/build/utils/assertion-verify/contract-lints.js +87 -0
  48. package/build/utils/assertion-verify/format.d.ts +5 -0
  49. package/build/utils/assertion-verify/format.js +65 -0
  50. package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
  51. package/build/utils/assertion-verify/helper-imports.js +178 -0
  52. package/build/utils/assertion-verify/index.d.ts +3 -0
  53. package/build/utils/assertion-verify/index.js +7 -0
  54. package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
  55. package/build/utils/assertion-verify/integration-lints.js +36 -0
  56. package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
  57. package/build/utils/assertion-verify/js-regex-blank.js +153 -0
  58. package/build/utils/assertion-verify/lint-types.d.ts +33 -0
  59. package/build/utils/assertion-verify/lint-types.js +57 -0
  60. package/build/utils/assertion-verify/marker.d.ts +27 -0
  61. package/build/utils/assertion-verify/marker.js +61 -0
  62. package/build/utils/assertion-verify/metrics.d.ts +30 -0
  63. package/build/utils/assertion-verify/metrics.js +341 -0
  64. package/build/utils/assertion-verify/python-strip.d.ts +6 -0
  65. package/build/utils/assertion-verify/python-strip.js +75 -0
  66. package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
  67. package/build/utils/assertion-verify/strip-dispatch.js +42 -0
  68. package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
  69. package/build/utils/assertion-verify/ui-lints.js +244 -0
  70. package/build/utils/assertion-verify/verify.d.ts +61 -0
  71. package/build/utils/assertion-verify/verify.js +215 -0
  72. package/build/utils/featureFlags.d.ts +12 -2
  73. package/build/utils/featureFlags.js +33 -3
  74. package/build/utils/pathSignatures.d.ts +4 -1
  75. package/build/utils/pathSignatures.js +14 -2
  76. package/build/utils/planOnlyMode.d.ts +33 -0
  77. package/build/utils/planOnlyMode.js +40 -0
  78. package/build/utils/reportVerification.d.ts +9 -0
  79. package/build/utils/reportVerification.js +39 -6
  80. package/build/utils/scenarioDrafting.d.ts +1 -1
  81. package/build/utils/scenarioDrafting.js +1 -1
  82. package/build/utils/utils-verify/allow.d.ts +22 -4
  83. package/build/utils/utils-verify/allow.js +8 -2
  84. package/build/utils/utils-verify/call-sites.d.ts +40 -1
  85. package/build/utils/utils-verify/call-sites.js +196 -30
  86. package/build/utils/utils-verify/importers.d.ts +31 -0
  87. package/build/utils/utils-verify/importers.js +78 -0
  88. package/build/utils/utils-verify/index.d.ts +1 -0
  89. package/build/utils/utils-verify/index.js +1 -0
  90. package/build/utils/utils-verify/language-spec.d.ts +13 -2
  91. package/build/utils/utils-verify/language-spec.js +12 -2
  92. package/build/utils/utils-verify/parse.d.ts +31 -3
  93. package/build/utils/utils-verify/parse.js +190 -9
  94. package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
  95. package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
  96. package/build/utils/utils-verify/stage.d.ts +6 -0
  97. package/build/utils/utils-verify/stage.js +12 -2
  98. package/build/utils/utils-verify/verify.d.ts +54 -4
  99. package/build/utils/utils-verify/verify.js +224 -12
  100. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
  102. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
  103. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
  104. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  105. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
  106. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  107. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  109. package/node_modules/playwright/package.json +1 -1
  110. package/package.json +2 -2
  111. package/build/tools/code-refactor/enhance-state.d.ts +0 -49
  112. package/build/tools/code-refactor/enhance-state.js +0 -109
@@ -0,0 +1,244 @@
1
+ import { escapeRegExp } from "../regex.js";
2
+ import { strippedSources } from "./strip-dispatch.js";
3
+ import { balancedCloseIndex, identifierRe, inScope, interpolationRe, lineOfOffset, } from "./lint-types.js";
4
+ /**
5
+ * Deterministic UI (Playwright) lints — the machine-checkable slice of the D5
6
+ * rubric's UI dims:
7
+ * critical_assertions → expect import source, pageerror listener asserted,
8
+ * dead response captures, repeated elements w/o count
9
+ * computed_values → tautologies here; exact-vs-visibility is enforced by
10
+ * the strength score + weak-additions gate in verify.ts
11
+ * post_edit → unasserted trailing state-changing action
12
+ */
13
+ const PLAYWRIGHT_EXPECT_IMPORT_RES = [
14
+ /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]/g,
15
+ /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"]@playwright\/test['"]\s*\)/g,
16
+ ];
17
+ /** `expect` imported from @playwright/test — banned flatly by the UI rules. */
18
+ function lintPlaywrightExpectImport(commentless) {
19
+ for (const re of PLAYWRIGHT_EXPECT_IMPORT_RES) {
20
+ re.lastIndex = 0;
21
+ let m;
22
+ while ((m = re.exec(commentless)) !== null) {
23
+ if (/\bexpect\b/.test(m[1])) {
24
+ return [
25
+ {
26
+ rule: "expect-import-source",
27
+ severity: "hard",
28
+ line: lineOfOffset(commentless, m.index),
29
+ message: "`expect` is imported from '@playwright/test'.",
30
+ remediation: "Import `expect` from '@skyramp/skyramp' instead (keep `test` on the playwright line).",
31
+ },
32
+ ];
33
+ }
34
+ }
35
+ }
36
+ return [];
37
+ }
38
+ // Any page-ish handle counts: specs keep a raw handle (`rawPage`) for
39
+ // assertions because expect() rejects the SkyrampPage proxy.
40
+ const PAGEERROR_LISTENER_RE = /[\w$]*[Pp]age\s*\.\s*on\s*\(\s*['"]pageerror['"]/;
41
+ const ERRORS_LENGTH_ASSERT_RES = [
42
+ /toHaveLength\s*\(\s*0\s*\)/,
43
+ // `(?:,[^)]*)?` admits expect's optional message argument.
44
+ /\.length\s*(?:,[^)]*)?\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(\s*0\s*\)/,
45
+ /(?:toEqual|toStrictEqual)\s*\(\s*\[\s*\]\s*\)/,
46
+ ];
47
+ /** pageerror listener registered but its errors array never asserted (hard);
48
+ * listener absent entirely stays warn-only — helpers/fixtures may register it,
49
+ * and forcing a structural insertion conflicts with autoApply's
50
+ * only-add-assertion-lines constraint. Promotion pending run evidence. */
51
+ function lintPageErrors(commentless) {
52
+ const listener = PAGEERROR_LISTENER_RE.exec(commentless);
53
+ if (!listener) {
54
+ return [
55
+ {
56
+ rule: "pageerror-listener-absent",
57
+ severity: "warn",
58
+ message: "No `page.on('pageerror', ...)` listener found in this file.",
59
+ remediation: "Register the listener before the first navigation and assert the collected errors array is empty at the end of the test.",
60
+ },
61
+ ];
62
+ }
63
+ if (ERRORS_LENGTH_ASSERT_RES.some((re) => re.test(commentless)))
64
+ return [];
65
+ return [
66
+ {
67
+ rule: "pageerror-unasserted",
68
+ severity: "hard",
69
+ line: lineOfOffset(commentless, listener.index),
70
+ message: "A `page.on('pageerror', ...)` listener collects errors, but the errors array is never asserted.",
71
+ remediation: "Add `expect(errors).toHaveLength(0)` (or equivalent) at the end of the test.",
72
+ },
73
+ ];
74
+ }
75
+ // Codegen emits BOTH shapes: direct (`const r = await page.waitForResponse(…)`)
76
+ // and promise-then-await (`const p = page.waitForResponse(…); …; const r = await p;`).
77
+ // Matching only the direct form made this lint a no-op on real codegen output.
78
+ const DIRECT_RESPONSE_BINDING_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+[\w$]*[Pp]age\s*\.\s*waitForResponse\s*\(/g;
79
+ const PROMISE_BINDING_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[\w$]*[Pp]age\s*\.\s*waitForResponse\s*\(/g;
80
+ /** A captured `waitForResponse` response that is never referenced again.
81
+ * Takes the raw comment-stripped source too: template-literal interpolations
82
+ * are blanked in `stripped`, so a use like `` `order-${resp.headers()...}` ``
83
+ * is only visible there.
84
+ *
85
+ * WARN, not hard — measured against skyramp's gen_reference UI goldens,
86
+ * pristine codegen emits 3–9 unused `const rN = await rPromiseN` captures per
87
+ * file (63 across the corpus): many are synchronization-only waits whose
88
+ * bodies aren't meaningfully assertable, and a hard finding per capture would
89
+ * dominate every UI verify loop with no marker escape. The rubric pressure to
90
+ * assert captured responses stays via this warn plus the strength gate. */
91
+ function lintDeadResponseCaptures(stripped, commentless) {
92
+ const findings = [];
93
+ // Response bindings from the direct shape…
94
+ const bindings = [];
95
+ let m;
96
+ DIRECT_RESPONSE_BINDING_RE.lastIndex = 0;
97
+ while ((m = DIRECT_RESPONSE_BINDING_RE.exec(stripped)) !== null) {
98
+ const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
99
+ bindings.push({
100
+ varName: m[1],
101
+ index: m.index,
102
+ end: close === -1 ? m.index + m[0].length : close + 1,
103
+ });
104
+ }
105
+ // …plus the promise-then-await shape: resolve `const r = await <promiseVar>`
106
+ // for every promise binding, and treat a promise that is never referenced at
107
+ // all as a dead capture itself.
108
+ // (The promise pattern cannot match the direct form — `await ` between `=`
109
+ // and the page handle breaks its contiguous match — so no overlap handling.)
110
+ PROMISE_BINDING_RE.lastIndex = 0;
111
+ while ((m = PROMISE_BINDING_RE.exec(stripped)) !== null) {
112
+ const promiseVar = m[1];
113
+ const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
114
+ const restStart = close === -1 ? m.index + m[0].length : close + 1;
115
+ const rest = stripped.slice(restStart);
116
+ const awaited = new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*await\\s+${escapeRegExp(promiseVar)}(?![\\w$])`).exec(rest);
117
+ if (awaited) {
118
+ bindings.push({
119
+ varName: awaited[1],
120
+ index: restStart + awaited.index,
121
+ end: restStart + awaited.index + awaited[0].length,
122
+ });
123
+ }
124
+ else if (!identifierRe(promiseVar).test(rest)) {
125
+ bindings.push({ varName: promiseVar, index: m.index, end: restStart });
126
+ }
127
+ }
128
+ for (const { varName, index, end } of bindings) {
129
+ const rest = stripped.slice(end);
130
+ const usedInInterpolation = interpolationRe(varName, "typescript").test(commentless.slice(end));
131
+ // Identifier-boundary lookarounds, not \b: `\b` breaks on `$`-prefixed
132
+ // names ($resp), silently turning this into a guaranteed false positive.
133
+ if (!identifierRe(varName).test(rest) && !usedInInterpolation) {
134
+ findings.push({
135
+ rule: "dead-response-capture",
136
+ severity: "warn",
137
+ line: lineOfOffset(stripped, index),
138
+ message: `\`${varName}\` captures a network response via waitForResponse but is never used.`,
139
+ remediation: `Assert at least one status, body, or header field on \`${varName}\` (e.g. \`expect(${varName}.status()).toBe(200)\`).`,
140
+ });
141
+ }
142
+ }
143
+ return findings;
144
+ }
145
+ // getByText('X') asserted toHaveText('X') with the identical literal. Restricted
146
+ // to getByText — getByRole+name asserted with the same text is a sanctioned
147
+ // pattern in the rules' own examples. Misses chains with links in between
148
+ // (.first() etc.) by design: a miss is fine, a false positive is not.
149
+ // The literal groups exclude `\` from the any-char alternative so the two
150
+ // alternatives are disjoint — the ambiguous form backtracks exponentially on
151
+ // escape-heavy literals (ReDoS that would hang the whole MCP server).
152
+ const TAUTOLOGY_RE = /(?:getByText|get_by_text)\s*\(\s*(['"`])((?:\\.|(?!\1)[^\\\n])*)\1[^)]*\)\s*\)?\s*\.\s*(?:toHaveText|toContainText|to_have_text|to_contain_text)\s*\(\s*(['"`])((?:\\.|(?!\3)[^\\\n])*)\3/g;
153
+ function lintTautologies(commentless, opts) {
154
+ const findings = [];
155
+ let m;
156
+ TAUTOLOGY_RE.lastIndex = 0;
157
+ while ((m = TAUTOLOGY_RE.exec(commentless)) !== null) {
158
+ const line = lineOfOffset(commentless, m.index);
159
+ if (m[2] === m[4] && inScope(line, opts)) {
160
+ findings.push({
161
+ rule: "tautological-assertion",
162
+ severity: "hard",
163
+ line,
164
+ message: `Tautology: element located by text ${JSON.stringify(m[2])} is asserted to have that same text.`,
165
+ remediation: "Assert a different knowable property (count, value, attribute), or locate the element by a structural selector and keep the text assertion.",
166
+ });
167
+ }
168
+ }
169
+ return findings;
170
+ }
171
+ // Signals that the page renders repeated elements (rows, list items, nth/first
172
+ // access), which the rubric expects paired with an exact toHaveCount(N).
173
+ const REPEATED_ELEMENT_RE = /getByRole\s*\(\s*['"](?:row|listitem|cell|option)['"]|\.\s*nth\s*\(|\.\s*first\s*\(\s*\)|\.\s*last\s*\(\s*\)/;
174
+ const HAS_COUNT_RE = /toHaveCount\s*\(|to_have_count\s*\(/;
175
+ /** Repeated/collection elements are exercised but no exact count is asserted.
176
+ * Warn-only: whether a list truly renders is not knowable from the spec alone
177
+ * (the D5 judge has a matching N/A condition for the same reason). */
178
+ function lintRepeatedElementsWithoutCount(commentless) {
179
+ const repeated = REPEATED_ELEMENT_RE.exec(commentless);
180
+ if (!repeated || HAS_COUNT_RE.test(commentless))
181
+ return [];
182
+ return [
183
+ {
184
+ rule: "repeated-elements-without-count",
185
+ severity: "warn",
186
+ line: lineOfOffset(commentless, repeated.index),
187
+ message: "The test works with repeated/collection elements but never asserts an exact count.",
188
+ remediation: "Add `toHaveCount(N)` (N from the trace or rendered DOM) plus at least one per-item content assertion.",
189
+ },
190
+ ];
191
+ }
192
+ // State-changing interactions; a test whose LAST such action is never followed
193
+ // by an assertion leaves the post-action state unverified (post_edit dim).
194
+ const ACTION_RE = /\.\s*(?:click|fill|press|check|uncheck|selectOption|select_option|set_input_files|setInputFiles)\s*\(/g;
195
+ const EXPECT_SITE_RE = /\bexpect(?:\s*\.\s*soft)?\s*\(/g;
196
+ function lintUnassertedTrailingAction(stripped) {
197
+ let lastAction = -1;
198
+ let m;
199
+ ACTION_RE.lastIndex = 0;
200
+ while ((m = ACTION_RE.exec(stripped)) !== null)
201
+ lastAction = m.index;
202
+ if (lastAction === -1)
203
+ return [];
204
+ let lastExpect = -1;
205
+ EXPECT_SITE_RE.lastIndex = 0;
206
+ while ((m = EXPECT_SITE_RE.exec(stripped)) !== null)
207
+ lastExpect = m.index;
208
+ if (lastExpect > lastAction)
209
+ return [];
210
+ return [
211
+ {
212
+ rule: "unasserted-trailing-action",
213
+ severity: "warn",
214
+ line: lineOfOffset(stripped, lastAction),
215
+ message: "The test's final state-changing action is never followed by an assertion — the updated state goes unverified.",
216
+ remediation: "After the last action, assert the visible updated outcome (`toHaveText`/`toHaveValue`/`toBeChecked`/`toHaveCount`) reflecting the edit.",
217
+ },
218
+ ];
219
+ }
220
+ /** UI (Playwright) lints. Python (playwright-python) UI specs deliberately get
221
+ * the site-scoped lints only: the structural rules' patterns and accept-lists
222
+ * are JS/TS-specific (imports, waitForResponse shapes, expect matcher accept
223
+ * forms), and porting them without a python golden corpus to validate against
224
+ * would recreate the false-block class review round 2 removed. Pinned by test. */
225
+ export function lintUiSpec(raw, language, opts) {
226
+ if (language === "java")
227
+ return [];
228
+ const maintenance = opts?.scopeLines !== undefined;
229
+ const { commentless, stripped } = strippedSources(raw, language);
230
+ const findings = [];
231
+ if (language !== "python" && !maintenance) {
232
+ // Import lint is generation-only too: maintenance updates existing customer
233
+ // specs, which legitimately import expect from @playwright/test (the repo
234
+ // may not even depend on @skyramp/skyramp), and the maintenance scope
235
+ // forbids touching imports.
236
+ findings.push(...lintPlaywrightExpectImport(commentless));
237
+ findings.push(...lintPageErrors(commentless));
238
+ findings.push(...lintDeadResponseCaptures(stripped, commentless));
239
+ findings.push(...lintRepeatedElementsWithoutCount(commentless));
240
+ findings.push(...lintUnassertedTrailingAction(stripped));
241
+ }
242
+ findings.push(...lintTautologies(commentless, opts));
243
+ return findings;
244
+ }
@@ -0,0 +1,61 @@
1
+ import { type AssertionMetrics } from "./metrics.js";
2
+ import type { LintFinding } from "./lint-types.js";
3
+ export type AssertionEnhanceType = "generation" | "maintenance";
4
+ /** Snapshot taken when the enhancement instructions were handed out. */
5
+ export interface AssertionBaseline {
6
+ fileSha256: string;
7
+ count?: number;
8
+ strengthScore?: number;
9
+ fingerprints?: string[];
10
+ /** Copy of the generated file at hand-out time (run artifact dir). */
11
+ baselineFilePath?: string;
12
+ }
13
+ export interface AssertionVerifyResult {
14
+ ok: boolean;
15
+ enhanceType: AssertionEnhanceType;
16
+ /** False when verifying stateless (no baseline) — differential gates skipped. */
17
+ baselinePresent: boolean;
18
+ /** Maintenance verified with an empty assertion diff — nothing was added,
19
+ * changed, or removed, so there was nothing to verify. */
20
+ maintenanceNoAssertionChanges?: boolean;
21
+ hashUnchanged: boolean;
22
+ /** Baseline assertions whose SUBJECT lost all coverage — genuine removals
23
+ * (sample of 5). Replaced assertions (subject still asserted) never appear. */
24
+ removedFingerprints?: string[];
25
+ /** Total genuine removals — lets the report say "showing 5 of N" so the
26
+ * agent restores everything in ONE pass instead of discovering the rest
27
+ * on the next verify round. */
28
+ removedCount?: number;
29
+ /** Baseline assertions superseded by a new assertion on the same subject. */
30
+ replacedCount?: number;
31
+ /** Baseline assertions whose subject now lives in a locally imported helper
32
+ * file (modularization) — sanctioned, never counted as removed. */
33
+ movedToHelperCount?: number;
34
+ /** Copy of the generated file at hand-out time, for diff/restore remediation. */
35
+ baselineFilePath?: string;
36
+ /** Current − baseline strength score; undefined without a baseline. */
37
+ strengthDelta?: number;
38
+ strengthGateFailed: boolean;
39
+ /** Assertions were added, but every one is existence/visibility-tier — the
40
+ * computed-values rubric wants exact matchers. Runs in generation AND
41
+ * maintenance (it requires additions to exist, so value-only maintenance
42
+ * fixes cannot trip it). Marker-clearable, like the strength gate. */
43
+ weakAdditionsOnly: boolean;
44
+ hardFindings: LintFinding[];
45
+ warnings: LintFinding[];
46
+ markerReason?: string;
47
+ currentMetrics?: AssertionMetrics;
48
+ }
49
+ export declare function sha256Of(content: string): string;
50
+ /**
51
+ * Deterministic verification that the enhancement instructions were applied:
52
+ * hash delta, no assertions removed, rubric lints clean, and (generation only)
53
+ * strength strictly increased unless a documented `assertions complete` marker
54
+ * declines it. Reads the file as delivered — throws if it cannot be read.
55
+ */
56
+ export declare function verifyAssertionEnhancement(params: {
57
+ testFile: string;
58
+ testType: string;
59
+ enhanceType: AssertionEnhanceType;
60
+ baseline?: AssertionBaseline;
61
+ }): Promise<AssertionVerifyResult>;
@@ -0,0 +1,215 @@
1
+ import { createHash } from "crypto";
2
+ import { readFile } from "fs/promises";
3
+ import { computeAssertionMetrics, detectAssertionLanguage, subjectOf, } from "./metrics.js";
4
+ import { lintUiSpec } from "./ui-lints.js";
5
+ import { lintIntegrationTest } from "./integration-lints.js";
6
+ import { lintContractTest } from "./contract-lints.js";
7
+ import { findAssertionsCompleteMarker, findUnparsableAssertionsComplete, markerCommentToken, } from "./marker.js";
8
+ import { importedHelperSubjects } from "./helper-imports.js";
9
+ export function sha256Of(content) {
10
+ return createHash("sha256").update(content).digest("hex");
11
+ }
12
+ /** Current sites whose fingerprint occurs more often now than in the baseline
13
+ * multiset — the assertions ADDED since hand-out (includes the new side of a
14
+ * replacement). Powers maintenance lint scoping and the weak-additions gate. */
15
+ function addedSites(current, baselineFingerprints) {
16
+ const budget = new Map();
17
+ for (const fp of baselineFingerprints)
18
+ budget.set(fp, (budget.get(fp) ?? 0) + 1);
19
+ const added = [];
20
+ for (const site of current.sites) {
21
+ const left = budget.get(site.fingerprint) ?? 0;
22
+ if (left > 0)
23
+ budget.set(site.fingerprint, left - 1);
24
+ else
25
+ added.push(site);
26
+ }
27
+ return added;
28
+ }
29
+ /**
30
+ * Deterministic verification that the enhancement instructions were applied:
31
+ * hash delta, no assertions removed, rubric lints clean, and (generation only)
32
+ * strength strictly increased unless a documented `assertions complete` marker
33
+ * declines it. Reads the file as delivered — throws if it cannot be read.
34
+ */
35
+ export async function verifyAssertionEnhancement(params) {
36
+ const { testFile, testType, enhanceType, baseline } = params;
37
+ const content = await readFile(testFile, "utf8");
38
+ const language = detectAssertionLanguage(testFile);
39
+ const hashUnchanged = baseline !== undefined && sha256Of(content) === baseline.fileSha256;
40
+ // Unknown language: only the hash gate is computable — everything else fails open.
41
+ if (language === undefined) {
42
+ return {
43
+ ok: !(hashUnchanged && enhanceType === "generation"),
44
+ enhanceType,
45
+ baselinePresent: baseline !== undefined,
46
+ hashUnchanged,
47
+ strengthGateFailed: false,
48
+ weakAdditionsOnly: false,
49
+ hardFindings: [],
50
+ warnings: [],
51
+ };
52
+ }
53
+ const metrics = computeAssertionMetrics(content, language);
54
+ const countDecreasedBy = baseline?.count !== undefined
55
+ ? Math.max(0, baseline.count - metrics.count)
56
+ : 0;
57
+ // Correlate what disappeared since the baseline. A missing fingerprint whose
58
+ // SUBJECT is still asserted (by any current site) was REPLACED — a sanctioned
59
+ // enhancement (usually weak matcher → strong). A subject asserted inside a
60
+ // locally imported helper file was MOVED (modularization) — also sanctioned.
61
+ // A KNOWN subject that lost all coverage counts as removed regardless of the
62
+ // raw count (a same-count remove-A-add-B edit must not slip through);
63
+ // unrecoverable subjects stay conservative and count only when the raw count
64
+ // actually decreased.
65
+ let removedFingerprints;
66
+ let removedCount;
67
+ let replacedCount;
68
+ let movedToHelperCount;
69
+ if (baseline?.fingerprints !== undefined) {
70
+ const current = new Map();
71
+ for (const fp of metrics.fingerprints) {
72
+ current.set(fp, (current.get(fp) ?? 0) + 1);
73
+ }
74
+ const missing = baseline.fingerprints.filter((fp) => {
75
+ const left = current.get(fp) ?? 0;
76
+ if (left > 0) {
77
+ current.set(fp, left - 1);
78
+ return false;
79
+ }
80
+ return true;
81
+ });
82
+ const currentSubjects = new Set(metrics.fingerprints.map(subjectOf).filter((s) => s !== undefined));
83
+ let removed = missing.filter((fp) => {
84
+ const subject = subjectOf(fp);
85
+ if (subject === undefined)
86
+ return countDecreasedBy > 0;
87
+ return !currentSubjects.has(subject);
88
+ });
89
+ // Only pay the helper-file scan when something actually looks removed.
90
+ if (removed.length > 0) {
91
+ const helperSubjects = await importedHelperSubjects(testFile, content);
92
+ if (helperSubjects.size > 0) {
93
+ const stillRemoved = removed.filter((fp) => {
94
+ const subject = subjectOf(fp);
95
+ return subject === undefined || !helperSubjects.has(subject);
96
+ });
97
+ movedToHelperCount = removed.length - stillRemoved.length;
98
+ removed = stillRemoved;
99
+ }
100
+ }
101
+ replacedCount = missing.length - removed.length - (movedToHelperCount ?? 0);
102
+ if (removed.length > 0) {
103
+ removedFingerprints = removed.slice(0, 5);
104
+ removedCount = removed.length;
105
+ }
106
+ }
107
+ const strengthDelta = baseline?.strengthScore !== undefined
108
+ ? metrics.strengthScore - baseline.strengthScore
109
+ : undefined;
110
+ const added = baseline?.fingerprints !== undefined
111
+ ? addedSites(metrics, baseline.fingerprints)
112
+ : undefined;
113
+ // Maintenance scopes site lints to assertions added since the baseline; the
114
+ // baseline fingerprints are what make that diff computable.
115
+ const scopeLines = enhanceType === "maintenance" && added !== undefined
116
+ ? new Set(added.map((s) => s.line))
117
+ : undefined;
118
+ const lintOpts = scopeLines !== undefined ? { scopeLines } : undefined;
119
+ // Per-test-type lints; Java is never linted or strength-gated — count+hash
120
+ // only. Test types with no lint module (smoke/fuzz/load/e2e — the hand-out
121
+ // path refuses them) get NO lints rather than silently inheriting
122
+ // integration's: those rules were not written for them.
123
+ const findings = testType === "ui"
124
+ ? lintUiSpec(content, language, lintOpts)
125
+ : testType === "contract"
126
+ ? lintContractTest(content, language, lintOpts)
127
+ : testType === "integration"
128
+ ? lintIntegrationTest(content, language, lintOpts)
129
+ : [];
130
+ const marker = findAssertionsCompleteMarker(content, testFile, language);
131
+ const hardFindings = findings.filter((f) => f.severity === "hard");
132
+ const commentToken = markerCommentToken(language);
133
+ for (const line of findUnparsableAssertionsComplete(content, language)) {
134
+ hardFindings.push({
135
+ rule: "malformed-assertions-complete-marker",
136
+ severity: "hard",
137
+ message: `Unreadable marker: \`${line}\``,
138
+ remediation: `Use \`${commentToken} assertions complete: <test-file basename> — <reason>\` (this language's comment token; em dash, or a hyphen with a space on BOTH sides), with a real reason.`,
139
+ });
140
+ }
141
+ // Removals are NEVER marker-clearable: a marker that excused them would let
142
+ // "delete the failing assertion + one comment line" verify green — the exact
143
+ // case this gate exists to catch. The sanctioned coverage-moves (verify
144
+ // before modularization per the mandated steps; passed-verdict skip at
145
+ // execute time) don't need the exemption, and a post-modularize failure is
146
+ // always fixable by re-adding direct assertions beside the helper calls.
147
+ // (Derived, not stored: removedFingerprints !== undefined IS the gate.)
148
+ const removalGateFailed = removedFingerprints !== undefined;
149
+ // A decline marker is accepted on a GENERATION file only when work
150
+ // actually happened (something added or replaced). Without this, a
151
+ // marker-only comment edit was a full bypass: the comment changes the hash,
152
+ // the marker cleared both strength-family gates, and an untouched file
153
+ // trips nothing else — the exact "tests already have the key assertions"
154
+ // no-op the gate exists to catch (review finding on ae8b397e). The marker
155
+ // can excuse INSUFFICIENT strengthening ("only existence is knowable"); it
156
+ // cannot excuse ABSENT strengthening. Maintenance keeps marker semantics
157
+ // unchanged (its strength gates don't run; value-only fixes are legitimate).
158
+ const markerAccepted = marker !== undefined &&
159
+ (enhanceType !== "generation" ||
160
+ (added?.length ?? 0) > 0 ||
161
+ (replacedCount ?? 0) > 0);
162
+ const strengthGateFailed = enhanceType === "generation" &&
163
+ language !== "java" &&
164
+ strengthDelta !== undefined &&
165
+ strengthDelta <= 0 &&
166
+ !markerAccepted;
167
+ // Computed-values rubric: additions must not be exclusively existence/
168
+ // visibility-tier. A replacement's new side counts as an added site, so a
169
+ // weak→strong upgrade satisfies this. Marker-clearable (strength family).
170
+ // Unlike the strength-increase gate this runs in MAINTENANCE too: it is
171
+ // conditional on additions existing, so a value-only drift fix (no added
172
+ // sites) can never trip it — only genuinely weak new assertions do.
173
+ const weakAdditionsOnly = language !== "java" &&
174
+ added !== undefined &&
175
+ added.length > 0 &&
176
+ added.every((s) => s.weight === 1) &&
177
+ !markerAccepted;
178
+ // The hash gate ("byte-identical → nothing applied") is generation-only: in
179
+ // maintenance the file edits precede the enhance call, and "no assertion
180
+ // changes needed" is a legitimate outcome — the assertion-diff below is what
181
+ // decides whether there is anything to verify at all.
182
+ const hashGateFailed = hashUnchanged && enhanceType === "generation";
183
+ // Maintenance with an EMPTY assertion diff (nothing added, changed, or
184
+ // removed since hand-out): there is no assertion work to verify — pass, and
185
+ // let the report say so honestly.
186
+ const maintenanceNoAssertionChanges = enhanceType === "maintenance" &&
187
+ baseline?.fingerprints !== undefined &&
188
+ (added?.length ?? 0) === 0 &&
189
+ replacedCount === 0 &&
190
+ (removedCount ?? 0) === 0 &&
191
+ (movedToHelperCount ?? 0) === 0;
192
+ return {
193
+ ok: !hashGateFailed &&
194
+ !removalGateFailed &&
195
+ hardFindings.length === 0 &&
196
+ !strengthGateFailed &&
197
+ !weakAdditionsOnly,
198
+ maintenanceNoAssertionChanges,
199
+ enhanceType,
200
+ baselinePresent: baseline !== undefined,
201
+ hashUnchanged,
202
+ removedFingerprints,
203
+ removedCount,
204
+ replacedCount,
205
+ movedToHelperCount,
206
+ baselineFilePath: baseline?.baselineFilePath,
207
+ strengthDelta,
208
+ strengthGateFailed,
209
+ weakAdditionsOnly,
210
+ hardFindings,
211
+ warnings: findings.filter((f) => f.severity === "warn"),
212
+ markerReason: markerAccepted ? marker?.reason : undefined,
213
+ currentMetrics: metrics,
214
+ };
215
+ }
@@ -4,7 +4,8 @@
4
4
  * Each helper in this module reads a `SKYRAMP_FEATURE_*` env var and returns
5
5
  * a boolean. These helpers treat a flag as ON only when the env var is
6
6
  * exactly `"1"`; anything else (unset, empty, "0", "true", "false", etc.) is
7
- * treated as OFF.
7
+ * treated as OFF. The utils-reuse flag is additionally ON for the orgs in
8
+ * `UTILS_REUSE_DEFAULT_ORGS` regardless of the env vars.
8
9
  */
9
10
  /**
10
11
  * Gates BOTH consumer-side contract tests AND the SDK's "default mode"
@@ -55,6 +56,14 @@ export declare function isOneClickEnabled(): boolean;
55
56
  * Gates the POM-aware path of `skyramp_reuse_code`
56
57
  * (SKYRAMP_FEATURE_POM_REUSE=1).
57
58
  *
59
+ * Deliberately NOT defaulted on for `UTILS_REUSE_DEFAULT_ORGS`: for UI tests
60
+ * the POM path supersedes the modularize-first flow (see
61
+ * `isModularizeFirstTarget` in utils/reuseRouting.ts), so an org-level POM
62
+ * default would stop those orgs' UI tests from ever seeding a SkyrampUtils
63
+ * file — the very thing the utils org default exists to provide (SKYR-4290).
64
+ * And an explicit-"0"-override escape hatch cannot work either, because
65
+ * testbot normalizes an unset flag to a literal "0" before forwarding.
66
+ *
58
67
  * When OFF (default), TypeScript/JavaScript + Playwright targets are treated
59
68
  * like every other target: `skyramp_reuse_code` returns the SkyrampUtils
60
69
  * consolidation prompt, and none of the POM machinery runs — no deterministic
@@ -68,7 +77,8 @@ export declare function isOneClickEnabled(): boolean;
68
77
  export declare function isPomReuseEnabled(): boolean;
69
78
  /**
70
79
  * Gates SkyrampUtils code reuse for integration tests on the testbot path, and
71
- * for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1).
80
+ * for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1,
81
+ * or a `UTILS_REUSE_DEFAULT_ORGS` org).
72
82
  *
73
83
  * When OFF (default), the testbot prompt does not set `codeReuse` on
74
84
  * `skyramp_integration_test_generation` and its post-generation
@@ -4,7 +4,8 @@
4
4
  * Each helper in this module reads a `SKYRAMP_FEATURE_*` env var and returns
5
5
  * a boolean. These helpers treat a flag as ON only when the env var is
6
6
  * exactly `"1"`; anything else (unset, empty, "0", "true", "false", etc.) is
7
- * treated as OFF.
7
+ * treated as OFF. The utils-reuse flag is additionally ON for the orgs in
8
+ * `UTILS_REUSE_DEFAULT_ORGS` regardless of the env vars.
8
9
  */
9
10
  /**
10
11
  * Gates BOTH consumer-side contract tests AND the SDK's "default mode"
@@ -63,6 +64,14 @@ export function isOneClickEnabled() {
63
64
  * Gates the POM-aware path of `skyramp_reuse_code`
64
65
  * (SKYRAMP_FEATURE_POM_REUSE=1).
65
66
  *
67
+ * Deliberately NOT defaulted on for `UTILS_REUSE_DEFAULT_ORGS`: for UI tests
68
+ * the POM path supersedes the modularize-first flow (see
69
+ * `isModularizeFirstTarget` in utils/reuseRouting.ts), so an org-level POM
70
+ * default would stop those orgs' UI tests from ever seeding a SkyrampUtils
71
+ * file — the very thing the utils org default exists to provide (SKYR-4290).
72
+ * And an explicit-"0"-override escape hatch cannot work either, because
73
+ * testbot normalizes an unset flag to a literal "0" before forwarding.
74
+ *
66
75
  * When OFF (default), TypeScript/JavaScript + Playwright targets are treated
67
76
  * like every other target: `skyramp_reuse_code` returns the SkyrampUtils
68
77
  * consolidation prompt, and none of the POM machinery runs — no deterministic
@@ -78,7 +87,8 @@ export function isPomReuseEnabled() {
78
87
  }
79
88
  /**
80
89
  * Gates SkyrampUtils code reuse for integration tests on the testbot path, and
81
- * for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1).
90
+ * for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1,
91
+ * or a `UTILS_REUSE_DEFAULT_ORGS` org).
82
92
  *
83
93
  * When OFF (default), the testbot prompt does not set `codeReuse` on
84
94
  * `skyramp_integration_test_generation` and its post-generation
@@ -97,5 +107,25 @@ export function isPomReuseEnabled() {
97
107
  * yields the single reuse pass it did before the feature existed.
98
108
  */
99
109
  export function isUtilsReuseEnabled() {
100
- return process.env.SKYRAMP_FEATURE_UTILS_REUSE === "1";
110
+ return (process.env.SKYRAMP_FEATURE_UTILS_REUSE === "1" || isUtilsReuseDefaultOrg());
111
+ }
112
+ /**
113
+ * GitHub orgs where SkyrampUtils code reuse is on by default, without
114
+ * requiring the SKYRAMP_FEATURE_UTILS_REUSE env var. Compared
115
+ * case-insensitively (GitHub owner handles are case-insensitive, but
116
+ * GITHUB_REPOSITORY_OWNER preserves display casing). Because this ORs with
117
+ * the env check, these orgs cannot turn the feature off by setting the env
118
+ * var to "0". POM reuse has no org default — see `isPomReuseEnabled`.
119
+ *
120
+ * "Skyramp-HandsOn" is the Wipro / IntelliAssure pilot org (SKYR-4286).
121
+ */
122
+ const UTILS_REUSE_DEFAULT_ORGS = ["skyramp-handson"];
123
+ function isUtilsReuseDefaultOrg() {
124
+ // GITHUB_REPOSITORY_OWNER is set on GitHub Actions runners; fall back to
125
+ // the owner half of GITHUB_REPOSITORY ("owner/repo") for environments that
126
+ // only set the latter.
127
+ const owner = process.env.GITHUB_REPOSITORY_OWNER ??
128
+ process.env.GITHUB_REPOSITORY?.split("/")[0] ??
129
+ "";
130
+ return UTILS_REUSE_DEFAULT_ORGS.includes(owner.toLowerCase());
101
131
  }
@@ -22,7 +22,10 @@ export declare function isIndexModule(file: string): boolean;
22
22
  * (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
23
23
  * ends with the aliased path AND the importer lives under the same root that the
24
24
  * alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
25
- * not match `packages/admin/app/shop/_constants/index.ts`. All paths are
25
+ * not match `packages/admin/app/shop/_constants/index.ts`. Prefix-less
26
+ * multi-segment specifiers (`Components/Foo` under tsconfig `baseUrl: "src"` —
27
+ * SKYR-4287) go through the same suffix + shared-root check; only single-segment
28
+ * prefix-less specifiers (`react`) are treated as npm packages. All paths are
26
29
  * repo-relative with forward slashes.
27
30
  */
28
31
  export declare function importsIndexModule(importerFile: string, importerContent: string, indexFile: string): boolean;
@@ -60,7 +60,10 @@ export function isIndexModule(file) {
60
60
  * (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
61
61
  * ends with the aliased path AND the importer lives under the same root that the
62
62
  * alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
63
- * not match `packages/admin/app/shop/_constants/index.ts`. All paths are
63
+ * not match `packages/admin/app/shop/_constants/index.ts`. Prefix-less
64
+ * multi-segment specifiers (`Components/Foo` under tsconfig `baseUrl: "src"` —
65
+ * SKYR-4287) go through the same suffix + shared-root check; only single-segment
66
+ * prefix-less specifiers (`react`) are treated as npm packages. All paths are
64
67
  * repo-relative with forward slashes.
65
68
  */
66
69
  export function importsIndexModule(importerFile, importerContent, indexFile) {
@@ -76,7 +79,16 @@ export function importsIndexModule(importerFile, importerContent, indexFile) {
76
79
  continue;
77
80
  }
78
81
  const aliased = spec.replace(/^(@\/|~~\/|~\/|#\/|src\/)/, "");
79
- if (aliased === spec)
82
+ // A prefix-less specifier is only a bare npm package when it has a single
83
+ // segment ('react'). Multi-segment prefix-less specifiers are how tsconfig
84
+ // `baseUrl` repos import internal modules ('Components/Foo' with
85
+ // baseUrl: "src" — SKYR-4287: every folder-per-component index.tsx in such
86
+ // repos was misreported as no-importers). Those flow into the same
87
+ // suffix + shared-root check as aliases; a deep npm import ('lodash/get')
88
+ // is unlikely to match unless the repo actually contains a `lodash/get`
89
+ // directory under the same root, and a scoped package ('@scope/pkg') would
90
+ // require a source directory literally named '@scope'.
91
+ if (aliased === spec && !spec.includes("/"))
80
92
  continue; // bare package specifier — not a repo path
81
93
  if (moduleDir === aliased)
82
94
  return true; // alias maps to the repo root