ccqa 1.58.1 → 1.58.3

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.
package/dist/bin/ccqa.mjs CHANGED
@@ -7868,7 +7868,7 @@ function reuseFirstContract(hasDraft, draftInvariant) {
7868
7868
  "3. **Match the examples.** Write in the same style as the conventions examples: naming, structure, assertion phrasing, import layout. Where you follow an existing file rather than the plain reading of a guideline — a waiting pattern, a locator shape, a teardown structure — say which file, by path, in the comment. \"Following the existing implementation\" without naming it cannot be checked by the next reader, and it is how a pattern that was right once outlives the reason for it."
7869
7869
  ];
7870
7870
  if (hasDraft) {
7871
- rules.push("4. **The draft is ground truth.** The mechanical draft below encodes the recorded route. Do not change the meaning of its operation sequence or its assertions — no reordering, dropping, or weakening. You may rewrite locators when the conventions or the sources you read justify a better one.");
7871
+ rules.push("4. **The draft is ground truth for the route, not for the form of its checks.** Keep its operation sequence: no reordering or dropping of the actions the case needs. Its `expect` lines are what the recorder observed on the way, written in one fixed form (`await expect(...)`, one per observation). Which of them the test keeps, and in which form — hard or soft, where the check lives, how the locator is scoped — is decided by the case's expected results and the project's conventions, never by the draft. Where the conventions and the draft disagree, follow the conventions. What an expected result claims must still be checked: never loosen the value it states.");
7872
7872
  if (draftInvariant) rules.push(`5. ${draftInvariant}`);
7873
7873
  }
7874
7874
  return `## Reuse contract\n\n${rules.join("\n")}`;
