Package not found. Please check the package name and try again.

@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,218 @@
1
+ import { execFile } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { promisify } from "util";
5
+ import { logger } from "../logger.js";
6
+ import { detectGitRoot } from "../gitStaging.js";
7
+ import { blankCommentsAndStrings, parseUtilsFile } from "./parse.js";
8
+ import { normalizePath } from "./language-spec.js";
9
+ import { callText, hasSchemaCheck, requestFields, REQUEST_CALL_RE, SCHEMA_SCAN_LINES, shapeKeys, } from "./call-sites.js";
10
+ import { escapeRegExp } from "../regex.js";
11
+ const exec = promisify(execFile);
12
+ /**
13
+ * Whether a retrofit changed the request the rewired test sends.
14
+ *
15
+ * STEP 5c edits a PRE-EXISTING test — code the customer already owns and whose
16
+ * behaviour nothing in this run chose — to call a shared helper instead of its own
17
+ * inline request block. That is only safe if the helper sends the same request. It
18
+ * usually does, because the helper was extracted from an identical block; but nothing
19
+ * checked it, and a helper that hardcodes a field the block never sent (or omits one
20
+ * it did) silently changes what the customer's test exercises. The failure is quiet:
21
+ * the test still runs, and a red result reads like any other red.
22
+ *
23
+ * The advisory path already refuses to OFFER such a site (see `substitutable` in
24
+ * call-sites.ts). This is the same question asked after the fact, about the edit that
25
+ * was actually made, and the rule is stricter: offering tolerates a site with an extra
26
+ * field, because extending the helper with a defaulted parameter is the sanctioned
27
+ * move; a completed retrofit must leave the request byte-equivalent in shape, so the
28
+ * field sets must match exactly in both directions.
29
+ *
30
+ * LIMIT: this compares the request's SHAPE — which fields and options it carries —
31
+ * not the values passed. A baseline sending `status: "draft"` rewired to
32
+ * `createPost(..., "published")` has the same field set and is not reported. Value
33
+ * equivalence needs argument-to-parameter binding, default resolution and expression
34
+ * normalisation; a wrong answer here REFUSES the report, so it is deliberately left
35
+ * out rather than approximated. The value case is also visible in the rewired file's
36
+ * own diff, where a hardcoded helper field is not.
37
+ *
38
+ * Compares against the file's committed baseline (`git show HEAD:<path>`), which is
39
+ * the only record of what the block sent before the edit. Fails OPEN — an untracked
40
+ * file, a detached worktree, or an unreadable baseline yields no mismatches rather
41
+ * than a refusal the agent cannot act on.
42
+ */
43
+ export async function retrofitMismatches(retrofitFile, utilsFiles, spec) {
44
+ const baseline = await committedBaseline(retrofitFile);
45
+ if (baseline === undefined)
46
+ return [];
47
+ let delivered;
48
+ try {
49
+ delivered = await fs.promises.readFile(retrofitFile, "utf8");
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ const byRoute = new Map();
55
+ for (const utilsFile of utilsFiles) {
56
+ let content;
57
+ try {
58
+ content = await fs.promises.readFile(utilsFile, "utf8");
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ const helpers = parseUtilsFile(content, spec);
64
+ // Positions come from a comment/string-blanked copy (offsets preserved), so a
65
+ // commented-out or quoted request is never read as a real one; the text itself
66
+ // is taken from the original.
67
+ const scan = blankCommentsAndStrings(content, spec);
68
+ for (const m of scan.matchAll(new RegExp(REQUEST_CALL_RE))) {
69
+ const start = m.index ?? 0;
70
+ const line = content.slice(0, start).split("\n").length;
71
+ const owner = helpers.find((h) => h.line <= line && line <= h.endLine);
72
+ if (!owner?.method || !owner.normalizedPath)
73
+ continue;
74
+ const call = callText(content, start + m[0].length - 1);
75
+ const route = `${owner.method} ${owner.normalizedPath}`;
76
+ const list = byRoute.get(route) ?? [];
77
+ // One helper may issue several requests; judge it by its first for this route.
78
+ if (!list.some((h) => h.helper === owner.name))
79
+ list.push({
80
+ helper: owner.name,
81
+ keys: shapeKeys(call),
82
+ fields: requestFields(content, call, start),
83
+ schemaChecked: hasSchemaCheck(content, owner.line, owner.endLine),
84
+ });
85
+ byRoute.set(route, list);
86
+ }
87
+ }
88
+ if (byRoute.size === 0)
89
+ return [];
90
+ // The requests the file used to make itself, before the edit.
91
+ const baselineByRoute = new Map();
92
+ const ambiguous = new Set();
93
+ const baselineScan = blankCommentsAndStrings(baseline, spec);
94
+ for (const m of baselineScan.matchAll(new RegExp(REQUEST_CALL_RE))) {
95
+ const start = m.index ?? 0;
96
+ const line = baseline.slice(0, start).split("\n").length;
97
+ const call = callText(baseline, start + m[0].length - 1);
98
+ const method = spec.methodRe.exec(call)?.[1]?.toUpperCase();
99
+ const p = spec.pathRe.exec(call)?.[1];
100
+ if (!method || !p)
101
+ continue;
102
+ const key = `${method} ${normalizePath(p)}`;
103
+ const seen = baselineByRoute.get(key);
104
+ const here = {
105
+ keys: shapeKeys(call),
106
+ fields: requestFields(baseline, call, start),
107
+ schemaChecked: hasSchemaCheck(baseline, line, line + SCHEMA_SCAN_LINES),
108
+ };
109
+ if (!seen) {
110
+ baselineByRoute.set(key, here);
111
+ continue;
112
+ }
113
+ // The same route more than once: only one of them was replaced, and nothing here
114
+ // says which. Identical blocks compare the same either way; DIFFERING ones are
115
+ // ambiguous, so the route is dropped rather than judged against a block that may
116
+ // not be the one the helper took over. Occurrence-level attribution would need to
117
+ // know which block disappeared, and getting that wrong refuses a safe edit.
118
+ if (!sameSet(seen.fields, here.fields) || !sameSet(seen.keys, here.keys))
119
+ ambiguous.add(key);
120
+ }
121
+ for (const key of ambiguous)
122
+ baselineByRoute.delete(key);
123
+ if (baselineByRoute.size === 0)
124
+ return [];
125
+ const calls = blankCommentsAndStrings(delivered, spec);
126
+ const out = [];
127
+ for (const [route, helpers] of byRoute) {
128
+ const before = baselineByRoute.get(route);
129
+ if (!before)
130
+ continue; // the file never made this request; nothing was replaced
131
+ for (const helper of helpers) {
132
+ // Only a helper this file actually calls now can have replaced anything —
133
+ // under whatever local name it was imported as (`import create_order as mk`).
134
+ const local = localBindings(calls, helper.helper);
135
+ if (!local.some((n) => new RegExp(`\\b${escapeRegExp(n)}\\s*\\(`).test(calls)))
136
+ continue;
137
+ const added = [...helper.fields].filter((f) => !before.fields.has(f));
138
+ const dropped = [...before.fields].filter((f) => !helper.fields.has(f));
139
+ const reasons = [];
140
+ // Non-liftable call options (query_params, timeout, …): the advisory compares
141
+ // these before offering a site, so the completed edit must be held to them too.
142
+ const optAdded = [...helper.keys].filter((k) => !before.keys.has(k));
143
+ const optDropped = [...before.keys].filter((k) => !helper.keys.has(k));
144
+ if (optAdded.length > 0 || optDropped.length > 0)
145
+ reasons.push(`the request options changed (${[
146
+ ...optAdded.map((k) => `+\`${k}\``),
147
+ ...optDropped.map((k) => `-\`${k}\``),
148
+ ].join(", ")})`);
149
+ if (added.length > 0)
150
+ reasons.push(`it now also sends ${added.map((f) => `\`${f}\``).join(", ")}`);
151
+ if (dropped.length > 0)
152
+ reasons.push(`it no longer sends ${dropped.map((f) => `\`${f}\``).join(", ")}`);
153
+ if (before.schemaChecked && !helper.schemaChecked)
154
+ reasons.push("the response-schema check the block carried is gone");
155
+ if (reasons.length === 0)
156
+ continue;
157
+ const [method, ...rest] = route.split(" ");
158
+ out.push({
159
+ file: retrofitFile,
160
+ helper: helper.helper,
161
+ method,
162
+ path: rest.join(" "),
163
+ detail: reasons.join("; "),
164
+ });
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+ /**
170
+ * The local names a module's export can be called by in this file: the export's own
171
+ * name, plus any `import X as Y` / `{ X as Y }` alias. A retrofit that imported the
172
+ * helper under an alias would otherwise look uncalled and fail open.
173
+ */
174
+ function localBindings(content, exported) {
175
+ const esc = escapeRegExp(exported);
176
+ const names = new Set([exported]);
177
+ // Python `from M import x as y`; TS `import { x as y }` / `{ x: y } = require(...)`.
178
+ for (const re of [
179
+ new RegExp(`\\b${esc}\\s+as\\s+([A-Za-z_$][\\w$]*)`, "g"),
180
+ new RegExp(`\\b${esc}\\s*:\\s*([A-Za-z_$][\\w$]*)`, "g"),
181
+ ])
182
+ for (const m of content.matchAll(re))
183
+ names.add(m[1]);
184
+ return [...names];
185
+ }
186
+ function sameSet(a, b) {
187
+ if (a.size !== b.size)
188
+ return false;
189
+ for (const v of a)
190
+ if (!b.has(v))
191
+ return false;
192
+ return true;
193
+ }
194
+ /** The file's content at HEAD, or undefined when there is nothing to compare against
195
+ * (untracked, no git root, or git unavailable). Never throws. */
196
+ async function committedBaseline(file) {
197
+ const root = detectGitRoot(file);
198
+ if (!root)
199
+ return undefined;
200
+ const rel = path.relative(root, file);
201
+ if (!rel || rel.startsWith(".."))
202
+ return undefined;
203
+ try {
204
+ const { stdout } = await exec("git", ["show", `HEAD:${rel}`], {
205
+ cwd: root,
206
+ maxBuffer: 10 * 1024 * 1024,
207
+ });
208
+ return stdout;
209
+ }
210
+ catch (err) {
211
+ // A file added by this run has no HEAD version — the common case, not an error.
212
+ logger.debug("No committed baseline for a retrofit; skipping equivalence", {
213
+ file,
214
+ error: String(err),
215
+ });
216
+ return undefined;
217
+ }
218
+ }
@@ -16,6 +16,12 @@
16
16
  export interface StagingResult {
17
17
  /** Files that reached the index. */
18
18
  staged: string[];
19
+ /** Utils files located for the test (absolute), staged or not. */
20
+ utilsFiles: string[];
21
+ /** Pre-existing generated siblings this run edited to import from those files
22
+ * (absolute) — the caller records them as retrofits so the report must disclose
23
+ * them. */
24
+ retrofits: string[];
19
25
  /** Utils files that `git add` refused (gitignored, outside the repo, git failure) —
20
26
  * the tests importing them will fail in the output commit, so the verify pass
21
27
  * treats a non-empty list as a failed verification. */
@@ -12,7 +12,9 @@ const execFileAsync = promisify(execFile);
12
12
  export async function stageUtilsArtifacts(testFile, language) {
13
13
  const staged = [];
14
14
  const failed = [];
15
- const result = { staged, failed };
15
+ const utilsFilesOut = [];
16
+ const retrofits = [];
17
+ const result = { staged, failed, utilsFiles: utilsFilesOut, retrofits };
16
18
  // Outside a testbot run stageGeneratedPaths is a no-op; report nothing as staged
17
19
  // rather than a list of files nothing touched.
18
20
  if (!isTestbotEnabled())
@@ -46,7 +48,9 @@ export async function stageUtilsArtifacts(testFile, language) {
46
48
  // file, so without this the sibling's edit drops out of the output commit while the
47
49
  // utils change lands — the same class of hole as the unstaged utils file.
48
50
  if (files.length > 0) {
51
+ utilsFilesOut.push(...files);
49
52
  for (const sibling of await modifiedSiblingsImporting(testFile, files, language)) {
53
+ retrofits.push(sibling);
50
54
  try {
51
55
  await stageGeneratedPaths(sibling);
52
56
  staged.push(sibling);
@@ -69,7 +73,13 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
69
73
  let modified;
70
74
  try {
71
75
  // `-z`: unquoted, NUL-separated — git C-quotes non-ASCII paths otherwise.
72
- const { stdout } = await execFileAsync("git", ["diff", "-z", "--name-only"], {
76
+ const { stdout } = await execFileAsync("git",
77
+ // Against HEAD, modified only: a test THIS run created is staged at discovery, so
78
+ // its later STEP 5b edits show in a plain `git diff` and it would masquerade as a
79
+ // pre-existing sibling (observed: the report gate then demanded an execution no
80
+ // record could satisfy, and the agent un-shared the helpers to escape). Files
81
+ // added since HEAD are excluded; they are staged by their own tool calls.
82
+ ["diff", "-z", "--name-only", "--diff-filter=M", "HEAD"], {
73
83
  cwd: root,
74
84
  encoding: "utf8",
75
85
  });
@@ -1,14 +1,23 @@
1
1
  import { type InlineCallSite } from "./call-sites.js";
2
- import { type UtilsViolationKind } from "./allow.js";
2
+ import { type UtilsAdvisoryKind, type UtilsViolationKind } from "./allow.js";
3
+ import { type SingleImporterHelper } from "./importers.js";
3
4
  import { type HelperFamilySpec, type UtilsLanguageSpec } from "./language-spec.js";
4
5
  export interface UtilsViolation {
5
6
  kind: UtilsViolationKind;
6
7
  /** Absolute path of the utils file. */
7
8
  file: string;
9
+ /** The helper the finding is about — or, for a spec-level finding (assertion-loss,
10
+ * guard-removed), the spec's marker token: its basename with non-word characters
11
+ * as `_` (`orders.spec.ts` → `orders_spec_ts`), what the allow grammar accepts. */
8
12
  helper: string;
9
13
  line: number;
10
14
  detail: string;
11
15
  }
16
+ /** A violation-shaped finding whose kind may also be advisory-only. Only the
17
+ * `advisories` list carries these; `violations` never does. */
18
+ export type UtilsFinding = Omit<UtilsViolation, "kind"> & {
19
+ kind: UtilsViolationKind | UtilsAdvisoryKind;
20
+ };
12
21
  export interface UtilsVerifyResult {
13
22
  /** No unallowed violations and no malformed allow markers. */
14
23
  ok: boolean;
@@ -24,18 +33,23 @@ export interface UtilsVerifyResult {
24
33
  /** Findings that inform but never fail the pass. `scenario-name` lives here: a naming
25
34
  * heuristic is a quality signal, not a broken shared helper, and blocking a
26
35
  * customer's run on a false positive of it is the worse outcome. */
27
- advisories: UtilsViolation[];
36
+ advisories: UtilsFinding[];
28
37
  /** Allow-marker lines the grammar cannot read, per file. */
29
38
  malformedAllows: {
30
39
  file: string;
31
40
  line: string;
32
41
  }[];
33
42
  spec?: UtilsLanguageSpec;
34
- /** Which invariant set applied decided by the test type, see helperFamilyFor. */
35
- family: HelperFamilySpec;
43
+ /** The test type's invariant set — the FALLBACK for helpers that wrap no request;
44
+ * a helper that issues an SDK request is judged by the api rules regardless. See
45
+ * helperFamilyOf. */
46
+ fallbackFamily: HelperFamilySpec;
36
47
  /** Advisory (does not affect `ok`): inline request calls in sibling generated tests
37
48
  * that a utils helper already wraps — see findSiblingInlineCallSites. API family only. */
38
49
  inlineCallSites: InlineCallSite[];
50
+ /** Helpers imported by 0 or 1 delivered test (SKYR-4276 A6). Advisory: a first run
51
+ * seeds every helper single-use by design; the count is the trend to watch. */
52
+ singleImporters: SingleImporterHelper[];
39
53
  }
40
54
  export interface VerifyUtilsParams {
41
55
  testFile: string;
@@ -43,6 +57,13 @@ export interface VerifyUtilsParams {
43
57
  /** Selects the helper family (api vs browser). Absent → api. */
44
58
  testType?: string;
45
59
  cwd?: string;
60
+ /** The spec at the modularization hand-out (SKYR-4276 A7/A8). When present, the
61
+ * delivered spec plus its imported helpers must hold at least as many assertions,
62
+ * and a page-error guard that was there must still be. */
63
+ baseline?: {
64
+ assertions: number;
65
+ pageErrorGuard: boolean;
66
+ };
46
67
  }
47
68
  /**
48
69
  * Deterministic post-reuse check of the shared utils file(s) a spec depends on.
@@ -61,3 +82,32 @@ export interface VerifyUtilsParams {
61
82
  * except to count what the spec imports.
62
83
  */
63
84
  export declare function verifyUtils(params: VerifyUtilsParams): Promise<UtilsVerifyResult>;
85
+ /**
86
+ * Python only. The import form that works here is not the one that works in the
87
+ * customer's repo, and the difference is invisible in this run.
88
+ *
89
+ * pytest imports a test inside a package directory (one carrying `__init__.py`) from
90
+ * that package's first NON-package ancestor, so a bare `from SkyrampUtils import …`
91
+ * resolves only when the runner puts the test's own directory on `sys.path`. The
92
+ * Skyramp executor flattens its bundle and drops the `__init__.py` files, so it does;
93
+ * the customer running `pytest` from their repo root does not, and the test dies at
94
+ * collection with `ModuleNotFoundError`, taking the whole file with it. Measured on
95
+ * prefect (`tests/`, `tests/server/`, `tests/server/api/` all packages) and present in
96
+ * five of the six real Python repos in the eval corpus — the majority shape, not an
97
+ * edge case.
98
+ *
99
+ * The relative form is not simply the answer: under the flattened bundle it fails with
100
+ * `attempted relative import with no known parent package`. The two layouts want
101
+ * opposite forms, so a package directory must carry BOTH — the relative import first,
102
+ * falling back to the bare one:
103
+ *
104
+ * try:
105
+ * from .SkyrampUtils import create_order
106
+ * except ImportError: # flattened test bundle (no package context)
107
+ * from SkyrampUtils import create_order
108
+ *
109
+ * Verified in both layouts. Outside a package the bare form alone is correct, and the
110
+ * fallback would be noise, so it is required only where `__init__.py` sits beside the
111
+ * spec — which is exactly where pytest changes its rule.
112
+ */
113
+ export declare function importFormViolations(testFile: string, specContent: string, utilsFiles: string[]): UtilsViolation[];