@skyramp/mcp 0.4.0 → 0.4.1-rc.2

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 (83) hide show
  1. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +2 -1
  2. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
  3. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
  4. package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
  5. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
  7. package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
  8. package/build/prompts/testbot/testbot-prompts.js +19 -12
  9. package/build/recommendation/answers.d.ts +11 -7
  10. package/build/recommendation/answers.js +14 -10
  11. package/build/recommendation/pullRequestText.d.ts +18 -0
  12. package/build/recommendation/pullRequestText.js +31 -0
  13. package/build/recommendation/registerPlan.d.ts +9 -1
  14. package/build/recommendation/registerPlan.js +8 -1
  15. package/build/recommendation/runVerifiers.js +6 -0
  16. package/build/recommendation/types.d.ts +58 -0
  17. package/build/recommendation/verifierContracts.d.ts +108 -13
  18. package/build/recommendation/verifierContracts.js +147 -29
  19. package/build/recommendation/verifiers/coverage.d.ts +10 -0
  20. package/build/recommendation/verifiers/coverage.js +144 -22
  21. package/build/recommendation/verifiers/defects.d.ts +9 -0
  22. package/build/recommendation/verifiers/defects.js +117 -0
  23. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
  24. package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
  25. package/build/recommendation/verifiers/existingCoverage.js +53 -0
  26. package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
  27. package/build/recommendation/verifiers/expectedValueSourced.js +246 -0
  28. package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
  29. package/build/recommendation/verifiers/issueTraceability.js +197 -0
  30. package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
  31. package/build/recommendation/verifiers/requirementSourced.js +168 -0
  32. package/build/services/TestGenerationService.js +3 -1
  33. package/build/tools/code-refactor/codeReuseTool.js +1 -1
  34. package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
  35. package/build/tools/code-refactor/reuse-state.d.ts +85 -7
  36. package/build/tools/code-refactor/reuse-state.js +239 -34
  37. package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
  38. package/build/tools/code-refactor/utils-verify-gates.js +103 -11
  39. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
  40. package/build/tools/submitReportTool.js +276 -39
  41. package/build/tools/test-management/actionsTool.js +5 -0
  42. package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
  43. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  44. package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
  45. package/build/tools/test-management/registerTestPlanTool.js +105 -16
  46. package/build/types/ReuseOutcome.d.ts +73 -7
  47. package/build/types/TestAnalysis.d.ts +6 -0
  48. package/build/types/TestbotReport.d.ts +22 -4
  49. package/build/utils/AnalysisStateManager.d.ts +7 -1
  50. package/build/utils/AnalysisStateManager.js +5 -1
  51. package/build/utils/assertion-verify/api-shared-lints.js +118 -11
  52. package/build/utils/assertion-verify/format.js +2 -2
  53. package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
  54. package/build/utils/assertion-verify/helper-imports.js +119 -27
  55. package/build/utils/assertion-verify/lint-types.d.ts +31 -2
  56. package/build/utils/assertion-verify/lint-types.js +66 -0
  57. package/build/utils/assertion-verify/metrics.d.ts +13 -0
  58. package/build/utils/assertion-verify/metrics.js +16 -0
  59. package/build/utils/assertion-verify/verify.d.ts +11 -6
  60. package/build/utils/assertion-verify/verify.js +56 -15
  61. package/build/utils/canonicalJson.d.ts +11 -0
  62. package/build/utils/canonicalJson.js +17 -0
  63. package/build/utils/utils-verify/action-key.d.ts +27 -0
  64. package/build/utils/utils-verify/action-key.js +292 -0
  65. package/build/utils/utils-verify/allow.d.ts +8 -1
  66. package/build/utils/utils-verify/allow.js +14 -1
  67. package/build/utils/utils-verify/call-sites.d.ts +76 -8
  68. package/build/utils/utils-verify/call-sites.js +256 -70
  69. package/build/utils/utils-verify/language-spec.d.ts +3 -2
  70. package/build/utils/utils-verify/parse.d.ts +22 -3
  71. package/build/utils/utils-verify/parse.js +123 -52
  72. package/build/utils/utils-verify/verify.d.ts +33 -3
  73. package/build/utils/utils-verify/verify.js +126 -12
  74. package/build/utils/workspaceAuth.d.ts +59 -19
  75. package/build/utils/workspaceAuth.js +228 -31
  76. package/package.json +1 -1
  77. package/plugin/prompts/generate-tests/execution-plan.md +3 -3
  78. package/plugin/prompts/generate-tests/generation.md +1 -0
  79. package/plugin/prompts/plan-tests.md +41 -15
  80. package/plugin/prompts/testbot-task1.md +3 -9
  81. package/build/prompts/testbot/planDeclarations.d.ts +0 -6
  82. package/build/prompts/testbot/planDeclarations.js +0 -9
  83. package/plugin/prompts/declaring-a-plan.md +0 -20
@@ -10,9 +10,12 @@ import { verifyReuse, } from "../../utils/pom-verify/index.js";
10
10
  import { buildSkipped, findUnparsableKeptInline, flaggedFrom, mergeFlagged, verificationFrom, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./reuse-outcome.js";
11
11
  import { verifyUtils, realpath, utilsSpecFor, countAssertions, hasPageErrorGuard, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
12
12
  import { detectGitRoot } from "../../utils/gitStaging.js";
13
+ import { formatUtilsVerifyText } from "./utils-verify-gates.js";
14
+ import { composeVerifyText, infraGateFailure, zeroReuseGateFailure, } from "./verify-gates.js";
13
15
  const execFileAsync = promisify(execFile);
14
16
  import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
15
17
  import { parsePomCatalog, findMissedReuse, } from "../../utils/pom-catalog-parse.js";
18
+ import { ProgrammingLanguage } from "../../types/TestTypes.js";
16
19
  /**
17
20
  * Persistence for the server-derived POM-reuse outcome, keyed by test-file
18
21
  * basename (what `newTestsCreated[].fileName` carries, so
@@ -75,7 +78,7 @@ async function readReuseRecord(testFile, explicitStateFile) {
75
78
  *
76
79
  * The identity is written here and not only by {@link recordVerifyOutcome} because
77
80
  * the verify pass is a call the agent decides whether to make. When it skips it,
78
- * a record holding the candidate count alone makes `rederiveReuseOutcome` bail out,
81
+ * a record holding the candidate count alone makes `rederiveReuse` bail out,
79
82
  * and the report states "N candidate POM files detected" with no reuse numbers —
80
83
  * indistinguishable from a run where reuse was measured and simply not summarized.
81
84
  * Recording it unconditionally is what makes the zero-reuse case reportable without
@@ -139,9 +142,11 @@ export async function recordVerifyOutcome(testFile, r, gateFired, language, expl
139
142
  * NOT terminal. A PASS is always reachable in one edit plus one call (substitute the
140
143
  * members, or document each candidate with a `// kept inline:` marker, which the gate
141
144
  * accepts), and an agent that resolves neither can still walk past these tools to
142
- * `skyramp_submit_report`, which does not block and reports the honest zero. So the
143
- * worst case degrades to the reporting half rather than to a run that produces
144
- * nothing which is what a bound would have been protecting against.
145
+ * `skyramp_submit_report`. That tool refuses a blocking verdict on the delivered
146
+ * files too, but its refusal is BOUNDED (`REUSE_SUBMIT_MAX_REFUSALS`): past the bound
147
+ * it accepts the report with the fault recorded in the row. So the worst case still
148
+ * degrades to the reporting half rather than to a run that produces nothing — which
149
+ * is what a bound here would have been protecting against.
145
150
  */
146
151
  export async function pendingReuseVerification(testFile, explicitStateFile) {
147
152
  try {
@@ -246,9 +251,33 @@ export function samePath(a, b) {
246
251
  export function canonPath(p) {
247
252
  return canon(p);
248
253
  }
249
- /** The basename label a report row shows for a set of utils files. */
254
+ /**
255
+ * The label a report row shows for a set of utils files: each file's path relative
256
+ * to its repository root, posix-separated, joined with `, `.
257
+ *
258
+ * Repository-relative, never absolute — an absolute path carries the runner's
259
+ * temporary directory and means nothing to a customer. Not a basename either: a
260
+ * fullstack delivery writes one module per test directory under the same
261
+ * conventional name, and two rows labelled `skyrampUtils.ts` cannot be told apart
262
+ * (a renderer keyed on the label then merges them into one aggregate). Outside a
263
+ * repository, or for a file the root does not contain, the basename stands in.
264
+ */
250
265
  export function utilsFileLabel(files) {
251
- return files.map((f) => path.basename(f)).join(", ");
266
+ return files.map(repoRelativeLabel).join(", ");
267
+ }
268
+ function repoRelativeLabel(file) {
269
+ const root = detectGitRoot(file);
270
+ if (!root)
271
+ return path.basename(file);
272
+ // Both sides canonical: git reports the REAL root, and a path through a symlink
273
+ // (macOS /tmp → /private/tmp) would otherwise relativise to `../../…`.
274
+ const rel = path.relative(canon(root), canon(file));
275
+ // Outside the root means a leading `..` SEGMENT — not any name starting with two
276
+ // dots (`..generated/SkyrampUtils.py` is a valid in-repository path).
277
+ const outside = rel === ".." || rel.startsWith(`..${path.sep}`);
278
+ if (!rel || outside || path.isAbsolute(rel))
279
+ return path.basename(file);
280
+ return rel.split(path.sep).join("/");
252
281
  }
253
282
  function canon(p) {
254
283
  try {
@@ -261,11 +290,15 @@ function canon(p) {
261
290
  /** Verdict of one utils verify pass. The verdict alone is stored — counts and the
262
291
  * file list are re-derived from the delivered files at report time. */
263
292
  export async function recordUtilsVerifyOutcome(testFile, r, language, framework, testType, explicitStateFile) {
264
- const verification = r.utilsFiles.length === 0
265
- ? HelperVerificationOutcome.NoUtilsFile
266
- : r.ok
267
- ? HelperVerificationOutcome.Passed
268
- : HelperVerificationOutcome.Failed;
293
+ // `ok` first: a baseline violation is a failed verify with or without a module
294
+ // beside the spec (the live text says FAILED), and recording NoUtilsFile there
295
+ // cleared the debt, let execution proceed, and left the fault to be refused first
296
+ // at submit — the recorded verdict was a stale positive.
297
+ const verification = !r.ok
298
+ ? HelperVerificationOutcome.Failed
299
+ : r.utilsFiles.length === 0
300
+ ? HelperVerificationOutcome.NoUtilsFile
301
+ : HelperVerificationOutcome.Passed;
269
302
  return updateReuseOutcome(testFile, (existing) => {
270
303
  const mine = owns(existing, testFile);
271
304
  const prior = mine ? existing.utils : undefined;
@@ -306,7 +339,9 @@ async function runStateWritable(explicitStateFile) {
306
339
  /** `skyramp_modularization` was called for this spec. Ownership on the write, like
307
340
  * the utils debt: a same-basename spec elsewhere does not inherit the mark. */
308
341
  export async function recordModularized(testFile, explicitStateFile) {
309
- // The spec as handed out after the enhancer, before any code moves. Taken once;
342
+ // The spec as it stands at the modularization call, before any code moves. For UI
343
+ // that is after the enhancer (enhance → modularize); for integration it is before
344
+ // it (modularize → reuse → verify → enhance), i.e. the generated spec. Taken once;
310
345
  // a second modularization call must not overwrite the true "before".
311
346
  let baseline;
312
347
  const spec = utilsSpecForFile(testFile);
@@ -626,40 +661,127 @@ function reuseRan(record) {
626
661
  record.candidatesDetected !== undefined ||
627
662
  record.verification !== undefined);
628
663
  }
664
+ /**
665
+ * How many times skyramp_submit_report refuses a report over one spec's blocking
666
+ * verdict before accepting it with the fault recorded in the row.
667
+ *
668
+ * The refusal is a repair loop, and a repair loop at the last step of a run needs a
669
+ * terminal state (SKYR-4059: an unbounded loop destroyed verified work). Without one,
670
+ * an agent that cannot reach a pass — out of turns, or a `guard-removed` it cannot
671
+ * restore, which no marker documents away — delivers no report at all, and a
672
+ * customer with no pull request is a worse outcome than one with a disclosed fault.
673
+ * Two refusals cover the common case, where the repair is one edit and one verify
674
+ * call away; the third call ships the row with `verificationFailures` set.
675
+ */
676
+ export const REUSE_SUBMIT_MAX_REFUSALS = 2;
629
677
  /**
630
678
  * Re-derive the shared-helper outcome from the delivered files. Everything is
631
679
  * measured now: the utils file is located by header, its invariants re-checked, the
632
680
  * imports re-counted — the recorded verdict is not consulted, so a utils file
633
- * rewritten after the verify pass is described as it stands. Returns `undefined`
634
- * (omit `helpers`) when no utils file exists for the spec.
681
+ * rewritten after the verify pass is described as it stands. Omits `helpers` when
682
+ * no utils file exists for the spec; the top-level un-extracted duplicate count is
683
+ * reported either way.
684
+ *
685
+ * The blocking verdict is `!r.ok` — exactly what the live pass fails on: the five
686
+ * blocking kinds after allow-marker filtering, plus a malformed allow marker. It is
687
+ * raised whether or not a utils file was located: a spec that drops below its
688
+ * modularization baseline is below it with or without a module beside it.
635
689
  */
636
690
  async function rederiveHelperOutcome(record) {
637
691
  const { testFilePath, language } = record;
638
692
  if (!testFilePath || !language)
639
- return undefined;
693
+ return { outcome: undefined };
640
694
  const r = await verifyUtils({
641
695
  testFile: testFilePath,
642
696
  language,
643
697
  testType: record.utils?.testType,
644
698
  baseline: record.baseline,
645
699
  });
700
+ const failures = r.ok ? [] : verificationFailures(r);
701
+ const blocking = r.ok
702
+ ? undefined
703
+ : {
704
+ file: testFilePath,
705
+ failures,
706
+ // The live failure text: file and line per finding, the paste-ready allow
707
+ // line for each markerable kind, and the guard rule — minus this pass's own
708
+ // call-to-action and round budget, which the refusal supplies.
709
+ detail: formatUtilsVerifyText(r, { embedded: true }),
710
+ verifyCall: utilsVerifyCall(record, testFilePath),
711
+ };
712
+ // Counted before the no-utils-file return: the run that extracted nothing is the
713
+ // one this count exists for, and it needs no module to be true.
714
+ const unextracted = r.unextractedDuplicates.length > 0
715
+ ? {
716
+ unextractedDuplicateSites: r.unextractedDuplicates.reduce((n, d) => n + d.sites.length, 0),
717
+ }
718
+ : {};
646
719
  if (r.utilsFiles.length === 0)
647
- return {};
648
- const lost = r.violations.some((v) => v.kind === "assertion-loss" || v.kind === "guard-removed");
720
+ return { outcome: unextracted, blocking };
721
+ // Two faults, two fields: a deleted guard is not a lost assertion, and a reader
722
+ // acting on the row must not be sent after the wrong one. The count is MEASURED:
723
+ // a `reuse-verify: allow` marker documents a loss and clears the verdict, it does
724
+ // not put the assertions back, so an allowed loss still reads as not preserved.
725
+ // (A guard is never allowable, so `violations` alone is complete for it.)
726
+ const assertionLoss = [...r.violations, ...r.allowed].some((v) => v.kind === "assertion-loss");
727
+ const guardRemoved = r.violations.some((v) => v.kind === "guard-removed");
649
728
  return {
650
- helpers: {
651
- utilsFile: utilsFileLabel(r.utilsFiles),
652
- helpersImported: r.imported,
653
- ...(record.baseline ? { assertionsPreserved: !lost } : {}),
654
- ...(r.inlineCallSites.length > 0
655
- ? { siblingInlineCallSites: r.inlineCallSites.length }
656
- : {}),
657
- verification: r.ok
658
- ? HelperVerificationOutcome.Passed
659
- : HelperVerificationOutcome.Failed,
729
+ outcome: {
730
+ ...unextracted,
731
+ helpers: {
732
+ utilsFile: utilsFileLabel(r.utilsFiles),
733
+ helpersImported: r.imported,
734
+ helperNames: r.importedNames,
735
+ ...(record.baseline ? { assertionsPreserved: !assertionLoss } : {}),
736
+ // Absent only with no baseline, like `assertionsPreserved`: a baseline that
737
+ // recorded no guard had nothing to lose, so its guard is preserved.
738
+ ...(record.baseline
739
+ ? {
740
+ pageErrorGuardPreserved: record.baseline.pageErrorGuard
741
+ ? !guardRemoved
742
+ : true,
743
+ }
744
+ : {}),
745
+ ...(r.inlineCallSites.length > 0
746
+ ? { siblingInlineCallSites: r.inlineCallSites.length }
747
+ : {}),
748
+ verification: r.ok
749
+ ? HelperVerificationOutcome.Passed
750
+ : HelperVerificationOutcome.Failed,
751
+ ...(r.ok ? {} : { verificationFailures: failures }),
752
+ },
660
753
  },
754
+ blocking,
661
755
  };
662
756
  }
757
+ /**
758
+ * The reason behind a failed shared-helper verdict, one entry per blocking kind in
759
+ * the order the verifier found them. The verify pass had these in hand and dropped
760
+ * them after computing `ok`; without them a `failed` row is a verdict nobody can act
761
+ * on. The detail text is the verifier's own — for an assertion loss it carries the
762
+ * counts (baseline, delivered, in the test, in the helpers) that decided the verdict.
763
+ */
764
+ export function verificationFailures(r) {
765
+ const byKind = new Map();
766
+ for (const v of r.violations) {
767
+ const entry = byKind.get(v.kind) ?? { count: 0, details: [] };
768
+ entry.count++;
769
+ entry.details.push(v.detail);
770
+ byKind.set(v.kind, entry);
771
+ }
772
+ // A marker the grammar cannot read fails the verify on its own (the decline it
773
+ // documents would be dropped silently), so it is a reason like any other.
774
+ if (r.malformedAllows.length > 0)
775
+ byKind.set("malformed-allow-marker", {
776
+ count: r.malformedAllows.length,
777
+ details: r.malformedAllows.map((m) => `${path.basename(m.file)}: ${m.line}`),
778
+ });
779
+ return [...byKind].map(([kind, { count, details }]) => ({
780
+ kind,
781
+ count,
782
+ detail: details.join("; "),
783
+ }));
784
+ }
663
785
  /**
664
786
  * Raw-locator coverage for the delivered spec, or `undefined` when it cannot be
665
787
  * established.
@@ -715,11 +837,18 @@ async function coverageFrom(testFilePath, specContent) {
715
837
  *
716
838
  * Returns the recorded outcome minus internals when re-derivation is impossible
717
839
  * (no path recorded — now only the no-POM-layer path, which has no spec to measure
718
- * and no candidates to contrast against), and `undefined` when it fails outright.
840
+ * and no candidates to contrast against), and no outcome when it fails outright.
719
841
  * Failing closed matters: falling back to the recorded counts is exactly the false
720
842
  * claim this exists to prevent.
843
+ *
844
+ * Beside the outcome comes the blocking verdict the delivered files earn under the
845
+ * live check — see {@link ReuseBlockingVerdict}. One measurement feeds both: the row
846
+ * the report publishes and the refusal `skyramp_submit_report` returns. That half
847
+ * fails OPEN like every reuse check: a re-derivation that throws yields no outcome
848
+ * and no verdict. A verifier that cannot run is not evidence against the file, and
849
+ * refusing a report on a malfunction would deliver nothing to the customer.
721
850
  */
722
- export async function rederiveReuseOutcome(record) {
851
+ export async function rederiveReuse(record) {
723
852
  const { candidatesDetected, verification, flagged, testFilePath, language } = record;
724
853
  // A SkyrampUtils-path record has nothing for the POM verifier to measure; its
725
854
  // outcome is re-derived from the utils file instead.
@@ -729,7 +858,7 @@ export async function rederiveReuseOutcome(record) {
729
858
  }
730
859
  catch (err) {
731
860
  logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: testFilePath, error: String(err) });
732
- return undefined;
861
+ return { outcome: undefined };
733
862
  }
734
863
  }
735
864
  // No spec identity was ever recorded, so there is nothing to measure — only the
@@ -737,10 +866,11 @@ export async function rederiveReuseOutcome(record) {
737
866
  // no-POM-layer path; a run that detected candidates always records the identity
738
867
  // (see recordCandidates), precisely so the zero-reuse case does not land here.
739
868
  if (!testFilePath || !language)
740
- return { candidatesDetected, verification };
869
+ return { outcome: { candidatesDetected, verification } };
741
870
  try {
742
871
  const r = await verifyReuse(testFilePath, language);
743
872
  const specContent = await readFile(testFilePath, "utf8");
873
+ const blocking = await pomBlockingVerdict(testFilePath, language, r);
744
874
  // Diagnose the file the report is actually built from. A marker the zero-reuse
745
875
  // gate accepts but this cannot parse satisfies the escape hatch while its
746
876
  // decline never reaches the report — silent signal loss is the failure class
@@ -753,7 +883,7 @@ export async function rederiveReuseOutcome(record) {
753
883
  const skipped = buildSkipped(specContent, flagged);
754
884
  const coverage = await coverageFrom(testFilePath, specContent);
755
885
  const reuseHappened = r.reusedCalls > 0;
756
- return {
886
+ const outcome = {
757
887
  candidatesDetected,
758
888
  callsReused: r.reusedCalls,
759
889
  // Only a POM call can be checked, so `checkedCalls` is a subset of
@@ -778,13 +908,88 @@ export async function rederiveReuseOutcome(record) {
778
908
  missedReuse: coverage && coverage.missed.length > 0 ? coverage.missed : undefined,
779
909
  // A PASSED verdict describes a spec that no longer exists once reuse is gone,
780
910
  // and pairing it with a zero count reads as though the empty result had been
781
- // blessed.
782
- verification: reuseHappened ? verification : undefined,
911
+ // blessed. And a spec that fails the live check NOW is failed, whatever the
912
+ // recorded verdict says that is the row that ships past the refusal bound.
913
+ verification: blocking
914
+ ? ReuseVerificationOutcome.Failed
915
+ : reuseHappened
916
+ ? verification
917
+ : undefined,
918
+ ...(blocking ? { verificationFailures: blocking.failures } : {}),
783
919
  skipped: skipped.length > 0 ? skipped : undefined,
784
920
  };
921
+ return { outcome, blocking };
785
922
  }
786
923
  catch (err) {
787
924
  logger.warning("Could not re-derive the POM-reuse outcome from the delivered spec — omitting the reuse summary rather than reporting counts that may no longer hold", { testFile: testFilePath, error: String(err) });
925
+ return { outcome: undefined };
926
+ }
927
+ }
928
+ /**
929
+ * The page-object path's blocking verdict on the delivered spec: the live verify
930
+ * chain — POM calls resolved against source, then the infra gate, then the
931
+ * zero-reuse gate — re-run as it stands. The Ghost instance is the zero-reuse gate:
932
+ * a full-file rewrite after verification leaves raw selectors that match the page
933
+ * objects detection found, with no `// kept inline:` decline.
934
+ *
935
+ * Every POM record with a spec identity is measured, not only one that recorded a
936
+ * candidate count: selector-grep discovery withholds `candidatesDetected` (the count
937
+ * would claim a page-object layer that app markup only resembles) yet still hands
938
+ * the POMs to the reuse flow, and a member that no longer exists on such a spec fails
939
+ * the live verify too. The zero-reuse gate limits itself to glob discovery
940
+ * internally, so running the chain here adds no false zero-reuse refusal. The spec
941
+ * identity is written only by the POM-aware verify branch, whose predicate requires
942
+ * Playwright — which is why the framework can be stated here rather than read from a
943
+ * record that never held it. Fails open when a gate throws — a gate that cannot run
944
+ * is no evidence.
945
+ */
946
+ async function pomBlockingVerdict(testFile, language, r) {
947
+ // The record's language was written from the reuse tool's validated `language`
948
+ // param, but the record type keeps it a string: a value outside the enum means a
949
+ // check that cannot run, and a check that cannot run does not block.
950
+ const known = Object.values(ProgrammingLanguage).includes(language);
951
+ if (!known) {
952
+ logger.warning("Report-time POM gate skipped — the record's language is not one the reuse tool accepts", { testFile, language });
788
953
  return undefined;
789
954
  }
955
+ const target = {
956
+ testFile,
957
+ language: language,
958
+ framework: "playwright",
959
+ };
960
+ let infra;
961
+ let zero;
962
+ try {
963
+ infra = await infraGateFailure(target, r);
964
+ zero =
965
+ infra === undefined ? await zeroReuseGateFailure(target, r) : undefined;
966
+ }
967
+ catch (err) {
968
+ logger.warning("Report-time POM gate threw — not refusing the report on a check that could not run", { testFile, error: String(err) });
969
+ return undefined;
970
+ }
971
+ const gate = infra ?? zero;
972
+ if (gate === undefined && r.ok)
973
+ return undefined;
974
+ const failures = [];
975
+ if (infra !== undefined)
976
+ failures.push({ kind: "skyramp-infra-missing", count: 1, detail: infra });
977
+ if (zero !== undefined)
978
+ failures.push({ kind: "zero-reuse", count: 1, detail: zero });
979
+ if (r.parseError)
980
+ failures.push({ kind: "parse-error", count: 1, detail: r.parseError });
981
+ if (r.violations.length > 0)
982
+ failures.push({
983
+ kind: "pom-member-missing",
984
+ count: r.violations.length,
985
+ detail: r.violations
986
+ .map((v) => `${v.binding}.${v.member} — not found in ${v.searched.join(", ")}`)
987
+ .join("; "),
988
+ });
989
+ return {
990
+ file: testFile,
991
+ failures,
992
+ detail: composeVerifyText(gate, r),
993
+ verifyCall: `skyramp_reuse_code with { testFile: "${testFile}", language: "${language}", framework: "playwright", verify: true }`,
994
+ };
790
995
  }
@@ -22,5 +22,10 @@ export interface UtilsVerifyContext {
22
22
  /** Whether the verdict reached run state. When it did not, the checkpoints fail
23
23
  * open on the unwritable state file — the agent is told rather than left to loop. */
24
24
  recorded?: boolean;
25
+ /** The text is quoted inside skyramp_submit_report's refusal, which supplies its
26
+ * own call-to-action and its own round bound. The findings and the paste-ready
27
+ * allow lines stay; this pass's "call skyramp_reuse_code again" and its
28
+ * `UTILS_VERIFY_MAX_ROUNDS` budget — which no counter at submit tracks — go. */
29
+ embedded?: boolean;
25
30
  }
26
31
  export declare function formatUtilsVerifyText(r: UtilsVerifyResult, ctx?: UtilsVerifyContext): string;
@@ -1,5 +1,6 @@
1
1
  import * as path from "path";
2
- import { allowMarkerLine, MARKERABLE_UTILS_KINDS, HELPER_FAMILIES, } from "../../utils/utils-verify/index.js";
2
+ import { allowMarkerLine, describeAssertionCount, MARKERABLE_UTILS_KINDS, HELPER_FAMILIES, } from "../../utils/utils-verify/index.js";
3
+ import { utilsFileLabel } from "./reuse-state.js";
3
4
  /** How many verify rounds the agent may spend before it must document what remains.
4
5
  * SKYR-4059: an unbounded repair loop made six execution attempts against a cap of
5
6
  * two and destroyed verified reuse in between. The bound is stated in the text the
@@ -23,17 +24,21 @@ export function formatUtilsVerifyText(r, ctx = {}) {
23
24
  if (r.utilsFiles.length === 0 && r.violations.length === 0) {
24
25
  return ("VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify. " +
25
26
  "(If you did create one, it carries no `Generated by Skyramp on …` header line, so it cannot be recognised or staged — add the header and re-verify.)" +
26
- tail);
27
+ tail +
28
+ unextractedDuplicatesAdvisory(r));
27
29
  }
28
30
  const files = r.utilsFiles.map((f) => show(f, r)).join(", ");
29
31
  if (r.ok) {
30
32
  const allowed = r.allowed.length > 0
31
33
  ? ` ${r.allowed.length} documented decline${r.allowed.length === 1 ? "" : "s"}.`
32
34
  : "";
33
- return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed} ` +
35
+ return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed}` +
36
+ `${assertionCountNote(r)} ` +
34
37
  `${ctx.staged && ctx.staged.length > 0 ? "The utils file is staged for the output commit. " : ""}Continue.` +
35
38
  tail +
36
39
  inlineCallSitesAdvisory(r) +
40
+ unextractedDuplicatesAdvisory(r) +
41
+ duplicateActionAdvisory(r) +
37
42
  singleImporterAdvisory(r));
38
43
  }
39
44
  const lines = [];
@@ -47,19 +52,26 @@ export function formatUtilsVerifyText(r, ctx = {}) {
47
52
  // clause reads as a continuation of the api one for mixed files.
48
53
  `Fix each shared-helper violation in the utils file (${r.fallbackFamily.family === "browser"
49
54
  ? `${HELPER_FAMILIES.api.fixRule}; ${HELPER_FAMILIES.browser.fixRule}`
50
- : HELPER_FAMILIES.api.fixRule}), update any call site whose signature changed, then call skyramp_reuse_code again with the SAME arguments and \`verify: true\`. Re-verify after every fix pass; finish only on PASSED.`);
55
+ : HELPER_FAMILIES.api.fixRule}), update any call site whose signature changed${ctx.embedded
56
+ ? "."
57
+ : ", then call skyramp_reuse_code again with the SAME arguments and `verify: true`. Re-verify after every fix pass; finish only on PASSED."}`);
51
58
  // Marker guidance only for kinds a marker can document: a guard-removed-only
52
59
  // failure must not be told "every deliberate violation may be documented".
53
- const markerable = r.violations.filter((v) => v.kind !== "guard-removed" && v.kind !== "import-form");
60
+ const markerable = r.violations.filter((v) => v.kind !== "guard-removed" &&
61
+ v.kind !== "import-form" &&
62
+ v.kind !== "helper-redefined");
54
63
  if (markerable.length > 0) {
55
64
  lines.push(`A violation that is deliberate may be documented instead of fixed. Copy the matching line below into the utils file as a comment, replacing only the <reason…> part — nothing else satisfies the gate:`);
56
65
  if (spec)
57
66
  for (const v of markerable)
58
67
  lines.push(` ${allowMarkerLine(spec, v.kind, v.helper)}`);
59
- lines.push(`Do not spend more than ${UTILS_VERIFY_MAX_ROUNDS} verify rounds on this file: if violations remain after round ${UTILS_VERIFY_MAX_ROUNDS}, document each remaining one with its line above and re-verify once more.`);
68
+ if (!ctx.embedded)
69
+ lines.push(`Do not spend more than ${UTILS_VERIFY_MAX_ROUNDS} verify rounds on this file: if violations remain after round ${UTILS_VERIFY_MAX_ROUNDS}, document each remaining one with its line above and re-verify once more.`);
60
70
  }
61
71
  if (r.violations.some((v) => v.kind === "import-form"))
62
72
  lines.push("An `import-form` violation is in the TEST file, not the utils file: replace that import with the try/except pair shown on the line above, then re-verify. Nothing in the utils file needs to change for it.");
73
+ if (r.violations.some((v) => v.kind === "helper-redefined"))
74
+ lines.push("A helper defined twice in one file cannot be documented away — the module does not load (TypeScript) or the second definition silently replaces the first (Python). Keep ONE definition, give every parameter the other had a default, update the call sites, then re-verify.");
63
75
  if (r.violations.some((v) => v.kind === "guard-removed"))
64
76
  lines.push("A removed page-error guard cannot be documented away — restore the registration (in the spec, or in a shared helper the spec imports and calls) and its final expect, then re-verify.");
65
77
  }
@@ -71,8 +83,41 @@ export function formatUtilsVerifyText(r, ctx = {}) {
71
83
  return (lines.join("\n") +
72
84
  tail +
73
85
  inlineCallSitesAdvisory(r) +
86
+ unextractedDuplicatesAdvisory(r) +
87
+ duplicateActionAdvisory(r) +
74
88
  singleImporterAdvisory(r));
75
89
  }
90
+ /** A PASSED that ran the baseline assertion check says what that check measured. It
91
+ * is a count: a swapped assertion (one removed, one added) holds the count, so the
92
+ * line names the pass that compares subjects rather than letting "PASSED" claim it.
93
+ * Empty when no baseline exists — an unmodularized spec never had one. Not appended
94
+ * to the no-utils-file PASSED, whose text says nothing was verified. */
95
+ function assertionCountNote(r) {
96
+ const c = r.assertionCount;
97
+ if (!c)
98
+ return "";
99
+ // The verdict is read from the numbers, not from `ok`: a PASSED can carry a
100
+ // documented assertion-loss (the marker moves it to `allowed`), and a rise
101
+ // (enhancement after the hand-out) is not "held" — eval readers took that
102
+ // word literally against a 0 → 54 pair.
103
+ const verdict = c.delivered < c.baseline
104
+ ? `Assertion count fell: ${describeAssertionCount(c)} — documented by an allow marker.`
105
+ : c.delivered === c.baseline
106
+ ? `Assertion count held: ${describeAssertionCount(c)}.`
107
+ : `Assertion count rose: ${describeAssertionCount(c)}.`;
108
+ return ` ${verdict} Count only — skyramp_execute_test compares assertion subjects against the generated spec.`;
109
+ }
110
+ /** How the un-extracted advisory names where a helper may go: every module in scope,
111
+ * each as its repository-relative path, joined with `, ` — the label the report row
112
+ * carries, so the text the agent edits from agrees with it. (The inline advisory
113
+ * names ONE module per site: the one that defines the matched helper.) Runs `git
114
+ * rev-parse` once per file: call it once per advisory, never inside a per-site loop.
115
+ * Falls back to the generic name when no module exists yet (the no-utils-file path). */
116
+ function moduleLabel(r) {
117
+ return r.utilsFiles.length > 0
118
+ ? utilsFileLabel(r.utilsFiles)
119
+ : "the shared utils file";
120
+ }
76
121
  /** SKYR-4219: the sibling call-site pass the prompt asks for and the agent skips (0/2
77
122
  * seeded eval runs). Stated here because the verify call is the one point every run
78
123
  * reaches. Advisory — the sibling is a pre-existing test the maintenance flow owns, so
@@ -80,16 +125,44 @@ export function formatUtilsVerifyText(r, ctx = {}) {
80
125
  function inlineCallSitesAdvisory(r) {
81
126
  if (r.inlineCallSites.length === 0)
82
127
  return "";
83
- const utils = r.utilsFiles.length === 1
84
- ? path.basename(r.utilsFiles[0])
85
- : "the utils file";
128
+ // One label per utils FILE, computed once (utilsFileLabel runs git per call), so a
129
+ // site names the module that defines its helper rather than the list of all.
130
+ const labels = new Map(r.utilsFiles.map((f) => [f, utilsFileLabel([f])]));
131
+ const labelOf = (f) => labels.get(f) ?? utilsFileLabel([f]);
86
132
  const n = r.inlineCallSites.length;
87
133
  return [
88
134
  "",
89
135
  "",
90
136
  `ADVISORY — ${n} inline request call${n === 1 ? "" : "s"} in other Skyramp-generated tests that a shared helper already wraps:`,
91
- ...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line} · ${c.method} ${c.path} · ${utils}.${c.helper}`),
92
- `Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there), then re-verify AND run the edited file with skyramp_execute_test — an edited pre-existing test without a recorded execution cannot be reported. This does not block execution.`,
137
+ ...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line}${c.localHelper ? ` (in local helper ${c.localHelper})` : ""} · ${c.method} ${c.path} · ${labelOf(c.utilsFile)}.${c.helper}`),
138
+ `Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there). For a site in a local helper, replace that request inside the local helper with a call to the shared helper and keep the local helper's other statements; delete the local helper only if it then does nothing but forward, moving the import and the differing literals to its call sites. Then re-verify AND run the edited file with skyramp_execute_test — an edited pre-existing test without a recorded execution cannot be reported. This does not block execution.`,
139
+ ].join("\n");
140
+ }
141
+ /** Operations written in two or more sibling generated tests with no shared helper —
142
+ * the mirror of inlineCallSitesAdvisory: that names a helper not USED, this a helper
143
+ * not CREATED. Every gate compares against a defined helper, so a run could leave the
144
+ * operation its PR was about duplicated four times and report a clean pass. Advisory —
145
+ * consolidation is a judgement, and a wrong block costs more than a wrong note. */
146
+ function unextractedDuplicatesAdvisory(r) {
147
+ const dups = r.unextractedDuplicates;
148
+ if (dups.length === 0)
149
+ return "";
150
+ const utils = moduleLabel(r);
151
+ const n = dups.length;
152
+ return [
153
+ "",
154
+ "",
155
+ `ADVISORY — ${n} operation${n === 1 ? " is" : "s are"} written in two or more Skyramp-generated tests here and no shared helper wraps ${n === 1 ? "it" : "them"} (does not block):`,
156
+ ...dups.map((d) => `- ${d.method} ${d.path} · ${d.sites.length} copies: ` +
157
+ // Every site is in the spec's own directory by construction, so the basename
158
+ // is unambiguous — and show() has nothing to relativise against on the
159
+ // no-utils-file path, where it would print every site in full.
160
+ d.sites
161
+ .map((s) => `${path.basename(s.file)}:${s.line}${s.localHelper ? ` (${s.localHelper})` : ""}`)
162
+ .join(", ")),
163
+ // The remediation is the policy's own rule, not a restatement; extracting a helper
164
+ // writes to the utils file, which the blocking gates measure, so re-verify.
165
+ `Each is a candidate for one helper in ${utils} (${HELPER_FAMILIES.api.fixRule}); the literals that differ between the copies are its defaulted parameters. Then re-verify; a pre-existing test you edited must also run with skyramp_execute_test.`,
93
166
  ].join("\n");
94
167
  }
95
168
  /** Helpers only this test (or no test) imports. Advisory: on a first run every helper
@@ -146,6 +219,25 @@ function typingAdvisory(r) {
146
219
  "Annotate each with its actual type — a Playwright page as `page: Page` (add `import type { Page } from '@playwright/test'` once, only if a page parameter is among them); API values by their shape (`client: SkyrampClient`, `headers: Record<string, string>`); never `any`. Advisory only: continue without re-verifying, and do NOT write a `reuse-verify` marker for it.",
147
220
  ].join("\n");
148
221
  }
222
+ /** Browser helpers whose Playwright action sequences agree — one operation under two
223
+ * names, the drift a second run compounds. Rendered beside the other
224
+ * duplicate-detection advisories (inline call sites, un-extracted duplicates), after
225
+ * the per-helper naming and typing notes and before the importer counts. The key is read from selector literals,
226
+ * so this advises where the api route key blocks: a wrong note costs a sentence, a
227
+ * wrong block costs a run. */
228
+ function duplicateActionAdvisory(r) {
229
+ const pairs = r.advisories.filter((v) => v.kind === "duplicate-action");
230
+ if (pairs.length === 0)
231
+ return "";
232
+ const n = pairs.length;
233
+ return [
234
+ "",
235
+ "",
236
+ `ADVISORY — ${n} pair${n === 1 ? "" : "s"} of browser helpers perform${n === 1 ? "s" : ""} the same action sequence (does not block):`,
237
+ ...pairs.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
238
+ "Two names for one action is how a shared module drifts on every later run. Keep ONE helper per action sequence, named for the user intent; lift the values that differ (a filled text, a quantity) into defaulted parameters; update the call sites and delete the other. Skip only if the two act on genuinely different elements. If you edit the utils file or a call site, re-verify; a pre-existing test whose call site changed must also be run with skyramp_execute_test. Advisory only: it never blocks, and do NOT write a `reuse-verify` marker for it.",
239
+ ].join("\n");
240
+ }
149
241
  function verdictTail(ctx) {
150
242
  if (ctx.recorded !== false)
151
243
  return "";
@@ -243,7 +243,8 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
243
243
  if (params.authHeader === undefined) {
244
244
  try {
245
245
  const repoPath = params.outputDir || process.cwd();
246
- const resolved = await resolveAuthFromWorkspace(repoPath, params.authHeader, params.authScheme);
246
+ // The directory the test is written into names the service it is for.
247
+ const resolved = await resolveAuthFromWorkspace(repoPath, params.authHeader, params.authScheme, params.outputDir);
247
248
  if (resolved) {
248
249
  params.authHeader = resolved.authHeader;
249
250
  if (resolved.authScheme !== undefined) {