@@ -7916,6 +7916,8 @@ function buildLlmGenPrompt(input) {
7916
7916
  sections.push(input.taskInstructions);
7917
7917
  const steps = input.steps.map(formatStep).join("\n");
7918
7918
  sections.push(`## Test spec\n\nTitle: ${input.specTitle}\n\nSteps:\n${steps}`);
7919
+ const facts = caseFacts(input);
7920
+ if (facts) sections.push(facts);
7919
7921
  if (input.draft) sections.push(`## Mechanical draft (recorded ground truth)\n\nPath: ${input.draft.path}\n\n\`\`\`
7920
7922
  ` + input.draft.contents + "\n```");
7921
7923
  if (input.resources.length > 0) {
@@ -7936,6 +7938,19 @@ These are the project's rules for the code you write, not background reading. Co
7936
7938
  return sections.join("\n\n") + languageDirective(input.language);
7937
7939
  }
7938
7940
  /**
7941
+ * What the case states beyond its steps. Project conventions key rules on
7942
+ * these (which checks are the case's, where it may run), and the review that
7943
+ * follows already reads them — a rewrite without them can only guess.
7944
+ */
7945
+ function caseFacts(input) {
7946
+ const parts = [];
7947
+ if (input.expectations && input.expectations.length > 0) parts.push(`### Expected results\n\nThe case states these for the flow as a whole. They are the checks the test exists for:\n\n${input.expectations.map((e) => `- ${body(e)}`).join("\n")}`);
7948
+ if (input.cleanup && input.cleanup.length > 0) parts.push(`### Cleanup steps\n\n${input.cleanup.map(formatStep).join("\n")}`);
7949
+ if (input.cleanupExpectations && input.cleanupExpectations.length > 0) parts.push(`### Expected results of the cleanup\n\n${input.cleanupExpectations.map((e) => `- ${body(e)}`).join("\n")}`);
7950
+ for (const c of input.context ?? []) parts.push(`### ${c.heading}\n\n${c.body.trim()}`);
7951
+ return parts.length === 0 ? "" : `## About this case\n\n${parts.join("\n\n")}`;
7952
+ }
7953
+ /**
7939
7954
  * Prompt for one iteration of the verification loop: the generated files
7940
7955
  * failed their run command; ask for corrected files under the same contract.
7941
7956
  */
@@ -8304,7 +8319,7 @@ function weakerTwin(file, source) {
8304
8319
  function decidesNothing(input) {
8305
8320
  const source = input.files.get(input.testPath);
8306
8321
  if (source === void 0 || input.caseText.join("").trim().length === 0) return [];
8307
- if (/\bexpect\(|\bjudgeByLlm\b/.test(source)) return [];
8322
+ if (/\bexpect(?:\.soft)?\(|\bjudgeByLlm\b/.test(source)) return [];
8308
8323
  return [{
8309
8324
  file: input.testPath,
8310
8325
  line: 1,
@@ -8373,7 +8388,7 @@ function containerOfPageText(file, source) {
8373
8388
  */
8374
8389
  function explained(all, i) {
8375
8390
  let above = i - 1;
8376
- while (above >= 0 && /\bexpect\(|^\s*$|^\s*\)/.test(all[above] ?? "")) above -= 1;
8391
+ while (above >= 0 && /\bexpect(?:\.soft)?\(|^\s*$|^\s*\)/.test(all[above] ?? "")) above -= 1;
8377
8392
  return /^\s*\/\//.test(all[above] ?? "");
8378
8393
  }
8379
8394
  /**
@@ -8388,7 +8403,7 @@ function explained(all, i) {
8388
8403
  function unjustifiedFirst(file, source) {
8389
8404
  const all = lines(source);
8390
8405
  return all.flatMap((text, i) => {
8391
- if (!/\bexpect\(/.test(text) || !/\.first\(\)/.test(text)) return [];
8406
+ if (!/\bexpect(?:\.soft)?\(/.test(text) || !/\.first\(\)/.test(text)) return [];
8392
8407
  if (explained(all, i)) return [];
8393
8408
  return [{
8394
8409
  file,
@@ -8463,7 +8478,7 @@ function assertedStrings(text, locators) {
8463
8478
  function unaskedAssertions(file, source, locators, said) {
8464
8479
  const all = lines(source);
8465
8480
  return all.flatMap((text, i) => {
8466
- if (!/\bexpect\(/.test(text)) return [];
8481
+ if (!/\bexpect(?:\.soft)?\(/.test(text)) return [];
8467
8482
  const strings = assertedStrings(text, locators);
8468
8483
  if (strings.length === 0 || strings.some((s) => said.includes(s))) return [];
8469
8484
  if (explained(all, i)) return [];
@@ -8710,16 +8725,14 @@ function rulesReview(guides) {
8710
8725
  ""
8711
8726
  ])
8712
8727
  ],
8713
- answerShape: "{ \"findings\": [ { \"stepId\": \"step-05\", \"problem\": \"…\" } ], \"ruleViolations\": [ { \"file\": \"…\", \"guide\": \"…\", \"rule\": \"…\", \"code\": \"…\", \"severity\": \"blocking\" } ] }",
8728
+ answerShape: "{ \"findings\": [ { \"stepId\": \"step-05\", \"problem\": \"…\" } ], \"ruleViolations\": [ { \"file\": \"…\", \"guide\": \"…\", \"rule\": \"…\", \"code\": \"…\" } ] }",
8714
8729
  answerNote: [
8715
8730
  "",
8716
8731
  "In `ruleViolations`: `file` is the file's path in this repository, and `code`",
8717
8732
  "the lines that break the rule. `guide` and `rule` are where the rule comes",
8718
8733
  "from and what it says — the document's path and its own words when you can",
8719
8734
  "quote one; otherwise the path of a file you read that shows the convention,",
8720
- "and what you counted (\"N of M files under <dir> do X\"). `severity` is",
8721
- "\"blocking\" when you would hold the change until it is fixed, and \"advisory\"",
8722
- "when it is worth saying but you would approve anyway. An empty",
8735
+ "and what you counted (\"N of M files under <dir> do X\"). An empty",
8723
8736
  "`ruleViolations` means every file follows every rule you could quote or",
8724
8737
  "count — write the key either way."
8725
8738
  ]
@@ -9150,8 +9163,7 @@ const ViolationSchema = z.object({
9150
9163
  file: z.string(),
9151
9164
  guide: z.string(),
9152
9165
  rule: z.string(),
9153
- code: z.string(),
9154
- severity: z.enum(["blocking", "advisory"]).catch("blocking")
9166
+ code: z.string()
9155
9167
  });
9156
9168
  const ViolationsSchema = z.object({ ruleViolations: z.array(z.unknown()) });
9157
9169
  /**
@@ -9239,14 +9251,11 @@ function formatFinding(finding, labels = evidenceLabels()) {
9239
9251
  }
9240
9252
  /**
9241
9253
  * The warning a rule violation becomes: the file, the rule in the words it was
9242
- * judged by, and where that came from. An advisory one says so — a reader must
9243
- * be able to tell a line that was worth a fix round from one that was not. The
9244
- * offending code is left out: it is what the fix pass needs, not what a reader
9245
- * scanning a log does.
9254
+ * judged by, and where that came from. The offending code is left out: it is
9255
+ * what the fix pass needs, not what a reader scanning a log does.
9246
9256
  */
9247
9257
  function formatViolation(violation) {
9248
- const advisory = violation.severity === "advisory" ? ", advisory" : "";
9249
- return `${violation.file}: ${violation.rule} (${violation.guide}${advisory})`;
9258
+ return `${violation.file}: ${violation.rule} (${violation.guide})`;
9250
9259
  }
9251
9260
  /**
9252
9261
  * Hand the written files to a reviewer who knows nothing about where they came
@@ -14789,6 +14798,10 @@ async function generateWithLlmEngine(req) {
14789
14798
  specTitle: ctx.spec.title,
14790
14799
  steps: req.steps,
14791
14800
  draft: req.draft,
14801
+ expectations: ctx.expectations,
14802
+ cleanup: ctx.cleanup,
14803
+ cleanupExpectations: ctx.cleanupExpectations,
14804
+ context: ctx.context ?? [],
14792
14805
  ...req.draftInvariant ? { draftInvariant: req.draftInvariant } : {},
14793
14806
  resources: resources.map(toPromptResource),
14794
14807
  conventionSections: conventions.sections,
@@ -15179,13 +15192,12 @@ function uncheckedSteps(review, alreadyAsked = /* @__PURE__ */ new Set()) {
15179
15192
  * arrived. Absent violations are absent either way — there is nothing here to
15180
15193
  * act on when that half never came.
15181
15194
  *
15182
- * Only what the reviewer would hold the change for. It is asked to sort its
15183
- * own violations, because the alternative is a round spent on a line it would
15184
- * have approved anyway. Every violation is still reported: `warnings` carries
15185
- * the advisory ones too, marked as such.
15195
+ * Every violation the reviewer reports: a rule the project wrote down, or a
15196
+ * convention its files share, is the project's decision, not the reviewer's to
15197
+ * waive.
15186
15198
  */
15187
15199
  function guideViolations(review, alreadyAsked = /* @__PURE__ */ new Set()) {
15188
- const violations = (review?.ruleViolations ?? []).filter((v) => v.severity === "blocking" && !alreadyAsked.has(violationKey(v)));
15200
+ const violations = (review?.ruleViolations ?? []).filter((v) => !alreadyAsked.has(violationKey(v)));
15189
15201
  if (violations.length === 0) return null;
15190
15202
  return {
15191
15203
  exitCode: 1,
@@ -15474,7 +15486,7 @@ function headerPreserveRule(header, titleSuffix) {
15474
15486
  * rejection, which costs a whole round.
15475
15487
  */
15476
15488
  function stepCommentPreserveRule() {
15477
- return "**Keep every `test.step(...)` block, with its title exactly as the draft wrote it.** Those titles are how the evidence table, the run's per-step screenshots and the review of this test say which assertions belong to which step. A reshaped one parses as no step at all, and every step then reads as deciding nothing. Keep the wording, the numbering and the punctuation, and keep each step's actions inside its own block — a step's call to a page-object method belongs in the block, not outside it. Do not merge, split or reorder the blocks. A test already at this path may have been written by an older ccqa in a different shape: the draft is what this file must look like, not that one.";
15489
+ return "**Keep every `test.step(...)` block, with its title exactly as the draft wrote it.** Those titles are how the evidence table, the run's per-step screenshots and the review of this test say which assertions belong to which step. A reshaped one parses as no step at all, and every step then reads as deciding nothing. Keep the wording, the numbering and the punctuation, and keep each step's actions inside its own block — a step's call to a page-object method belongs in the block, not outside it. Do not merge, split or reorder the blocks. A test already at this path may have been written by an older ccqa in a different shape: the draft's step blocks are what this file must keep, not that one's.";
15478
15490
  }
15479
15491
  /**
15480
15492
  * Told to the rewrite, because the alternative failure is silent: a claim
@@ -26488,6 +26500,7 @@ async function runGenerateLocked(resolved, opts, cwd) {
26488
26500
  cleanup: testCase.cleanup,
26489
26501
  expectations: testCase.expectations,
26490
26502
  cleanupExpectations: testCase.cleanupExpectations,
26503
+ ...testCase.context.length > 0 ? { context: testCase.context } : {},
26491
26504
  fields: testCase.fields,
26492
26505
  cwd,
26493
26506
  testPath,
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
- record: "record";
39
38
  run: "run";
40
39
  drift: "drift";
40
+ record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
831
831
  declare const RunReportDataSchema: z.ZodObject<{
832
832
  schemaVersion: z.ZodLiteral<1>;
833
833
  kind: z.ZodDefault<z.ZodEnum<{
834
- record: "record";
835
834
  run: "run";
836
835
  drift: "drift";
836
+ record: "record";
837
837
  }>>;
838
838
  createdAt: z.ZodString;
839
839
  runId: z.ZodNullable<z.ZodString>;
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
- record: "record";
39
38
  run: "run";
40
39
  drift: "drift";
40
+ record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
831
831
  declare const RunReportDataSchema: z.ZodObject<{
832
832
  schemaVersion: z.ZodLiteral<1>;
833
833
  kind: z.ZodDefault<z.ZodEnum<{
834
- record: "record";
835
834
  run: "run";
836
835
  drift: "drift";
836
+ record: "record";
837
837
  }>>;
838
838
  createdAt: z.ZodString;
839
839
  runId: z.ZodNullable<z.ZodString>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.58.1",
3
+ "version": "1.58.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.58.1",
3
+ "version": "1.58.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {