@skyramp/mcp 0.4.0-rc.2 → 0.4.1-rc.1

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 (33) hide show
  1. package/build/prompts/enhance-assertions/sharedAssertionRules.js +20 -4
  2. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -5
  3. package/build/prompts/testbot/testbot-prompts.js +10 -8
  4. package/build/recommendation/answers.d.ts +11 -7
  5. package/build/recommendation/answers.js +14 -10
  6. package/build/recommendation/pullRequestText.d.ts +18 -0
  7. package/build/recommendation/pullRequestText.js +31 -0
  8. package/build/recommendation/registerPlan.d.ts +5 -1
  9. package/build/recommendation/registerPlan.js +3 -1
  10. package/build/recommendation/runVerifiers.js +6 -0
  11. package/build/recommendation/types.d.ts +35 -0
  12. package/build/recommendation/verifierContracts.d.ts +94 -11
  13. package/build/recommendation/verifierContracts.js +129 -27
  14. package/build/recommendation/verifiers/defects.d.ts +9 -0
  15. package/build/recommendation/verifiers/defects.js +117 -0
  16. package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
  17. package/build/recommendation/verifiers/expectedValueSourced.js +149 -0
  18. package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
  19. package/build/recommendation/verifiers/issueTraceability.js +197 -0
  20. package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
  21. package/build/recommendation/verifiers/requirementSourced.js +168 -0
  22. package/build/tools/submitReportTool.js +19 -3
  23. package/build/tools/test-management/registerTestPlanTool.d.ts +31 -17
  24. package/build/tools/test-management/registerTestPlanTool.js +79 -5
  25. package/build/types/TestbotReport.d.ts +7 -3
  26. package/build/utils/assertion-verify/api-shared-lints.js +70 -0
  27. package/package.json +1 -1
  28. package/plugin/prompts/generate-tests/execution-plan.md +2 -2
  29. package/plugin/prompts/plan-tests.md +18 -9
  30. package/plugin/prompts/testbot-task1.md +3 -9
  31. package/build/prompts/testbot/planDeclarations.d.ts +0 -6
  32. package/build/prompts/testbot/planDeclarations.js +0 -9
  33. package/plugin/prompts/declaring-a-plan.md +0 -20
@@ -28,12 +28,13 @@ expect(response.statusCode).toBe(201);`,
28
28
  ],
29
29
  },
30
30
  {
31
- title: "Error path (HTTP 4xx/5xx responses)",
32
- description: "For every HTTP 4xx/5xx response that includes a response body, assert every error body field with its exact value — including `error.code`, `error.message`, `detail`, `errors[0].message`, and `errors[0].extensions.code`.",
31
+ title: "Error path (HTTP 4xx/5xx, or a 2xx carrying only an error body)",
32
+ description: "For every error response that includes a body, assert every error body field with its exact value — including `error.code`, `error.message`, `detail`, `errors[0].message`, and `errors[0].extensions.code`. An error response is any 4xx/5xx with a body, and any 2xx whose body is an error object (`error`, `message`, `errors[]`) with no resource payload.",
33
33
  subPoints: [
34
34
  "Also apply the array-validation rule to any errors array.",
35
35
  "Asserting only the status code is never sufficient when a body is present.",
36
36
  "For no-body responses such as a successful DELETE (204): assert the status code only.",
37
+ "For a 2xx error body, assert the exact status code, the error field's exact value, and that the resource fields carry no value (`toBeNull()` / `is None` — the SDK helper reads a missing path and an explicit null the same way, so this asserts \"no payload\", not \"key absent\"; use `checkSchema` when the two must be told apart). Never assert the success shape (a non-empty collection, `error` absent) against a response the recorded trace or `expected_response_body` shows returning an error or no-match body — classify the response by what this request returns, not by the outcome the test was meant to check for. The one exception is a test the plan declared `expected.outcome: fail` (typically a `bug_caught` or `requirement_conflict` candidate; the category is only the fallback when nothing was declared): it asserts the intended behaviour established by the diff, the PR statement, or the code's evident intent — keep that assertion and let the test fail.",
37
38
  ],
38
39
  examples: [
39
40
  {
@@ -43,6 +44,20 @@ expect(getValue(response, "errors.0.message")).toBe("Item not found");
43
44
  expect(getValue(response, "errors.0.extensions.code")).toBe("RECORD_NOT_FOUND");
44
45
  expect(getValue(response, "errors.1")).toBeNull();`,
45
46
  },
47
+ {
48
+ language: "javascript",
49
+ code: `// Recorded response is a 200 carrying an error object and no payload
50
+ expect(searchPostResponse.statusCode, 'status code').toBe(200);
51
+ expect(getResponseValue(searchPostResponse, "error"), 'error').toBe("No items matched the criteria.");
52
+ expect(getResponseValue(searchPostResponse, "results"), 'results carries no value').toBeNull();`,
53
+ },
54
+ {
55
+ language: "python",
56
+ code: `# Recorded response: 400 {"error": "level must be one of Intern, Junior, Senior"}
57
+ assert record_post_response.status_code == 400
58
+ assert skyramp.get_response_value(record_post_response, "error") == "level must be one of Intern, Junior, Senior"
59
+ assert skyramp.get_response_value(record_post_response, "insertedId") is None`,
60
+ },
46
61
  ],
47
62
  },
