@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
@@ -2,10 +2,12 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { logger } from "../logger.js";
4
4
  import { locateUtilsFiles } from "./locate.js";
5
- import { parseUtilsFile } from "./parse.js";
6
- import { findSiblingInlineCallSites, } from "./call-sites.js";
7
- import { parseUtilsAllows } from "./allow.js";
8
- import { helperFamilyFor, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
5
+ import { blankCommentsAndStrings, countAssertions, hasPageErrorGuard, parseUtilsFile, } from "./parse.js";
6
+ import { escapeRegExp } from "../regex.js";
7
+ import { findSiblingInlineCallSites, generatedSiblings, } from "./call-sites.js";
8
+ import { parseUtilsAllows, } from "./allow.js";
9
+ import { singleImporterHelpers, } from "./importers.js";
10
+ import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
9
11
  /**
10
12
  * Deterministic post-reuse check of the shared utils file(s) a spec depends on.
11
13
  *
@@ -42,10 +44,11 @@ export async function verifyUtils(params) {
42
44
  advisories: [],
43
45
  malformedAllows: [],
44
46
  spec,
45
- family,
47
+ fallbackFamily: family,
46
48
  inlineCallSites: [],
49
+ singleImporters: [],
47
50
  };
48
- if (!spec || utilsFiles.length === 0)
51
+ if (!spec)
49
52
  return result;
50
53
  let specContent = "";
51
54
  try {
@@ -54,8 +57,14 @@ export async function verifyUtils(params) {
54
57
  catch {
55
58
  /* the spec may be absent outside a run; import counting then reports 0 */
56
59
  }
60
+ if (spec.language === "python")
61
+ result.violations.push(...importFormViolations(params.testFile, specContent, utilsFiles));
57
62
  const allHelpers = [];
63
+ const helpersByFile = new Map();
64
+ const allAllows = [];
58
65
  const importedNames = new Set();
66
+ const importedByFile = new Map();
67
+ const helpersOfFile = new Map();
59
68
  // Route map spans every utils file: the reuse prompt allows the helpers to split
60
69
  // across files, and the same method+path in two of them is still one duplicate.
61
70
  const byRoute = new Map();
@@ -73,16 +82,36 @@ export async function verifyUtils(params) {
73
82
  });
74
83
  throw err; // the caller records a verify error, which fails OPEN — never a silent PASS
75
84
  }
76
- const helpers = parseUtilsFile(content, spec, (expr) => family.isAllowed(spec, expr));
85
+ const helpers = parseUtilsFile(content, spec);
86
+ for (const h of helpers) {
87
+ // An untyped parameter is an error under `noImplicitAny`; whether the host repo's
88
+ // tsconfig reaches the tests directory is its call, so this advises and never blocks.
89
+ if (h.untypedParams.length > 0)
90
+ result.advisories.push({
91
+ kind: "untyped-parameter",
92
+ file,
93
+ helper: h.name,
94
+ line: h.line,
95
+ // Names only — the remediation is stated once, in the advisory footer.
96
+ detail: h.untypedParams.map((p) => `\`${p}\``).join(", "),
97
+ });
98
+ }
77
99
  allHelpers.push(...helpers);
100
+ helpersByFile.set(file, helpers);
101
+ helpersOfFile.set(file, helpers);
78
102
  const { allows, malformed } = parseUtilsAllows(content, spec);
103
+ allAllows.push(...allows);
79
104
  result.helpers += helpers.length;
80
105
  result.malformedAllows.push(...malformed.map((line) => ({ file, line })));
81
106
  const stem = path.basename(file).replace(/\.[^.]+$/, "");
82
107
  const defined = new Set(helpers.map((h) => h.name));
108
+ const importedHere = new Set();
83
109
  for (const n of spec.importedNames(specContent, stem))
84
- if (defined.has(n))
110
+ if (defined.has(n)) {
85
111
  importedNames.add(n);
112
+ importedHere.add(n);
113
+ }
114
+ importedByFile.set(file, importedHere);
86
115
  // One marker documents ONE violation of that kind in that helper: a second,
87
116
  // undocumented scenario claim in the same helper is not covered by the first
88
117
  // decline. Markers are consumed in order.
