@skyramp/mcp 0.3.7 → 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 (27) hide show
  1. package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
  2. package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
  3. package/build/prompts/test-recommendation/recommendationSections.js +3 -3
  4. package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
  5. package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
  7. package/build/prompts/testbot/testbot-prompts.js +25 -8
  8. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
  9. package/build/recommendation/budgeters/shared.d.ts +21 -8
  10. package/build/recommendation/budgeters/shared.js +134 -56
  11. package/build/recommendation/planRanker.d.ts +5 -3
  12. package/build/recommendation/planRanker.js +1 -1
  13. package/build/tools/submitReportTool.js +6 -1
  14. package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
  15. package/build/tools/test-management/registerTestPlanTool.js +29 -8
  16. package/build/types/RepositoryAnalysis.d.ts +10 -10
  17. package/build/types/TestRecommendation.d.ts +11 -1
  18. package/build/types/TestRecommendation.js +34 -0
  19. package/build/utils/pathSignatures.d.ts +4 -1
  20. package/build/utils/pathSignatures.js +14 -2
  21. package/build/utils/planOnlyMode.d.ts +33 -0
  22. package/build/utils/planOnlyMode.js +40 -0
  23. package/build/utils/reportVerification.d.ts +5 -0
  24. package/build/utils/reportVerification.js +7 -2
  25. package/build/utils/scenarioDrafting.d.ts +1 -1
  26. package/build/utils/scenarioDrafting.js +1 -1
  27. package/package.json +1 -1
@@ -12,7 +12,7 @@ import { CandidateSource, computeCandidateId, scenarioMergeKey, DiscriminatorKin
12
12
  import { selectPlan } from "../../recommendation/planRanker.js";
13
13
  import { resolveSubjectEndpoints } from "../../utils/subjectEndpoints.js";
14
14
  import { collectChangedRouteLines } from "../../utils/changedRoutes.js";
15
- import { reservedUISlots } from "../../recommendation/budgeters/shared.js";
15
+ import { generateFloors } from "../../recommendation/budgeters/shared.js";
16
16
  import { inferScenarioType } from "../../recommendation/diversity.js";
17
17
  import { validateDiscriminator } from "../../recommendation/discriminators.js";
18
18
  import { isAttackSurfaceSecurityBoundary } from "../../prompts/test-recommendation/recommendationShared.js";
@@ -76,7 +76,7 @@ const discriminatorClaimSchema = z.object({
76
76
  const registerCandidateSchema = z.object({
77
77
  scenarioName: z.string().min(1).describe("Name for this test/scenario. Reused as the match key against the generation tool's scenarioName — keep it identical when you later call the generation tool."),
78
78
  description: z.string().min(1),
79
- category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag)."),
79
+ category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag). `bug_caught` targets a `<bug_found>` flaw from code review. `requirement_conflict` asserts a requirement the PR title/description (or a requirements file it references) states and the implemented behavior contradicts — it fails on the current code by design. Both rank in the top tier but are carved out independently, so a requirement conflict never loses its slot to a code-review bug."),
80
80
  priority: z.enum(["high", "medium", "low"]).describe("Your own assessment — recorded, but ranking is derived from category, not this field."),
81
81
  testType: z.nativeEnum(TestType).refine((t) => PLAN_TEST_TYPES.has(t), {
82
82
  message: `testType must be one of ${[...PLAN_TEST_TYPES].join(", ")}`,
@@ -340,7 +340,26 @@ function renderGenerationDirective(plan) {
340
340
  (nonUICount > 0 ? ` Generate the ${tests(nonUICount)} of other types as well.` : ""),
341
341
  ];
342
342
  }
343
- function renderPlanText(plan, dropped) {
343
+ /** SKYR-4292: the server cannot count the diff's changed behaviors, so an
344
+ * under-budget plan is flagged (not rejected) with what a missing candidate looks
345
+ * like. Wipro PR #24 registered 3 candidates against a 20-slot UI-only budget and
346
+ * the tool answered "AUTHORITATIVE and FINAL" with no pushback. */
347
+ function renderBudgetCheck(plan, maxTotal) {
348
+ const filled = plan.generate.length + plan.additional.length;
349
+ if (filled >= maxTotal)
350
+ return [];
351
+ return [
352
+ "",
353
+ `### Budget check: ${filled} of ${maxTotal} slots filled`,
354
+ "Every observable behavior this diff changes — each option value added, removed, renamed or reordered; each label, " +
355
+ "element, route, state, validation rule, computed value or style value (spacing, size, color, radius, font weight, " +
356
+ "class name) that renders differently; each removed element, asserted absent — needs a candidate above, however " +
357
+ "small the change. A change is covered only when an existing test asserts the changed value, not when a test merely " +
358
+ "reaches the component. If any changed behavior has no candidate, call skyramp_register_test_plan again with your " +
359
+ "full updated list. If the diff truly changes fewer behaviors than the budget, this plan stands as-is.",
360
+ ];
361
+ }
362
+ function renderPlanText(plan, dropped, maxTotal) {
344
363
  const lines = [];
345
364
  lines.push(`## Approved Test Plan (${plan.planId})`);
346
365
  lines.push("");
@@ -402,6 +421,7 @@ function renderPlanText(plan, dropped) {
402
421
  }
403
422
  lines.push("");
404
423
  lines.push(...renderGenerationDirective(plan));
424
+ lines.push(...renderBudgetCheck(plan, maxTotal));
405
425
  return lines.join("\n");
406
426
  }
407
427
  // ── Tool registration ───────────────────────────────────────────────────────
@@ -544,10 +564,11 @@ export function registerRegisterTestPlanTool(server) {
544
564
  // changes would silently plan zero UI tests (runs 32419025266,
545
565
  // 32431042133, 32431106864) — reject so the agent self-corrects in one
546
566
  // step, exactly like the empty-candidates rejection above.
547
- if (reservedUISlots(budgetContext) > 0 &&
548
- !allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
549
- errorResult = toolError("This run has frontend changes, so the plan reserves a UI slotbut no submitted or recovered " +
550
- "candidate has testType 'ui'. Resubmit your full candidate list including at least one UI candidate " +
567
+ const uiFloor = generateFloors(budgetContext).ui;
568
+ if (uiFloor > 0 && !allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
569
+ errorResult = toolError(`This run has frontend changes, so the plan MUST include UI testsat least ${uiFloor} of the ` +
570
+ "GENERATE slots are held for them, and more are open to UI candidates on rank. No submitted or " +
571
+ "recovered candidate has testType 'ui'. Resubmit your full candidate list including UI candidates " +
551
572
  "for the changed frontend (see uiContext.changedFrontendFiles).");
552
573
  return errorResult;
553
574
  }
@@ -576,7 +597,7 @@ export function registerRegisterTestPlanTool(server) {
576
597
  return errorResult;
577
598
  }
578
599
  return {
579
- content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped) }],
600
+ content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped, budgetContext.maxTotal) }],
580
601
  };
581
602
  }
582
603
  catch (error) {
@@ -1119,7 +1119,7 @@ export declare const scenarioStepSchema: z.ZodObject<{
1119
1119
  export declare const draftedScenarioSchema: z.ZodObject<{
1120
1120
  scenarioName: z.ZodString;
1121
1121
  description: z.ZodString;
1122
- category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1122
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1123
1123
  priority: z.ZodEnum<["high", "medium", "low"]>;
1124
1124
  steps: z.ZodArray<z.ZodObject<{
1125
1125
  order: z.ZodNumber;
@@ -1231,7 +1231,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1231
1231
  description: string;
1232
1232
  priority: "high" | "medium" | "low";
1233
1233
  scenarioName: string;
1234
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1234
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1235
1235
  steps: {
1236
1236
  path: string;
1237
1237
  method: string;
@@ -1268,7 +1268,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1268
1268
  description: string;
1269
1269
  priority: "high" | "medium" | "low";
1270
1270
  scenarioName: string;
1271
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1271
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1272
1272
  steps: {
1273
1273
  path: string;
1274
1274
  method: string;
@@ -1607,7 +1607,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1607
1607
  draftedScenarios: z.ZodArray<z.ZodObject<{
1608
1608
  scenarioName: z.ZodString;
1609
1609
  description: z.ZodString;
1610
- category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1610
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1611
1611
  priority: z.ZodEnum<["high", "medium", "low"]>;
1612
1612
  steps: z.ZodArray<z.ZodObject<{
1613
1613
  order: z.ZodNumber;
@@ -1719,7 +1719,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1719
1719
  description: string;
1720
1720
  priority: "high" | "medium" | "low";
1721
1721
  scenarioName: string;
1722
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1722
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1723
1723
  steps: {
1724
1724
  path: string;
1725
1725
  method: string;
@@ -1756,7 +1756,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1756
1756
  description: string;
1757
1757
  priority: "high" | "medium" | "low";
1758
1758
  scenarioName: string;
1759
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1759
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1760
1760
  steps: {
1761
1761
  path: string;
1762
1762
  method: string;
@@ -1799,7 +1799,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1799
1799
  description: string;
1800
1800
  priority: "high" | "medium" | "low";
1801
1801
  scenarioName: string;
1802
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1802
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1803
1803
  steps: {
1804
1804
  path: string;
1805
1805
  method: string;
@@ -1842,7 +1842,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1842
1842
  description: string;
1843
1843
  priority: "high" | "medium" | "low";
1844
1844
  scenarioName: string;
1845
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1845
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1846
1846
  steps: {
1847
1847
  path: string;
1848
1848
  method: string;
@@ -2743,7 +2743,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2743
2743
  description: string;
2744
2744
  priority: "high" | "medium" | "low";
2745
2745
  scenarioName: string;
2746
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2746
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2747
2747
  steps: {
2748
2748
  path: string;
2749
2749
  method: string;
@@ -2956,7 +2956,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2956
2956
  description: string;
2957
2957
  priority: "high" | "medium" | "low";
2958
2958
  scenarioName: string;
2959
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2959
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2960
2960
  steps: {
2961
2961
  path: string;
2962
2962
  method: string;
@@ -12,7 +12,7 @@ export declare enum Novelty {
12
12
  EXISTING = "existing"
13
13
  }
14
14
  /** All categories including internal ones. */
15
- export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
15
+ export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
16
16
  export type ScenarioCategory = typeof SCENARIO_CATEGORIES[number];
17
17
  /** Categories valid for tool submissions (excludes internal-only categories). */
18
18
  export declare const TEST_CATEGORIES: readonly ["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
@@ -21,3 +21,13 @@ export type TestCategory = typeof TEST_CATEGORIES[number];
21
21
  export declare const CATEGORY_PRIORITY: Record<ScenarioCategory, PriorityTier>;
22
22
  /** Map internal-only categories to their external equivalent for tool submission. */
23
23
  export declare function externalCategory(cat: ScenarioCategory): TestCategory;
24
+ /**
25
+ * Categories whose scenarios target a specific identified defect — a code flaw
26
+ * (`bug_caught`) or a stated requirement the code contradicts
27
+ * (`requirement_conflict`). They survive external-test dedup: an existing test on
28
+ * the same endpoint exercises the surface, not the flaw, so removing them would
29
+ * drop the only test that fails on the defect.
30
+ */
31
+ export declare const FLAW_TARGETING_CATEGORIES: readonly ["bug_caught", "requirement_conflict"];
32
+ /** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
33
+ export declare function isFlawTargetingCategory(category: ScenarioCategory | undefined): boolean;
@@ -17,6 +17,14 @@ export var Novelty;
17
17
  const INTERNAL_CATEGORIES = [
18
18
  "new_endpoint", // MEDIUM - diff-direct scenario; where a test came from, not a guarantee of a slot
19
19
  "bug_caught", // CRITICAL - tests targeting a specific <bug_found> flaw identified during enrichment
20
+ // CRITICAL - tests asserting a requirement the PR title/description (or a
21
+ // requirements file it references) states, which the implemented behavior
22
+ // contradicts. Separate from bug_caught deliberately (SKYR-4291): labelled
23
+ // bug_caught, a requirement-vs-code mismatch competed with the code-review
24
+ // flaws for the same promotion and lost it on severity. Its own category means
25
+ // its own carve-out here, first place in the promotion order SKYR-4275's bound
26
+ // hands out, and its own coverage gate.
27
+ "requirement_conflict",
20
28
  ];
21
29
  /** External categories valid for tool submissions, ordered by priority. */
22
30
  const CATEGORIES = [
@@ -59,6 +67,12 @@ export const CATEGORY_PRIORITY = {
59
67
  // fill GENERATE.
60
68
  new_endpoint: PriorityTier.MEDIUM,
61
69
  bug_caught: PriorityTier.CRITICAL, // tests targeting a <bug_found> flaw — always in GENERATE
70
+ // A stated requirement the implementation contradicts is the point of the PR,
71
+ // so it sits in the same top tier as bug_caught. Being its own category, it is
72
+ // carved out separately in planRanker and takes the FIRST slot of the promotion
73
+ // bound SKYR-4275 sets — the code-review flaws promote into what is left, so a
74
+ // requirement conflict is never the finding that loses on severity.
75
+ requirement_conflict: PriorityTier.CRITICAL,
62
76
  business_rule: PriorityTier.HIGH, // formula/business-logic bugs are high priority
63
77
  security_boundary: PriorityTier.HIGH,
64
78
  data_integrity: PriorityTier.HIGH,
@@ -75,5 +89,25 @@ export function externalCategory(cat) {
75
89
  return "crud";
76
90
  if (cat === "bug_caught")
77
91
  return "business_rule";
92
+ // The stated requirement IS the business rule the test asserts — same landing
93
+ // spot as bug_caught, so a requirement-conflict test reads as a rule check in
94
+ // the customer-facing report rather than an unrecognised label.
95
+ if (cat === "requirement_conflict")
96
+ return "business_rule";
78
97
  return cat;
79
98
  }
99
+ /**
100
+ * Categories whose scenarios target a specific identified defect — a code flaw
101
+ * (`bug_caught`) or a stated requirement the code contradicts
102
+ * (`requirement_conflict`). They survive external-test dedup: an existing test on
103
+ * the same endpoint exercises the surface, not the flaw, so removing them would
104
+ * drop the only test that fails on the defect.
105
+ */
106
+ export const FLAW_TARGETING_CATEGORIES = [
107
+ "bug_caught",
108
+ "requirement_conflict",
109
+ ];
110
+ /** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
111
+ export function isFlawTargetingCategory(category) {
112
+ return category !== undefined && FLAW_TARGETING_CATEGORIES.includes(category);
113
+ }
@@ -22,7 +22,10 @@ export declare function isIndexModule(file: string): boolean;
22
22
  * (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
23
23
  * ends with the aliased path AND the importer lives under the same root that the
24
24
  * alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
25
- * not match `packages/admin/app/shop/_constants/index.ts`. All paths are
25
+ * not match `packages/admin/app/shop/_constants/index.ts`. Prefix-less
26
+ * multi-segment specifiers (`Components/Foo` under tsconfig `baseUrl: "src"` —
27
+ * SKYR-4287) go through the same suffix + shared-root check; only single-segment
28
+ * prefix-less specifiers (`react`) are treated as npm packages. All paths are
26
29
  * repo-relative with forward slashes.
27
30
  */
28
31
  export declare function importsIndexModule(importerFile: string, importerContent: string, indexFile: string): boolean;
@@ -60,7 +60,10 @@ export function isIndexModule(file) {
60
60
  * (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
61
61
  * ends with the aliased path AND the importer lives under the same root that the
62
62
  * alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
63
- * not match `packages/admin/app/shop/_constants/index.ts`. All paths are
63
+ * not match `packages/admin/app/shop/_constants/index.ts`. Prefix-less
64
+ * multi-segment specifiers (`Components/Foo` under tsconfig `baseUrl: "src"` —
65
+ * SKYR-4287) go through the same suffix + shared-root check; only single-segment
66
+ * prefix-less specifiers (`react`) are treated as npm packages. All paths are
64
67
  * repo-relative with forward slashes.
65
68
  */
66
69
  export function importsIndexModule(importerFile, importerContent, indexFile) {
@@ -76,7 +79,16 @@ export function importsIndexModule(importerFile, importerContent, indexFile) {
76
79
  continue;
77
80
  }
78
81
  const aliased = spec.replace(/^(@\/|~~\/|~\/|#\/|src\/)/, "");
79
- if (aliased === spec)
82
+ // A prefix-less specifier is only a bare npm package when it has a single
83
+ // segment ('react'). Multi-segment prefix-less specifiers are how tsconfig
84
+ // `baseUrl` repos import internal modules ('Components/Foo' with
85
+ // baseUrl: "src" — SKYR-4287: every folder-per-component index.tsx in such
86
+ // repos was misreported as no-importers). Those flow into the same
87
+ // suffix + shared-root check as aliases; a deep npm import ('lodash/get')
88
+ // is unlikely to match unless the repo actually contains a `lodash/get`
89
+ // directory under the same root, and a scoped package ('@scope/pkg') would
90
+ // require a source directory literally named '@scope'.
91
+ if (aliased === spec && !spec.includes("/"))
80
92
  continue; // bare package specifier — not a repo path
81
93
  if (moduleDir === aliased)
82
94
  return true; // alias maps to the repo root
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Plan-only mode capture for `skyramp_submit_report` (SKYR-4250).
3
+ *
4
+ * A plan-only eval run (SKYR-3879) writes no test files: `newTestsCreated`
5
+ * DECLARES the approved GENERATE selection instead of recording generated
6
+ * files, and the declaration itself is the deliverable. The SKYR-3883 guard
7
+ * (`findUnchangedFileClaims`) verifies every `newTestsCreated` claim against
8
+ * the working tree, so in that lane EVERY declaration is unbacked by
9
+ * construction and the report is rejected.
10
+ *
11
+ * That rejection is not evenly distributed. `findUnchangedFileClaims` exempts a
12
+ * claim attributed to a non-primary repo, so on a cross-repo fixture the
13
+ * related repo's declarations pass and only the primary repo's are checked.
14
+ * On a frontend-primary pair that makes the mandatory UI item (SKYR-4204's
15
+ * floor) the one claim that always fails, and the agent's only compliant move
16
+ * is to demote it to `additionalRecommendations` — which is exactly the
17
+ * under-declaration SKYR-4250 exists to fix (run 32903633247).
18
+ *
19
+ * `planOnly` reaches the server only as a testbot prompt/resource arg, so this
20
+ * mirrors the SKYR-4185 report-language capture: the prompt render is the
21
+ * single capture point, and the report tool reads the captured mode later in
22
+ * the same process.
23
+ *
24
+ * Capture is fail-open by design: if the testbot prompt was never served, the
25
+ * mode stays false and the guard behaves exactly as it does today.
26
+ */
27
+ /** Capture plan-only mode at prompt-serve time. Last render wins: a later
28
+ * non-plan-only render disarms a mode captured earlier in a long-lived server
29
+ * process, so an ordinary run never silently skips the guard. */
30
+ export declare function setPlanOnlyMode(planOnly: boolean): void;
31
+ export declare function isPlanOnlyMode(): boolean;
32
+ /** Test isolation only — module state persists across tests in one process. */
33
+ export declare function resetPlanOnlyMode(): void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Plan-only mode capture for `skyramp_submit_report` (SKYR-4250).
3
+ *
4
+ * A plan-only eval run (SKYR-3879) writes no test files: `newTestsCreated`
5
+ * DECLARES the approved GENERATE selection instead of recording generated
6
+ * files, and the declaration itself is the deliverable. The SKYR-3883 guard
7
+ * (`findUnchangedFileClaims`) verifies every `newTestsCreated` claim against
8
+ * the working tree, so in that lane EVERY declaration is unbacked by
9
+ * construction and the report is rejected.
10
+ *
11
+ * That rejection is not evenly distributed. `findUnchangedFileClaims` exempts a
12
+ * claim attributed to a non-primary repo, so on a cross-repo fixture the
13
+ * related repo's declarations pass and only the primary repo's are checked.
14
+ * On a frontend-primary pair that makes the mandatory UI item (SKYR-4204's
15
+ * floor) the one claim that always fails, and the agent's only compliant move
16
+ * is to demote it to `additionalRecommendations` — which is exactly the
17
+ * under-declaration SKYR-4250 exists to fix (run 32903633247).
18
+ *
19
+ * `planOnly` reaches the server only as a testbot prompt/resource arg, so this
20
+ * mirrors the SKYR-4185 report-language capture: the prompt render is the
21
+ * single capture point, and the report tool reads the captured mode later in
22
+ * the same process.
23
+ *
24
+ * Capture is fail-open by design: if the testbot prompt was never served, the
25
+ * mode stays false and the guard behaves exactly as it does today.
26
+ */
27
+ let sessionPlanOnly = false;
28
+ /** Capture plan-only mode at prompt-serve time. Last render wins: a later
29
+ * non-plan-only render disarms a mode captured earlier in a long-lived server
30
+ * process, so an ordinary run never silently skips the guard. */
31
+ export function setPlanOnlyMode(planOnly) {
32
+ sessionPlanOnly = planOnly;
33
+ }
34
+ export function isPlanOnlyMode() {
35
+ return sessionPlanOnly;
36
+ }
37
+ /** Test isolation only — module state persists across tests in one process. */
38
+ export function resetPlanOnlyMode() {
39
+ sessionPlanOnly = false;
40
+ }
@@ -50,6 +50,11 @@ export interface UnchangedFileClaimsInput {
50
50
  /** owner/repo of the primary repo — used to exempt cross-repo newTest claims
51
51
  * whose files live in a different checkout. */
52
52
  primaryRepository?: string;
53
+ /** SKYR-4250: a plan-only eval run generates nothing — `newTestsCreated`
54
+ * declares the approved GENERATE list — so no declaration can be backed by
55
+ * a working-tree change. Exempts the declaration half only; a maintenance
56
+ * UPDATE still claims a real edit and is still verified. */
57
+ planOnly?: boolean;
53
58
  }
54
59
  /**
55
60
  * Cross-check report claims against files that actually changed in the working
@@ -124,7 +124,7 @@ export async function listChangedFiles(repoRoot) {
124
124
  * absolute path resolving outside `repoRoot`) — a different checkout owns them.
125
125
  */
126
126
  export function findUnchangedFileClaims(input) {
127
- const { repoRoot, changedFiles, newTests, verdicts, primaryRepository } = input;
127
+ const { repoRoot, changedFiles, newTests, verdicts, primaryRepository, planOnly, } = input;
128
128
  const unbacked = [];
129
129
  const isBacked = (claimedPath) => {
130
130
  let candidate = claimedPath;
@@ -137,7 +137,12 @@ export function findUnchangedFileClaims(input) {
137
137
  }
138
138
  return changedFiles.some((c) => testFileMatches(c, candidate));
139
139
  };
140
- for (const t of newTests) {
140
+ // A plan-only run declares its GENERATE list instead of writing files, so
141
+ // skip the claim scan wholesale rather than per-claim: checking it rejects
142
+ // every declaration the guard can see, and the ones it CANNOT see (cross-repo
143
+ // claims, exempted below) would pass — demoting the primary repo's items
144
+ // alone. Uneven enforcement is worse than none here.
145
+ for (const t of planOnly ? [] : newTests) {
141
146
  if (!t.fileName)
142
147
  continue;
143
148
  // Cross-repo finding — its file lives in a different checkout, not this tree.
@@ -46,7 +46,7 @@ export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInp
46
46
  /**
47
47
  * Enforce a global cap on drafted scenarios while preserving category diversity.
48
48
  *
49
- * 1. CRITICAL (bug_caught) scenarios prioritized first.
49
+ * 1. CRITICAL (bug_caught, requirement_conflict) scenarios prioritized first.
50
50
  * 2. One scenario per non-empty category guaranteed (breadth).
51
51
  * 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
52
52
  * 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
@@ -226,7 +226,7 @@ const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
226
226
  /**
227
227
  * Enforce a global cap on drafted scenarios while preserving category diversity.
228
228
  *
229
- * 1. CRITICAL (bug_caught) scenarios prioritized first.
229
+ * 1. CRITICAL (bug_caught, requirement_conflict) scenarios prioritized first.
230
230
  * 2. One scenario per non-empty category guaranteed (breadth).
231
231
  * 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
232
232
  * 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",