48
63
  {
@@ -77,7 +92,7 @@ expect(getValue(response, "errors.1")).toBeNull();`,
77
92
  "When the response is a non-empty array: assert the exact length, key fields on each item, and that the index after the last item is absent.",
78
93
  "Shape-only checks such as `Array.isArray` or `typeof` are not sufficient when the response contains actual values.",
79
94
  "If the response is sorted or ordered: assert the ordering direction across the first two items.",
80
- "When the response is an empty or minimal body (an empty object, empty array, null, or only a few keys): assert the empty or minimal shape and the absence of error fields — do not stop at the status code.",
95
+ "When the response is an empty or minimal body (an empty object, empty array, null, or only a few keys): assert the empty or minimal shape and the absence of error fields — do not stop at the status code. A minimal body that is itself an error object falls under the Error path rule instead: assert its error field exactly.",
81
96
  "When the request includes pagination or filter parameters, assert the response reflects them.",
82
97
  ],
83
98
  examples: [
@@ -207,7 +222,8 @@ Before editing the given file, you must output a \`<thinking>\` block. The aim o
207
222
  2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
208
223
  1. Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
209
224
  2. Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
210
- 3. Error response (4xx/5xx with a body): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index null). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is null.
225
+ 3. Error response (4xx/5xx with a body, or any status whose body is an error object such as \`{"error": "..."}\` with no resource payload): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index null). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is null.
226
+ Classify by the response the recorded trace or \`expected_response_body\` shows this request returning. The one exception is a test the plan declared \`expected.outcome: fail\` (typically a \`bug_caught\` or \`requirement_conflict\` candidate; read the declaration first and fall back to the category only when none was made): there the expected response is the intended behaviour established by the diff, the PR statement, or the code's evident intent, and the test is expected to fail until the code is fixed.
211
227
  3. For each in-scope response, output one JSON object using the template below. The output is an array — one object per in-scope response.
212
228
  - \`step\`: the HTTP method, path, and response variable name for this request (e.g. \`POST /products → products_POST_response\`).
213
229
  - \`response_status\`: one of \`success\`, \`no_body\`, or \`error\` based on the classification in step 2.
@@ -4,7 +4,6 @@ import { logger } from "../../utils/logger.js";
4
4
  import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildVerificationChecklist, } from "./recommendationSections.js";
5
5
  import { buildExecutionPlan, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
6
6
  import { readPromptAsset } from "../promptAssets.js";
7
- import { renderPlanDeclarationGuidance } from "../testbot/planDeclarations.js";
8
7
  import { TASK_GENERATE, taskRef, } from "./recommendationShared.js";
9
8
  // Re-export for backward compatibility (tests and external callers import this from this module)
10
9
  function formatTestLocations(locs) {
@@ -22,11 +21,11 @@ function formatTestLocations(locs) {
22
21
  // on subsequent testbot runs on the same PR.
23
22
  const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
24
23
  /** How this prompt tells the agent to plan: the whole procedure, every time.
25
- * Rendered from `plugin/prompts/plan-tests.md` and `declaring-a-plan.md` rather
26
- * than retyped, so there is one text and nothing to drift from. `isDiffScope` is
27
- * unused here — the procedure reads the same for a diff and for a repository. */
24
+ * Rendered from `plugin/prompts/plan-tests.md` rather than retyped, so there is
25
+ * one text and nothing to drift from. `isDiffScope` is unused here — the
26
+ * procedure reads the same for a diff and for a repository. */
28
27
  function planningInstruction(_isDiffScope) {
29
- return `${readPromptAsset("plan-tests.md").trim()}\n\n${renderPlanDeclarationGuidance()}`;
28
+ return readPromptAsset("plan-tests.md").trim();
30
29
  }
31
30
  export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, sessionId) {
32
31
  const isDiffScope = isDiff(analysisScope);
@@ -4,10 +4,10 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
4
4
  import { buildPathParamGuidance, } from "../test-recommendation/recommendationSections.js";
5
5
  import { setReportLanguage } from "../../utils/reportLanguage.js";
6
6
  import { setPlanOnlyMode } from "../../utils/planOnlyMode.js";
7
+ import { recordPullRequestText } from "../../recommendation/pullRequestText.js";
7
8
  import { TASK_ANALYZE_MAINTAIN, TASK_GENERATE, TASK_SUBMIT, TESTBOT_TASK1_MAINTAIN_LABELS, TESTBOT_TASK1_MULTIREPO_LABELS, TESTBOT_TASK1_STEP_LABELS, orderedLabels, stepSubRef, taskRef, } from "../test-recommendation/recommendationShared.js";
8
9
  import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
9
10
  import { isContractConsumerModeEnabled, isPomReuseEnabled, isUtilsReuseEnabled, isSkillsLoaded, } from "../../utils/featureFlags.js";
10
- import { renderPlanDeclarationGuidance } from "./planDeclarations.js";
11
11
  import { fixErrorsInstruction } from "../../skills/fixTestImportErrorsSkill.js";
12
12
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
13
13
  import { section, sectionBody } from "../promptAssets.js";
@@ -59,12 +59,12 @@ const PLAN_TESTS_MD = readPromptAsset("plan-tests.md").trim();
59
59
  // rendered neither it nor a copy, so the agent planned without the claims step and
60
60
  // without the backward surface map. Rendered here, never retyped.
61
61
  //
62
- // It sits OUTSIDE Task 1: the procedure is an eleven-item numbered list, Task 1's
62
+ // It sits OUTSIDE Task 1: the procedure is a twelve-item numbered list, Task 1's
63
63
  // own steps are numbered too, and nested inside Task 1 the two share one ordinal
64
64
  // space (`referenceIntegrity.test.ts` reads every `N.` there as a Task 1 step).
65
65
  //
66
- // Two steps run elsewhere in this lane — step 6's maintenance update is Task 1's
67
- // tool step, step 9's registration happens in Task 2 after it. The note states that
66
+ // Two steps run elsewhere in this lane — step 7's maintenance update is Task 1's
67
+ // tool step, step 10's registration happens in Task 2 after it. The note states that
68
68
  // order; the markdown stays the source.
69
69
  //
70
70
  // The count sentence sits HERE, beside the register instruction, because the agent
@@ -173,6 +173,8 @@ export function parseRelatedRepositories(raw) {
173
173
  }
174
174
  export function getTestbotPrompt(opts) {
175
175
  const { prTitle, prDescription, repositoryPath, baseBranch, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
176
+ // Kept in the process for the plan tool, which is registered on this server.
177
+ recordPullRequestText(prTitle, prDescription);
176
178
  // The legacy count arguments are DECLARED NOWHERE, so zod strips them and their
177
179
  // names never reach the agent — not in a schema it lists, not in rendered text.
178
180
  // A number in front of the agent reads as a ceiling whatever the words around it
@@ -357,7 +359,7 @@ ${uiGroundingBlock}
357
359
  This is a plan-only evaluation run: the application under test is NOT running, and this run evaluates test SELECTION only. Nothing is generated or executed in this task.
358
360
 
359
361
  - Draft your complete plannedTest list — every test you would generate OR recommend for this PR, grounded in the analysis output and the diff. Favor tests that would FAIL if the changed logic were buggy, not just tests that exercise the new surface.
360
- - Register that list through \`skyramp_register_test_plan\` as "How to declare your plan" above describes. What you register is the plan — nothing re-ranks or trims it.
362
+ - Register that list through \`skyramp_register_test_plan\` as the planning procedure above describes. What you register is the plan — nothing re-ranks or trims it.
361
363
  - Include UI and E2E planned tests on the same footing as the API types. The app is not running, so you cannot capture a blueprint — plan them ungrounded rather than drop them. For every UI plannedTest, set \`elements.items\` to null and set \`elements.pageUrl\` to the route path the test would visit, read out of the source: \`/orders/1\`, not a full URL, because no host is serving the app. Set \`screenEvidence.file\` to the changed frontend file that renders that route — the app not running does not excuse this one, because you read the file out of the diff. Describe the page or feature the test would exercise; do not name an element you have not seen. This lane is the one case where you fall back without attempting a capture first, so do NOT log the fallback in \`issuesFound\` — capture was never applicable here, and nothing failed.
362
364
  - Take no other actions in this task: no test generation tools, no browser traces or blueprint captures, no test files written, no test executions. Proceed directly to ${taskRef(TASK_SUBMIT)}.`;
363
365
  }
@@ -573,7 +575,7 @@ ${FINISH_CHECKS_BLOCK}
573
575
  // to name the changed file it is about, and cite the file it sits
574
576
  // in, and cite `routes` from the checkout, and all of those happen while
575
577
  // it analyzes in Task 1.
576
- const planningBlock = `\n${planProcedureBlock}\n\n${renderPlanDeclarationGuidance()}\n`;
578
+ const planningBlock = `\n${planProcedureBlock}\n`;
577
579
  return `<TITLE>${prTitle}</TITLE>
578
580
  <DESCRIPTION>${prDescription}</DESCRIPTION>
579
581
  ${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
@@ -603,9 +605,9 @@ In these cases:
603
605
  - \`issuesFound\` must be \`[]\` — do NOT add a "No testable behavioral surface" entry; the business case already explains the abstention
604
606
  - \`businessCaseAnalysis\` must be a one-sentence summary of what the PR actually does (do NOT leave it blank)
605
607
 
606
- **This zero-test path does NOT apply when the requirement check in ${taskRef(TASK_ANALYZE_MAINTAIN)} found a \`requirement_conflict\`.** A requirement the PR states and the diff did not implement is testable surface, whatever the diff otherwise contains. Report the conflict in \`issuesFound\` at severity \`high\` or above, and carry the failing \`requirement_conflict\` test in \`newTestsCreated\`. Where the requirement is observable nowhere you can reach, say that in \`businessCaseAnalysis\`. Abstaining there hands the author back their own code as though it were the requirement.
608
+ **This zero-test path does NOT apply when the requirement check found a \`requirement_conflict\`.** A requirement the PR states and the diff did not implement is testable surface, whatever the diff otherwise contains. Report the conflict in \`issuesFound\` at severity \`high\` or above, and carry the failing \`requirement_conflict\` test in \`newTestsCreated\`. Where the requirement is observable nowhere you can reach, say that in \`businessCaseAnalysis\`. Abstaining there hands the author back their own code as though it were the requirement.
607
609
 
608
- ${task3CountRule ? `${task3CountRule}\n\n` : ""}${reportLanguageBlock}Call \`skyramp_submit_report\`. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
610
+ ${task3CountRule ? `${task3CountRule}\n\n` : ""}${reportLanguageBlock}Call \`skyramp_submit_report\`. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly. A report check objects to an \`issuesFound\` entry with \`category: bug\` that no delivered test proves. The \`plannedTestId\` and \`defectId\` descriptions in the schema say what closes it. Answer an objection in one line.
609
611
 
610
612
  ${hasRelatedRepos
611
613
  ? `