@@ -91,7 +120,7 @@ export async function verifyUtils(params) {
91
120
  const k = `${a.kind}\0${a.helper}`;
92
121
  budget.set(k, (budget.get(k) ?? 0) + 1);
93
122
  }
94
- for (const v of violationsIn(helpers, file, family, byRoute)) {
123
+ for (const v of violationsIn(helpers, file, family, spec, byRoute)) {
95
124
  const k = `${v.kind}\0${v.helper}`;
96
125
  const left = budget.get(k) ?? 0;
97
126
  if (left > 0) {
@@ -107,6 +136,51 @@ export async function verifyUtils(params) {
107
136
  }
108
137
  }
109
138
  result.imported = importedNames.size;
139
+ // One directory walk feeds both advisories: every sibling is read exactly once.
140
+ let siblings = [];
141
+ try {
142
+ siblings = await generatedSiblings(params.testFile, spec, {
143
+ includeSelf: true,
144
+ });
145
+ result.singleImporters = await singleImporterHelpers(params.testFile, utilsFiles, helpersByFile, spec, siblings);
146
+ }
147
+ catch {
148
+ /* advisory only — never fails a verify */
149
+ }
150
+ if (params.baseline && specContent) {
151
+ // Delivered assertions live in the spec and in the helpers it imports — counted
152
+ // per utils FILE (a same-named helper in another module is not this import) and
153
+ // per CALL SITE on a comment/string-blanked copy of the spec: the baseline
154
+ // counted executions in source order, so a 2-assertion block extracted from 3
155
+ // repetitions delivers 6, and a commented-out call delivers nothing.
156
+ const blankedSpec = blankCommentsAndStrings(specContent, spec);
157
+ let inHelpers = 0;
158
+ // The guard may legitimately move INTO a shared helper — registered there,
159
+ // asserted in the spec, it still guards. Credited exactly like assertions.
160
+ let guardInHelpers = false;
161
+ for (const [file, imported] of importedByFile) {
162
+ for (const h of helpersOfFile.get(file) ?? []) {
163
+ if (!imported.has(h.name))
164
+ continue;
165
+ const calls = (blankedSpec.match(new RegExp(`\\b${escapeRegExp(h.name)}\\s*\\(`, "g")) ?? []).length;
166
+ if (calls === 0)
167
+ continue;
168
+ inHelpers += calls * h.assertions.length;
169
+ if (h.hasGuard)
170
+ guardInHelpers = true;
171
+ }
172
+ }
173
+ for (const v of baselineViolations(params.baseline, params.testFile, spec, specContent, inHelpers, guardInHelpers)) {
174
+ // A marker in any utils file documents an assertion-loss (never a removed
175
+ // guard). At most one loss finding exists per verify, so a matching marker's
176
+ // presence is the whole budget — nothing to consume.
177
+ if (v.kind === "assertion-loss" &&
178
+ allAllows.some((a) => a.kind === v.kind && a.helper === v.helper))
179
+ result.allowed.push(v);
180
+ else
181
+ result.violations.push(v);
182
+ }
183
+ }
110
184
  result.ok =
111
185
  result.violations.length === 0 && result.malformedAllows.length === 0;
112
186
  // Integration only — the same gate as the prompt rule. `contract`, `load` and an
@@ -115,7 +189,7 @@ export async function verifyUtils(params) {
115
189
  // not have taken.
116
190
  if ((params.testType ?? "").toLowerCase() === "integration") {
117
191
  try {
118
- result.inlineCallSites = await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec);
192
+ result.inlineCallSites = await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec, siblings);
119
193
  }
120
194
  catch {
121
195
  /* advisory only — never fails a verify */
@@ -123,9 +197,147 @@ export async function verifyUtils(params) {
123
197
  }
124
198
  return result;
125
199
  }
126
- function violationsIn(helpers, file, family, byRoute) {
200
+ /** The baseline (A7/A8) findings for a delivered spec — pure; marker handling is the
201
+ * caller's. Runs whether or not a utils file was located: deleting the utils file
202
+ * (with the moved assertions and guard in it) must not read as "nothing to verify". */
203
+ function baselineViolations(baseline, testFile, spec, specContent, inHelpers, guardInHelpers) {
204
+ const found = [];
205
+ const inSpec = countAssertions(specContent, spec);
206
+ const delivered = inSpec + inHelpers;
207
+ // Marker grammar takes an identifier: `orders.spec.ts` → `orders_spec_ts`.
208
+ const stem = path.basename(testFile).replace(/\W/g, "_");
209
+ if (delivered < baseline.assertions)
210
+ found.push({
211
+ kind: "assertion-loss",
212
+ file: testFile,
213
+ helper: stem,
214
+ line: 1,
215
+ detail: `${baseline.assertions} assertions at the modularization hand-out, ${delivered} delivered (${inSpec} in the test, ${inHelpers} in the helpers it imports and calls) — restore each removed check in the test body after the helper call it verifies, or document a deliberate consolidation with the allow marker`,
216
+ });
217
+ if (baseline.pageErrorGuard &&
218
+ !guardInHelpers &&
219
+ !hasPageErrorGuard(specContent, spec))
220
+ found.push({
221
+ kind: "guard-removed",
222
+ file: testFile,
223
+ helper: stem,
224
+ line: 1,
225
+ detail: `the page.on('pageerror', …) guard present at the modularization hand-out is gone — restore the registration (in this spec, or in a shared helper this spec imports and calls) and its final expect over the collected errors; it is never removed and cannot be documented away`,
226
+ });
227
+ return found;
228
+ }
229
+ /**
230
+ * Python only. The import form that works here is not the one that works in the
231
+ * customer's repo, and the difference is invisible in this run.
232
+ *
233
+ * pytest imports a test inside a package directory (one carrying `__init__.py`) from
234
+ * that package's first NON-package ancestor, so a bare `from SkyrampUtils import …`
235
+ * resolves only when the runner puts the test's own directory on `sys.path`. The
236
+ * Skyramp executor flattens its bundle and drops the `__init__.py` files, so it does;
237
+ * the customer running `pytest` from their repo root does not, and the test dies at
238
+ * collection with `ModuleNotFoundError`, taking the whole file with it. Measured on
239
+ * prefect (`tests/`, `tests/server/`, `tests/server/api/` all packages) and present in
240
+ * five of the six real Python repos in the eval corpus — the majority shape, not an
241
+ * edge case.
242
+ *
243
+ * The relative form is not simply the answer: under the flattened bundle it fails with
244
+ * `attempted relative import with no known parent package`. The two layouts want
245
+ * opposite forms, so a package directory must carry BOTH — the relative import first,
246
+ * falling back to the bare one:
247
+ *
248
+ * try:
249
+ * from .SkyrampUtils import create_order
250
+ * except ImportError: # flattened test bundle (no package context)
251
+ * from SkyrampUtils import create_order
252
+ *
253
+ * Verified in both layouts. Outside a package the bare form alone is correct, and the
254
+ * fallback would be noise, so it is required only where `__init__.py` sits beside the
255
+ * spec — which is exactly where pytest changes its rule.
256
+ */
257
+ export function importFormViolations(testFile, specContent, utilsFiles) {
258
+ const dir = path.dirname(testFile);
259
+ if (!fs.existsSync(path.join(dir, "__init__.py")))
260
+ return [];
261
+ const out = [];
262
+ // Docstrings and other triple-quoted text can carry an example import; blanked
263
+ // (length- and line-preserving) so an example is never read as a live import.
264
+ const lines = blankTripleQuoted(specContent).split("\n");
265
+ for (const file of utilsFiles) {
266
+ // Only a module beside the spec is addressable relatively.
267
+ if (path.dirname(file) !== dir)
268
+ continue;
269
+ const stem = path.basename(file).replace(/\.[^.]+$/, "");
270
+ const esc = escapeRegExp(stem);
271
+ // Both supported spellings: `from M import a, b` and `import M as alias`.
272
+ const bareFrom = new RegExp(`^\\s*from\\s+${esc}\\s+import\\b(.*)$`);
273
+ const bareModule = new RegExp(`^\\s*import\\s+${esc}\\b(.*)$`);
274
+ const relFrom = new RegExp(`^\\s*from\\s+\\.${esc}\\s+import\\b`);
275
+ const relModule = new RegExp(`^\\s*from\\s+\\.\\s+import\\s+${esc}\\b`);
276
+ lines.forEach((text, i) => {
277
+ const from = bareFrom.exec(text);
278
+ const mod = from ? null : bareModule.exec(text);
279
+ if (!from && !mod)
280
+ return;
281
+ // The bare form is legitimate ONLY as the `except ImportError` arm of a pair
282
+ // whose relative import comes first. Checked per import against what precedes
283
+ // IT — a relative import elsewhere in the file (or of other names) does not
284
+ // make this line a fallback, and a bare import above the pair still runs first
285
+ // and still fails collection.
286
+ const before = lines.slice(0, i);
287
+ const lastIndexWhere = (pred) => {
288
+ for (let k = before.length - 1; k >= 0; k--)
289
+ if (pred(before[k]))
290
+ return k;
291
+ return -1;
292
+ };
293
+ // ANY except clause counts, not just `except ImportError`. `except
294
+ // ModuleNotFoundError` (the error actually raised, and an ImportError
295
+ // subclass) and `except (ImportError, ModuleNotFoundError)` are both things a
296
+ // careful author writes, and this gate REFUSES THE REPORT — so the structural
297
+ // requirement below (a relative import first, this import indented inside the
298
+ // handler) is what carries the meaning. Narrowing on the exception name buys
299
+ // no detection and costs blocked runs on correct code.
300
+ const exceptAt = lastIndexWhere((l) => /^\s*except\b/.test(l));
301
+ // Either relative spelling satisfies either bare spelling: a pair mixing
302
+ // `from . import M` with `from M import x` is unusual but not wrong, and
303
+ // refusing it would again block correct code.
304
+ const relAt = lastIndexWhere((l) => relFrom.test(l) || relModule.test(l));
305
+ // Textual order is not enough: the bare import must sit INSIDE that except
306
+ // suite. `except ImportError: pass` followed by a top-level bare import passes
307
+ // an ordering test, still runs, and still dies at collection.
308
+ const indentOf = (l) => l.length - l.trimStart().length;
309
+ const insideExcept = exceptAt !== -1 &&
310
+ indentOf(text) > indentOf(lines[exceptAt]) &&
311
+ lines
312
+ .slice(exceptAt + 1, i)
313
+ .every((l) => l.trim() === "" || indentOf(l) > indentOf(lines[exceptAt]));
314
+ if (insideExcept && relAt !== -1 && relAt < exceptAt)
315
+ return;
316
+ const names = (from ? from[1] : (mod?.[1] ?? "")).trim();
317
+ const fixed = from
318
+ ? `from .${stem} import ${names}`
319
+ : `from . import ${stem}${names ? " " + names : ""}`;
320
+ out.push({
321
+ kind: "import-form",
322
+ file: testFile,
323
+ helper: stem,
324
+ line: i + 1,
325
+ detail: `\`${text.trim()}\` — this directory is a package (\`__init__.py\`), so pytest resolves imports from the package root and will NOT find \`${stem}\` on sys.path when the customer runs it; the test dies at collection. ` +
326
+ `Replace it with the pair that works in both layouts: \`try:\` / \` ${fixed}\` / \`except ImportError:\` / \` ${text.trim()}\``,
327
+ });
328
+ });
329
+ }
330
+ return out;
331
+ }
332
+ /** Triple-quoted regions blanked, preserving length and line count, so an example
333
+ * import inside a docstring is never read as a live one. */
334
+ function blankTripleQuoted(src) {
335
+ return src.replace(/("""|''')[\s\S]*?\1/g, (m) => m.replace(/[^\n]/g, " "));
336
+ }
337
+ function violationsIn(helpers, file, fallback, spec, byRoute) {
127
338
  const out = [];
128
339
  for (const h of helpers) {
340
+ const family = helperFamilyOf(h, fallback);
129
341
  if (family.routeDuplicates && h.method && h.normalizedPath) {
130
342
  const key = `${h.method} ${h.normalizedPath}`;
131
343
  const first = byRoute.get(key);
@@ -143,7 +355,7 @@ function violationsIn(helpers, file, family, byRoute) {
143
355
  }
144
356
  }
145
357
  for (const a of h.assertions) {
146
- if (!a.allowed) {
358
+ if (!family.isAllowed(spec, a.text)) {
147
359
  out.push({
148
360
  kind: "body-assertion",
149
361
  file,