@@ -19,13 +19,17 @@ export declare function unknownAnswerObjections(objections: Objection[], acknowl
19
19
  * leave the bug. `verifierContracts.ts` carries the evidence. */
20
20
  export declare const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
21
21
  export declare function isNonAnswerable(objectionId: unknown): boolean;
22
- /** The objection a sentence alone does not close either. A change with no test
23
- * needs the test, unless this run could not write oneso the answer closes it
24
- * only when it also names what stopped the run. Measured on run 34176038240:
25
- * across eight fixtures 45 objections were raised and 45 were closed, and the one
26
- * class that a sentence cannot close was never raised at all. Prose closed every
27
- * uncovered change, including changes nothing prevented a test from reaching. */
28
- export declare const BLOCKER_ONLY_PREFIX = "coverage:change:";
22
+ /** The objections a sentence alone does not close either. A change with no test
23
+ * needs the test, and a defect with no test needs the test a defect the code
24
+ * already had before this pull request included. Either one closes only when the
25
+ * answer also names what stopped the test: a service that is not running, a
26
+ * branch that no longer exists, the one credential the run holds. Measured on run
27
+ * 34176038240: across eight fixtures 45 objections were raised and 45 were
28
+ * closed, and the one class that a sentence cannot close was never raised at all.
29
+ * Prose closed every uncovered change, including changes nothing prevented a test
30
+ * from reaching. Runs 34423214322 and 34423217796 then lost three defects the
31
+ * baseline reported: each was listed and closed by a sentence. */
32
+ export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "defects:untested:"];
29
33
  export declare function needsBlocker(objectionId: unknown): boolean;
30
34
  /** The objection raised below carries this prefix and the id it objects to. */
31
35
  export declare const REFUSED_ANSWER_PREFIX = "answers:refused:";
@@ -49,15 +49,19 @@ export const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
49
49
  export function isNonAnswerable(objectionId) {
50
50
  return typeof objectionId === "string" && objectionId.startsWith(NON_ANSWERABLE_PREFIX);
51
51
  }
52
- /** The objection a sentence alone does not close either. A change with no test
53
- * needs the test, unless this run could not write oneso the answer closes it
54
- * only when it also names what stopped the run. Measured on run 34176038240:
55
- * across eight fixtures 45 objections were raised and 45 were closed, and the one
56
- * class that a sentence cannot close was never raised at all. Prose closed every
57
- * uncovered change, including changes nothing prevented a test from reaching. */
58
- export const BLOCKER_ONLY_PREFIX = "coverage:change:";
52
+ /** The objections a sentence alone does not close either. A change with no test
53
+ * needs the test, and a defect with no test needs the test a defect the code
54
+ * already had before this pull request included. Either one closes only when the
55
+ * answer also names what stopped the test: a service that is not running, a
56
+ * branch that no longer exists, the one credential the run holds. Measured on run
57
+ * 34176038240: across eight fixtures 45 objections were raised and 45 were
58
+ * closed, and the one class that a sentence cannot close was never raised at all.
59
+ * Prose closed every uncovered change, including changes nothing prevented a test
60
+ * from reaching. Runs 34423214322 and 34423217796 then lost three defects the
61
+ * baseline reported: each was listed and closed by a sentence. */
62
+ export const BLOCKER_ONLY_PREFIXES = ["coverage:change:", "defects:untested:"];
59
63
  export function needsBlocker(objectionId) {
60
- return typeof objectionId === "string" && objectionId.startsWith(BLOCKER_ONLY_PREFIX);
64
+ return typeof objectionId === "string" && BLOCKER_ONLY_PREFIXES.some((prefix) => objectionId.startsWith(prefix));
61
65
  }
62
66
  /** The objection raised below carries this prefix and the id it objects to. */
63
67
  export const REFUSED_ANSWER_PREFIX = "answers:refused:";
@@ -84,11 +88,11 @@ export function refusedAnswerObjections(objections, acknowledged) {
84
88
  objectionId: `${REFUSED_ANSWER_PREFIX}${id}`,
85
89
  verifier: "answers",
86
90
  message: needsBlocker(id)
87
- ? "The answer says why this change has no test, and it does not say what stopped this run from writing one, so the objection stays open."
91
+ ? "The answer says why there is no test, and it does not say what stopped this run from writing one, so the objection stays open."
88
92
  : "This objection is closed only by a planned test, so the answer closed nothing and the objection stays open.",
89
93
  evidence: `answered "${id}"${needsBlocker(id) ? " with no `blocker`" : ", which no answer closes"}`,
90
94
  suggestion: needsBlocker(id)
91
- ? "Plan a test for this change and register the plan again. If this run cannot write one, send the same answer with `blocker` naming what stopped it — a service that is not running, a paired branch that no longer exists, the one credential the run holds. A reason the change is not worth testing is not a blocker."
95
+ ? "Plan the test and register the plan again. If this run cannot write one, send the same answer with `blocker` naming what stopped it — a service that is not running, a paired branch that no longer exists, the one credential the run holds. A reason it is not worth testing is not a blocker."
92
96
  : "Add a planned test that names the record's state as `startState` and runs this route's mutation on it, then register the plan again. Every other objection takes an answer; this one takes a test.",
93
97
  });
94
98
  }
@@ -0,0 +1,18 @@
1
+ /** Called by every entry point that renders the testbot prompt, with the values it
2
+ * renders. */
3
+ export declare function recordPullRequestText(title: unknown, description: unknown): void;
4
+ /** UNDEFINED when this process never rendered the prompt — a caller that drives the
5
+ * tools directly, or a test. A check that reads the pull request stays silent then;
6
+ * it cannot tell an empty pull request from one it never saw. */
7
+ export declare function pullRequestText(): {
8
+ title: string;
9
+ description: string;
10
+ } | undefined;
11
+ export declare function clearPullRequestText(): void;
12
+ /** The pull request as one searchable string. Whitespace runs fold to one space, so
13
+ * a quote wrapped across lines still matches the sentence it was taken from. */
14
+ export declare const searchable: (pr: {
15
+ title: string;
16
+ description: string;
17
+ }) => string;
18
+ export declare const appearsIn: (haystack: string, needle: string) => boolean;
@@ -0,0 +1,31 @@
1
+ /** The pull request's own title and description, as the run rendered its prompt with.
2
+ *
3
+ * IN PROCESS, never the state file: the prompt and the plan tool are registered on
4
+ * the same server, so the text is already here, and writing it would put the pull
5
+ * request's prose on disk once per registration for a reader in the same process.
6
+ *
7
+ * ONE SLOT, last write wins. A run renders the prompt once, and a second render is
8
+ * a second run whose text replaces the first. */
9
+ let recorded;
10
+ const asText = (value) => (typeof value === "string" ? value : "");
11
+ /** Called by every entry point that renders the testbot prompt, with the values it
12
+ * renders. */
13
+ export function recordPullRequestText(title, description) {
14
+ recorded = { title: asText(title), description: asText(description) };
15
+ }
16
+ /** UNDEFINED when this process never rendered the prompt — a caller that drives the
17
+ * tools directly, or a test. A check that reads the pull request stays silent then;
18
+ * it cannot tell an empty pull request from one it never saw. */
19
+ export function pullRequestText() {
20
+ return recorded;
21
+ }
22
+ export function clearPullRequestText() {
23
+ recorded = undefined;
24
+ }
25
+ /** The pull request as one searchable string. Whitespace runs fold to one space, so
26
+ * a quote wrapped across lines still matches the sentence it was taken from. */
27
+ export const searchable = (pr) => `${pr.title}\n${pr.description}`.replace(/\s+/g, " ").trim().toLowerCase();
28
+ export const appearsIn = (haystack, needle) => {
29
+ const folded = needle.replace(/\s+/g, " ").trim().toLowerCase();
30
+ return folded.length > 0 && haystack.includes(folded);
31
+ };
@@ -1,10 +1,14 @@
1
- import { Objection, ObjectionAnswer, PlanChange, PlanInput, PlannedTest, VerifyContext } from "./types.js";
1
+ import { Objection, ObjectionAnswer, PlanChange, PlanDefect, PlanInput, PlannedTest, VerifyContext } from "./types.js";
2
2
  /** The plan as stored for the run. Every registration produces one; the newest
3
3
  * replaces the last. */
4
4
  export interface Plan {
5
5
  /** The changes the agent said the diff makes. Published in the report
6
6
  * beside the tests delivered against each one. */
7
7
  changes: PlanChange[];
8
+ /** The defects the code review found. skyramp_submit_report reads them to hold
9
+ * the report to the review: every one is an issue, and every issue names the
10
+ * test that proves it. */
11
+ defects: PlanDefect[];
8
12
  plannedTests: PlannedTest[];
9
13
  /** Objections the agent answered, with the answers. */
10
14
  answeredObjections: Array<{
@@ -53,12 +53,13 @@ export function registerPlan(registration, ctx, previous) {
53
53
  const acknowledged = Array.isArray(registration.answers) ? registration.answers : [];
54
54
  const plannedTests = Array.isArray(registration.plannedTests) ? registration.plannedTests : [];
55
55
  const changes = Array.isArray(registration.changes) ? registration.changes : [];
56
+ const defects = Array.isArray(registration.defects) ? registration.defects : [];
56
57
  const registrationNumber = storedRegistrationNumber(registration);
57
58
  // The verifiers see the same normalised fields the STORED plan gets, not the raw
58
59
  // registration: handing them the raw one let a malformed top-level field crash
59
60
  // every check at once, giving eight `:crashed` objections and no real ones.
60
61
  // Crash containment is for a malformed field INSIDE a planned test.
61
- const verified = runPlanTimeVerifiers({ ...registration, plannedTests, changes, answers: acknowledged }, ctx);
62
+ const verified = runPlanTimeVerifiers({ ...registration, plannedTests, changes, defects, answers: acknowledged }, ctx);
62
63
  const objections = [
63
64
  ...verified,
64
65
  ...unknownAnswerObjections(verified, acknowledged, {
@@ -107,6 +108,7 @@ export function registerPlan(registration, ctx, previous) {
107
108
  : {}),
108
109
  plan: {
109
110
  changes,
111
+ defects,
110
112
  plannedTests,
111
113
  answeredObjections,
112
114
  unverifiedCloses,
@@ -6,7 +6,10 @@ import { expectedOutcomeAtPlanTime } from "./verifiers/expectedOutcome.js";
6
6
  import { uiElementGrounded } from "./verifiers/uiElementGrounded.js";
7
7
  import { screenRoute } from "./verifiers/screenRoute.js";
8
8
  import { coverage } from "./verifiers/coverage.js";
9
+ import { expectedValueSourced } from "./verifiers/expectedValueSourced.js";
9
10
  import { removedElementGuarded } from "./verifiers/removedElementGuarded.js";
11
+ import { requirementSourced } from "./verifiers/requirementSourced.js";
12
+ import { defects } from "./verifiers/defects.js";
10
13
  /** Verifiers that need only the plan and the run's facts. The order is the order
11
14
  * objections are reported in; it carries no priority. */
12
15
  export const PLAN_TIME_VERIFIERS = [
@@ -18,7 +21,10 @@ export const PLAN_TIME_VERIFIERS = [
18
21
  uiElementGrounded,
19
22
  screenRoute,
20
23
  coverage,
24
+ expectedValueSourced,
21
25
  removedElementGuarded,
26
+ requirementSourced,
27
+ defects,
22
28
  ];
23
29
  /** How a crashed verifier reads back. The id is the verifier's name plus a fixed
24
30
  * word, so it stays stable across registrations and can be answered. */
@@ -23,6 +23,9 @@ export interface PlanChange {
23
23
  id: string;
24
24
  text: string;
25
25
  source: string;
26
+ /** The requirement in the pull request's own words, when `source` names the pull
27
+ * request. Checked against the text the run rendered its prompt with. */
28
+ quote?: string;
26
29
  /** Where a user or caller meets this change. Every change states it: an optional
27
30
  * field the agent skips gives the check nothing to read. */
28
31
  surfaces: Array<"api" | "page">;
@@ -34,8 +37,26 @@ export interface PlanChange {
34
37
  value?: string | number | boolean | null;
35
38
  absent?: true;
36
39
  expect: "accept" | "reject";
40
+ /** What the response must carry when this case is sent. Declared here so the
41
+ * test asserts a value the plan decided, not the value the running app
42
+ * happened to return — the one source that always agrees with a defect. */
43
+ expectedValue?: string | number | boolean | null;
44
+ /** Where the agent read `expectedValue`: `pr-description`, `spec:<path>`,
45
+ * `convention:<file:line>`, or `code`. */
46
+ expectedFrom?: string;
37
47
  }>;
38
48
  }
49
+ /** One defect the code review found, as the agent read it. Nothing on the server
50
+ * parses the description or opens the file: the check reads the id, and a test
51
+ * that cites it and expects to fail is what proves it. */
52
+ export interface PlanDefect {
53
+ id: string;
54
+ file: string;
55
+ /** Advisory. Line numbers drift, so nothing checks it. */
56
+ line?: number;
57
+ description: string;
58
+ severity: "critical" | "high" | "medium" | "low";
59
+ }
39
60
  /** What a planned test states so a verifier has a checkable change. Kept in step with
40
61
  * `declarationFieldsSchema`, which the plan tool asserts against this type. */
41
62
  export interface PlannedTestDetails {
@@ -74,6 +95,9 @@ export interface PlannedTestDetails {
74
95
  };
75
96
  /** The ids of the declared changes this test proves. */
76
97
  changes?: string[];
98
+ /** The ids of the declared defects this test proves. A test proves a defect
99
+ * only when it expects to fail; the defects check reads both. */
100
+ defects?: string[];
77
101
  /** The state the mutated record is in before the mutation. Presence only. */
78
102
  startState?: string;
79
103
  /** UI planned tests only. `items` entries are COPIES of elements a capture of
@@ -103,6 +127,10 @@ export interface ObjectionAnswer {
103
127
  export interface PlanInput {
104
128
  /** The changes the diff makes. Each planned test cites the ones it tests. */
105
129
  changes: PlanChange[];
130
+ /** The defects the code review found. Each planned test cites the ones it
131
+ * proves. Empty is a statement — the review found none — and draws an
132
+ * objection the agent answers. */
133
+ defects: PlanDefect[];
106
134
  plannedTests: PlannedTest[];
107
135
  answers: ObjectionAnswer[];
108
136
  /** The tool owns this and overwrites whatever the caller sends. */
@@ -141,6 +169,13 @@ export interface VerifyContext {
141
169
  /** Identifiers a rename replaced, empty when the diff renames nothing. Only the
142
170
  * server pairs the two sides of a diff. */
143
171
  retiredUiElements: RemovedUiElement[];
172
+ /** The pull request's title and description, as the run rendered its prompt with.
173
+ * The plan tool fills it from the process the prompt was rendered in. Both fields
174
+ * blank MEANS the run had no title and no description. */
175
+ pullRequest: {
176
+ title: string;
177
+ description: string;
178
+ };
144
179
  /** Whether a cited path names a file in any of the run's repositories. Injected
145
180
  * to keep the verifiers filesystem-free. */
146
181
  citedFileExists(relativePath: string): boolean;
@@ -17,8 +17,6 @@ export interface ContractObjection {
17
17
  export interface VerifierContract {
18
18
  /** The verifier's own `Verifier.name`. */
19
19
  readonly id: string;
20
- /** What the registered plan has to carry for the check to have anything to read. */
21
- readonly declarationFields: readonly string[];
22
20
  /** One entry per message the verifier can emit, keyed by the case. A key naming a
23
21
  * segment of the `objectionId` uses that segment's spelling. The keys are widened
24
22
  * here so every contract satisfies the interface, while each contract is `as
@@ -31,7 +29,6 @@ export interface VerifierContract {
31
29
  }
32
30
  export declare const CHANGED_FILE_CONTRACT: {
33
31
  readonly id: "changedFile";
34
- readonly declarationFields: readonly ["declarations.changedFile", "declarations.screenEvidence.file"];
35
32
  readonly objections: {
36
33
  readonly notStated: {
37
34
  readonly message: "This test does not state which changed file it targets.";
@@ -54,7 +51,6 @@ export declare const CHANGED_FILE_CONTRACT: {
54
51
  };
55
52
  export declare const SCREEN_ROUTE_CONTRACT: {
56
53
  readonly id: "screenRoute";
57
- readonly declarationFields: readonly ["declarations.elements.pageUrl", "declarations.screenEvidence.file", "declarations.changedFile"];
58
54
  readonly objections: {
59
55
  readonly mismatch: {
60
56
  readonly message: "The page this UI test opens is not a page that renders the changed file it is about.";
@@ -69,7 +65,6 @@ export declare const SCREEN_ROUTE_CONTRACT: {
69
65
  };
70
66
  export declare const ENDPOINT_GROUNDED_CONTRACT: {
71
67
  readonly id: "endpointGrounded";
72
- readonly declarationFields: readonly ["steps[]", "testType", "declarations.routes"];
73
68
  readonly objections: {
74
69
  readonly uncited: {
75
70
  readonly message: "A call this test makes cites no file that declares it.";
@@ -88,7 +83,6 @@ export declare const ENDPOINT_GROUNDED_CONTRACT: {
88
83
  };
89
84
  export declare const STATED_DIFFERENCE_CONTRACT: {
90
85
  readonly id: "statedDifference";
91
- readonly declarationFields: readonly ["steps[]", "testType", "scenarioName", "declarations.differsFrom"];
92
86
  readonly objections: {
93
87
  readonly unexplainedPair: {
94
88
  readonly message: "Another planned test in this plan tests the same endpoint and neither says how they differ.";
@@ -107,7 +101,6 @@ export declare const STATED_DIFFERENCE_CONTRACT: {
107
101
  };
108
102
  export declare const EXISTING_COVERAGE_CONTRACT: {
109
103
  readonly id: "existingCoverage";
110
- readonly declarationFields: readonly ["declarations.existingTests"];
111
104
  readonly objections: {
112
105
  readonly citedTestMissing: {
113
106
  readonly message: "This planned test names an existing test file that is not in the checkout.";
@@ -118,7 +111,6 @@ export declare const EXISTING_COVERAGE_CONTRACT: {
118
111
  };
119
112
  export declare const EXPECTED_OUTCOME_CONTRACT: {
120
113
  readonly id: "expectedOutcome";
121
- readonly declarationFields: readonly ["category", "declarations.expected.outcome", "declarations.expected.why"];
122
114
  readonly objections: {
123
115
  readonly passInShouldFailCategory: {
124
116
  readonly message: "This planned test is categorised \"{category}\" but expects to pass on the app as it stands.";
@@ -133,7 +125,6 @@ export declare const EXPECTED_OUTCOME_CONTRACT: {
133
125
  };
134
126
  export declare const UI_ELEMENT_GROUNDED_CONTRACT: {
135
127
  readonly id: "uiElementGrounded";
136
- readonly declarationFields: readonly ["testType", "steps[]", "declarations.elements.items", "declarations.elements.pageUrl", "declarations.asserts", "scenarioName", "description", "declarations.expected.why"];
137
128
  readonly objections: {
138
129
  readonly nogrounding: {
139
130
  readonly message: "This UI test names no `elements.items`, so nothing says the elements it targets are on the page.";
@@ -156,7 +147,6 @@ export declare const UI_ELEMENT_GROUNDED_CONTRACT: {
156
147
  };
157
148
  export declare const COVERAGE_CONTRACT: {
158
149
  readonly id: "coverage";
159
- readonly declarationFields: readonly ["changes", "declarations.changes", "declarations.changedFile", "declarations.routes", "declarations.screenEvidence.file", "declarations.startState", "changes[].cases", "changes[].surfaces"];
160
150
  readonly objections: {
161
151
  readonly change: {
162
152
  readonly message: "This declared change has no test in the plan.";
@@ -195,7 +185,6 @@ export declare const COVERAGE_CONTRACT: {
195
185
  };
196
186
  export declare const REMOVED_ELEMENT_GUARDED_CONTRACT: {
197
187
  readonly id: "removedElementGuarded";
198
- readonly declarationFields: readonly ["declarations.elements.items", "declarations.asserts"];
199
188
  readonly objections: {
200
189
  readonly missing: {
201
190
  readonly message: "The diff removes `{element}` and no planned test says it is gone.";
@@ -208,6 +197,100 @@ export declare const REMOVED_ELEMENT_GUARDED_CONTRACT: {
208
197
  };
209
198
  readonly suggestion: "Does every element this diff removes have a planned test that asserts it is gone, and does no test claim a renamed identifier is gone?";
210
199
  };
200
+ export declare const EXPECTED_VALUE_SOURCED_CONTRACT: {
201
+ readonly id: "expectedValueSourced";
202
+ readonly objections: {
203
+ readonly sourcedFromCode: {
204
+ readonly message: "This case expects a value it read from the code as it stands, so a defect in that code becomes what the test demands.";
205
+ readonly suggestion: "Read the value from the pull request title or description, from a requirements file the description names, or from a convention the application already follows elsewhere, and name that source. If none of them states it, leave `expectedValue` out and assert the shape instead.";
206
+ };
207
+ readonly missingSource: {
208
+ readonly message: "This case states an expected value and does not say where the value came from.";
209
+ readonly suggestion: "Set `expectedFrom` to `pr-title`, `pr-description`, `spec:<path>`, `convention:<file:line>` or `code`.";
210
+ };
211
+ readonly unreadableSource: {
212
+ readonly message: "This case names a file as the source of its expected value, and that file is not in the checkout.";
213
+ readonly suggestion: "Give the path as the repository spells it, from the repository root. If the file is not there, the value has no source: read it from the pull request title or description instead, or leave `expectedValue` out and assert the shape.";
214
+ };
215
+ readonly changedConvention: {
216
+ readonly message: "This case reads its expected value from a line this pull request writes, so the same change decides both the behaviour and what counts as correct.";
217
+ readonly suggestion: "Cite a file the pull request leaves alone, or read the value from the description or a requirements file. If the cited file is changed but has nothing to do with this case, say so as the answer.";
218
+ };
219
+ readonly quoteMissing: {
220
+ readonly message: "This case reads its value from the pull request, and its change does not quote the sentence that states the rule.";
221
+ readonly suggestion: "Put the sentence from the title or description on the change as `quote`, as written there. Derive this case's value from that rule; the value itself need not appear in the pull request.";
222
+ };
223
+ readonly failWithoutValue: {
224
+ readonly message: "This test expects to fail, and no case on the changes it cites states the value it must assert.";
225
+ readonly suggestion: "Put the value the code does not return today on the case this test sends, as `expectedValue`, and name where you read it. If no source states the value, answer with what the test asserts instead and where that comes from.";
226
+ };
227
+ };
228
+ readonly suggestion: "Where does the correct value for this case come from?";
229
+ };
230
+ export declare const REQUIREMENT_SOURCED_CONTRACT: {
231
+ readonly id: "requirementSourced";
232
+ readonly objections: {
233
+ readonly unsourced: {
234
+ readonly message: "Change `{change}` does not name a source the plan accepts: `pr-title`, `pr-description`, `spec:<path>` or `diff`.";
235
+ readonly suggestion: "Set `source` on this change to `pr-title` or `pr-description`, to `spec:<path>` for the requirements file that states it, or to `diff`.";
236
+ };
237
+ readonly unreadable: {
238
+ readonly message: "Change `{change}` cites `{source}`, and the checkout holds no file at that path.";
239
+ readonly suggestion: "Cite the requirements file by the path the checkout holds, or name the source you actually read this change in.";
240
+ };
241
+ readonly fromDiff: {
242
+ readonly message: "This test reports a requirement conflict, and none of the changes it cites was read from the pull request or a requirements file.";
243
+ readonly suggestion: "Cite a change you read in the pull request or in a requirements file, or report this test as something other than a requirement conflict.";
244
+ };
245
+ readonly notNamed: {
246
+ readonly message: "Change `{change}` cites `{source}`, and the pull request names no such file.";
247
+ readonly suggestion: "Cite a file the title or description names. If neither names it, you read the change in the repository, so set `source` to `diff`.";
248
+ };
249
+ readonly emptyPullRequest: {
250
+ readonly message: "Change `{change}` says it was read from the pull request, and the pull request has no title or description.";
251
+ readonly suggestion: "Set `source` to `spec:<path>` for the requirements file that states this change, or to `diff` if only the code states it.";
252
+ };
253
+ readonly notQuoted: {
254
+ readonly message: "This test reports a requirement conflict, and change `{change}` quotes words the pull request does not carry.";
255
+ readonly suggestion: "Quote the requirement as the title or description spells it, word for word. If you cannot find those words there, the change came from somewhere else: say where in `source`.";
256
+ };
257
+ readonly unquoted: {
258
+ readonly message: "This test reports a requirement conflict, and change `{change}` does not quote the requirement.";
259
+ readonly suggestion: "Set `quote` on that change to the sentence in the title or description that states the requirement.";
260
+ };
261
+ readonly noChange: {
262
+ readonly message: "This test reports a requirement conflict and cites no change, so nothing says where the requirement came from.";
263
+ readonly suggestion: "Name in `changes` the change that states the requirement, or report this test as something other than a requirement conflict.";
264
+ };
265
+ };
266
+ readonly suggestion: "Does every change name where you read it, and does every requirement-conflict test cite a change that came from the pull request or a requirements file?";
267
+ };
268
+ export declare const DEFECTS_CONTRACT: {
269
+ readonly id: "defects";
270
+ readonly objections: {
271
+ readonly none: {
272
+ readonly message: "The plan declares no defects, so nothing says the code review happened before the plan.";
273
+ readonly suggestion: "Review the code that serves each change — the handler, the functions it calls to read or write data, the component and what it calls — and list each defect in `defects`. If the review found none, answer in one line what you read.";
274
+ };
275
+ readonly untested: {
276
+ readonly message: "This declared defect has no test in the plan.";
277
+ readonly suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If something stopped this run from writing the test, answer it and set `blocker` to what stopped it.";
278
+ };
279
+ readonly expectedPass: {
280
+ readonly message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.";
281
+ readonly suggestion: "Assert what the fixed code returns, set `expected.outcome` to `fail` with `why`, or answer in one line why a passing test is the proof.";
282
+ };
283
+ readonly unknownId: {
284
+ readonly message: "A planned test cites a defect id that this plan does not declare.";
285
+ readonly suggestion: "Spell the defect's `id` exactly as your own `defects` list gives it — the match is exact apart from surrounding space. Declare the defect if it is missing.";
286
+ };
287
+ readonly conflictWithoutDefect: {
288
+ readonly message: "This test reports a requirement conflict and cites no defect.";
289
+ readonly suggestion: "List the conflict in `defects`: the file and line that contradict the requirement, and a description that quotes the requirement. Cite that id from this test.";
290
+ };
291
+ };
292
+ readonly suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?";
293
+ };
211
294
  /** Fills a contract sentence's `{placeholder}` spans from the run's or the
212
295
  * planned test's own values. A placeholder with no value is left as written rather
213
296
  * than blanked, so a missed key shows up as `{knob}` instead of reading as a gap