@cspeach/cli 1.1.18 → 1.1.19

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 (224) hide show
  1. package/README.md +3 -4
  2. package/dist/agent/loop.js +143 -25
  3. package/dist/agent/providers/local-provider.js +4 -1
  4. package/dist/agent/repair-partial.js +76 -19
  5. package/dist/agent/tool-dispatch.js +58 -21
  6. package/dist/agent/turn-error-ux.js +77 -26
  7. package/dist/agent/turn-stream.js +25 -3
  8. package/dist/approvals/adt-type.js +27 -0
  9. package/dist/approvals/advisory-prompt.js +48 -0
  10. package/dist/approvals/advisory-render.js +25 -11
  11. package/dist/approvals/approval-prompt.js +74 -24
  12. package/dist/approvals/jwt.js +2 -1
  13. package/dist/approvals/render.js +64 -36
  14. package/dist/approvals/risk-floor.js +53 -2
  15. package/dist/auth/api-key.js +30 -16
  16. package/dist/auth/keychain.js +0 -0
  17. package/dist/auth/me.js +25 -7
  18. package/dist/cli.js +25 -6
  19. package/dist/commands/compact.js +12 -9
  20. package/dist/commands/config-set.js +6 -2
  21. package/dist/commands/config-show.js +7 -5
  22. package/dist/commands/cost.js +41 -34
  23. package/dist/commands/help.js +88 -50
  24. package/dist/commands/login.js +28 -19
  25. package/dist/commands/logout.js +7 -3
  26. package/dist/commands/plan-chain.js +71 -77
  27. package/dist/commands/plan-continue.js +1 -0
  28. package/dist/commands/plan-resume.js +20 -12
  29. package/dist/commands/whoami.js +23 -9
  30. package/dist/config/loader.js +15 -3
  31. package/dist/cost/pricing.js +6 -5
  32. package/dist/doctor/checks/forge-rules.js +13 -3
  33. package/dist/doctor/checks/keychain.js +6 -4
  34. package/dist/doctor/run.js +101 -20
  35. package/dist/index.js +5 -2
  36. package/dist/lib/graceful-exit.js +56 -0
  37. package/dist/lib/piped-prompt.js +65 -0
  38. package/dist/lock-contention.js +2 -2
  39. package/dist/one-shot.js +135 -40
  40. package/dist/projects/answer-blockers.js +6 -1
  41. package/dist/projects/handover-md.js +15 -14
  42. package/dist/projects/plan-run.js +97 -54
  43. package/dist/projects/promote-command.js +11 -2
  44. package/dist/projects/save-command.js +3 -2
  45. package/dist/projects/status.js +2 -1
  46. package/dist/projects/yes-no.js +12 -0
  47. package/dist/renderer/abap-inline.js +18 -14
  48. package/dist/renderer/answer-trim.js +69 -0
  49. package/dist/renderer/banners.js +8 -8
  50. package/dist/renderer/brand-settled.js +19 -0
  51. package/dist/renderer/change-card.js +156 -0
  52. package/dist/renderer/checklist-format.js +130 -0
  53. package/dist/renderer/color-mode.js +45 -0
  54. package/dist/renderer/error-detail.js +104 -0
  55. package/dist/renderer/fence-state.js +46 -0
  56. package/dist/renderer/footer-line.js +96 -0
  57. package/dist/renderer/glyphs.js +25 -0
  58. package/dist/renderer/goodbye.js +38 -0
  59. package/dist/renderer/legacy-palette.js +41 -0
  60. package/dist/renderer/look.js +30 -0
  61. package/dist/renderer/markdown.js +299 -92
  62. package/dist/renderer/notice-log.js +72 -0
  63. package/dist/renderer/notice-shape.js +48 -0
  64. package/dist/renderer/notices.js +40 -8
  65. package/dist/renderer/panel-rows.js +16 -0
  66. package/dist/renderer/pipeline.js +66 -0
  67. package/dist/renderer/progress-chatter.js +23 -17
  68. package/dist/renderer/rendering-mode.js +34 -0
  69. package/dist/renderer/routing-line.js +14 -0
  70. package/dist/renderer/sanitize.js +12 -0
  71. package/dist/renderer/severity.js +2 -7
  72. package/dist/renderer/startup-lines.js +152 -0
  73. package/dist/renderer/status-footer.js +52 -34
  74. package/dist/renderer/steering-echo.js +41 -0
  75. package/dist/renderer/syntax.js +31 -12
  76. package/dist/renderer/tables.js +5 -1
  77. package/dist/renderer/theme.js +44 -0
  78. package/dist/renderer/thinking-heartbeat.js +33 -2
  79. package/dist/renderer/todo-block.js +10 -49
  80. package/dist/renderer/tool-labels.js +238 -0
  81. package/dist/renderer/tool-widget.js +245 -51
  82. package/dist/renderer/trace.js +42 -0
  83. package/dist/renderer/transcript-flow.js +132 -0
  84. package/dist/renderer/tty.js +34 -1
  85. package/dist/renderer/ui-width.js +42 -0
  86. package/dist/renderer/verify-chain.js +4 -2
  87. package/dist/renderer/widget-fallback.js +58 -65
  88. package/dist/repl/bracketed-paste.js +7 -1
  89. package/dist/repl/builtin-commands.js +19 -8
  90. package/dist/repl/credential-handover-gate.js +28 -0
  91. package/dist/repl/early-line-buffer.js +5 -2
  92. package/dist/repl/file-picker.js +54 -12
  93. package/dist/repl/ink-stdin-guard.js +66 -0
  94. package/dist/repl/inquirer-guard.js +59 -16
  95. package/dist/repl/inquirer-theme.js +27 -33
  96. package/dist/repl/paste-marker.js +19 -0
  97. package/dist/repl/plan-turn-end.js +20 -0
  98. package/dist/repl/post-turn-status.js +36 -11
  99. package/dist/repl/reroute-turn.js +21 -0
  100. package/dist/repl/reset-tty-stdin.js +44 -0
  101. package/dist/repl/restore-guard.js +22 -0
  102. package/dist/repl/resume-standalone.js +63 -0
  103. package/dist/repl/rule8-detector.js +11 -3
  104. package/dist/repl/safety-confirm.js +161 -93
  105. package/dist/repl/session-spend-line.js +8 -4
  106. package/dist/repl/themed-prompts.js +19 -0
  107. package/dist/repl/ui-look-command.js +75 -0
  108. package/dist/repl/update-method-preview-hook.js +48 -5
  109. package/dist/repl.js +795 -332
  110. package/dist/rewind/cli.js +3 -2
  111. package/dist/rewind/format.js +14 -9
  112. package/dist/rewind/restore.js +11 -0
  113. package/dist/router/classifier.js +47 -4
  114. package/dist/router/intent-extractor.js +24 -8
  115. package/dist/router/piped-routing.js +23 -0
  116. package/dist/router/resume-routing.js +16 -0
  117. package/dist/router/routing-failure.js +57 -0
  118. package/dist/sap/first-run-choice.js +1 -1
  119. package/dist/sap/onboarding.js +3 -2
  120. package/dist/sap/standalone-onboarding.js +1 -1
  121. package/dist/sap/system-info.js +4 -2
  122. package/dist/sap/unreachable.js +28 -0
  123. package/dist/sap-errors/clean-error-text.js +182 -0
  124. package/dist/session/interrupt-reason.js +39 -0
  125. package/dist/session/recap.js +36 -24
  126. package/dist/session/resume.js +104 -36
  127. package/dist/session/store.js +69 -27
  128. package/dist/session/user-prompt.js +21 -0
  129. package/dist/skill-catalog.js +7 -0
  130. package/dist/skills/bundled-skills.js +76 -69
  131. package/dist/standards/standards-init.js +1 -1
  132. package/dist/test-helpers/answer-any.js +46 -0
  133. package/dist/tools/approval.js +27 -11
  134. package/dist/tools/ask-question.js +47 -5
  135. package/dist/tools/filesystem/file-write.js +19 -2
  136. package/dist/tools/fiori/fe-extend.js +16 -1
  137. package/dist/tools/fiori/fe-scaffold.js +105 -3
  138. package/dist/tools/fiori/html-escapes.js +28 -0
  139. package/dist/tools/fiori/metadata/audit.js +605 -0
  140. package/dist/tools/fiori/metadata/references.js +104 -0
  141. package/dist/tools/fiori/metadata/types.js +8 -0
  142. package/dist/tools/fiori/preview/app-guard.js +215 -0
  143. package/dist/tools/fiori/preview/env.js +193 -0
  144. package/dist/tools/fiori/preview/readiness.js +127 -0
  145. package/dist/tools/fiori/preview/registry.js +412 -0
  146. package/dist/tools/fiori/preview/start.js +469 -0
  147. package/dist/tools/fiori/samples/loader.js +20 -5
  148. package/dist/tools/fiori/scaffold.js +18 -6
  149. package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
  150. package/dist/tools/fiori/smoke/app-driver.js +100 -0
  151. package/dist/tools/fiori/smoke/assertions.js +161 -24
  152. package/dist/tools/fiori/smoke/audit-columns.js +21 -0
  153. package/dist/tools/fiori/smoke/batch.js +273 -0
  154. package/dist/tools/fiori/smoke/draft-safety.js +241 -0
  155. package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
  156. package/dist/tools/fiori/smoke/driver.js +171 -4
  157. package/dist/tools/fiori/smoke/evidence.js +35 -0
  158. package/dist/tools/fiori/smoke/labels-codes.js +207 -0
  159. package/dist/tools/fiori/smoke/nav-error.js +26 -0
  160. package/dist/tools/fiori/smoke/run-smoke.js +155 -12
  161. package/dist/tools/fiori/tools.js +651 -7
  162. package/dist/tools/fiori/ui5-version.js +16 -0
  163. package/dist/tools/index.js +8 -0
  164. package/dist/tools/local-build.js +6 -0
  165. package/dist/tools/sap-read.js +16 -1
  166. package/dist/tools/sap-write.js +122 -10
  167. package/dist/tools/shell/shell_exec.js +30 -12
  168. package/dist/tools/syntax-state.js +67 -0
  169. package/dist/tools/todo.js +23 -18
  170. package/dist/tools/transport-fit.js +462 -0
  171. package/dist/tools/transport-resolution.js +3 -1
  172. package/dist/tools/transport.js +33 -7
  173. package/dist/tools/verify.js +184 -8
  174. package/dist/ui/app.js +284 -146
  175. package/dist/ui/approval-modal.js +41 -15
  176. package/dist/ui/ask-answer-text.js +17 -0
  177. package/dist/ui/body.js +25 -6
  178. package/dist/ui/card-slot.js +98 -0
  179. package/dist/ui/checklist.js +9 -0
  180. package/dist/ui/coaching-picker-classic.js +8 -5
  181. package/dist/ui/command-palette.js +6 -4
  182. package/dist/ui/confirm-request.js +18 -0
  183. package/dist/ui/context-grid.js +2 -1
  184. package/dist/ui/file-palette.js +6 -4
  185. package/dist/ui/files-card-emitter.js +19 -0
  186. package/dist/ui/footer-line.js +16 -0
  187. package/dist/ui/footer.js +105 -104
  188. package/dist/ui/input-wrap.js +122 -0
  189. package/dist/ui/interrupt.js +103 -0
  190. package/dist/ui/key-burst.js +117 -0
  191. package/dist/ui/line-resolution.js +4 -3
  192. package/dist/ui/live-area.js +71 -0
  193. package/dist/ui/login-banner.js +54 -43
  194. package/dist/ui/rewind-panel.js +76 -24
  195. package/dist/ui/role-props.js +47 -0
  196. package/dist/ui/sap-state-store.js +1 -0
  197. package/dist/ui/session-timeline.js +49 -3
  198. package/dist/ui/skill-picker.js +5 -47
  199. package/dist/ui/text-input.js +128 -24
  200. package/dist/ui/todo-emitter.js +19 -0
  201. package/dist/ui/turn-status-emitter.js +28 -2
  202. package/dist/ui/turn-status.js +16 -19
  203. package/dist/ui/use-card-request.js +44 -0
  204. package/dist/ui/widgets/ask-card.js +521 -0
  205. package/dist/ui/widgets/bar-chart.js +11 -5
  206. package/dist/ui/widgets/coaching-picker.js +50 -55
  207. package/dist/ui/widgets/confirm-card.js +193 -0
  208. package/dist/ui/widgets/dep-graph.js +3 -2
  209. package/dist/ui/widgets/diff-viewer.js +6 -5
  210. package/dist/ui/widgets/files-card.js +97 -0
  211. package/dist/ui/widgets/question-card.js +2 -1
  212. package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
  213. package/dist/ui/widgets/shortcuts-sheet.js +25 -0
  214. package/dist/ui/widgets/stack-frames.js +2 -1
  215. package/dist/upgrade-check.js +39 -8
  216. package/package.json +11 -2
  217. package/test-harness/fe-draft/app.ts +188 -0
  218. package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
  219. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
  220. package/test-harness/fe-draft/routes.ts +224 -0
  221. package/test-harness/fe-draft/routes.unit.test.ts +45 -0
  222. package/test-harness/fe-draft/server.ts +225 -0
  223. package/test-harness/fe-draft/variants.ts +196 -0
  224. package/vitest.smoke-e2e.config.ts +20 -0
@@ -24,6 +24,34 @@ import * as path from 'node:path';
24
24
  import { resolveBrowserExecutable } from './browser.js';
25
25
  import { openApp as realOpenApp } from './driver.js';
26
26
  import { checkBoot, checkOData, checkColumns, checkRows, checkExercise, } from './assertions.js';
27
+ import { deriveExpectedColumns } from './audit-columns.js';
28
+ import { checkLabels, judgeRawCodes, readRowTruths } from './labels-codes.js';
29
+ import { pairBatch } from './batch.js';
30
+ import { writeEvidence, auditSummary, SMOKE_VIEWPORT } from './evidence.js';
31
+ import { classifyNavError, NAV_TIMEOUT_MS, REFUSED_ATTEMPTS, REFUSED_BACKOFF_MS } from './nav-error.js';
32
+ /**
33
+ * L1-5a: the inner requests of every recorded `$batch`. A body that cannot be
34
+ * read is itself a failure (status 0), never dropped.
35
+ */
36
+ export async function innerBatchParts(page) {
37
+ if (!page.batchExchanges)
38
+ return [];
39
+ const out = [];
40
+ for (const ex of await page.batchExchanges()) {
41
+ try {
42
+ out.push(...pairBatch(ex));
43
+ }
44
+ catch (err) {
45
+ out.push({
46
+ method: '$batch',
47
+ url: '',
48
+ status: 0,
49
+ errorMessage: `could not read a $batch body (${err instanceof Error ? err.message : String(err)})`,
50
+ });
51
+ }
52
+ }
53
+ return out;
54
+ }
27
55
  /**
28
56
  * UI5 selectors used by the driver-side collection. ONE exported const so Track
29
57
  * 3 can extend it (add chart/form control selectors) without hunting through
@@ -32,8 +60,16 @@ import { checkBoot, checkOData, checkColumns, checkRows, checkExercise, } from '
32
60
  export const SMOKE_SELECTORS = {
33
61
  /** Responsive-table + grid-table column headers. */
34
62
  tableHeaders: '.sapMColumnHeader, [role="columnheader"]',
35
- /** Responsive-table + grid-table data rows. */
36
- tableRows: '.sapMListTbl tr.sapMListTblRow, .sapUiTableRow',
63
+ /**
64
+ * DATA rows only (L1-2b F-3): a responsive-table list item that is neither
65
+ * the header row nor the "no data" line, or a grid-table content row that is
66
+ * not a hidden filler row. The old selector counted the header and the
67
+ * "To start, set the relevant filters and choose Go" line as 2 rows.
68
+ */
69
+ tableRows: '.sapMListTbl tr.sapMListTblRow:not(.sapMListTblHeader):not([id$="-nodata"]), ' +
70
+ 'table.sapUiTableCtrlScroll tr.sapUiTableContentRow:not(.sapUiTableRowHidden)',
71
+ /** The table has finished loading and is empty (responsive no-data line / empty grid). */
72
+ emptyList: '.sapMListTbl tr[id$="-nodata"], .sapUiTableEmpty',
37
73
  /** Overview-page cards. */
38
74
  ovpCards: '.sapOvpCard',
39
75
  /** Freestyle chart control (sap.viz VizFrame) — Track 3 chart controlKind. */
@@ -89,8 +125,10 @@ function isRowsControl(kind) {
89
125
  */
90
126
  async function runExercises(page, exercises) {
91
127
  const results = [];
128
+ const pageErrors = () => (page.pageErrors ? page.pageErrors() : []);
92
129
  for (const ex of exercises) {
93
130
  const before = page.consoleErrors().length;
131
+ const beforePage = pageErrors().length;
94
132
  const label = ex.kind === 'press' ? ex.selectorHint : ex.name;
95
133
  let threw;
96
134
  try {
@@ -109,7 +147,8 @@ async function runExercises(page, exercises) {
109
147
  threw = err instanceof Error ? err.message : String(err);
110
148
  }
111
149
  const newConsoleErrors = page.consoleErrors().slice(before);
112
- results.push({ kind: ex.kind, label, threw, newConsoleErrors });
150
+ const newPageErrors = pageErrors().slice(beforePage);
151
+ results.push({ kind: ex.kind, label, threw, newConsoleErrors, newPageErrors });
113
152
  }
114
153
  return results;
115
154
  }
@@ -125,18 +164,49 @@ export async function runSmoke(spec, deps) {
125
164
  const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable());
126
165
  const open = deps.openApp ?? realOpenApp;
127
166
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
167
+ const startedAt = new Date();
168
+ const t0 = Date.now();
128
169
  // Skip path 2 — no browser.
129
170
  const browser = resolveBrowser();
130
171
  if (!browser) {
131
172
  return skipped('no Chromium-based browser found (set CSPEACH_BROWSER to an Edge/Chrome executable); no app was rendered');
132
173
  }
133
- // Skip path 3 — preview server unreachable (openApp throws on nav failure).
174
+ // Skip path 3 — the first navigation failed. L1-7: classified, never
175
+ // swallowed. Refused → the preview may still be starting: retry, then skip.
176
+ // Timeout → the app did not settle: a real `boot` FAILURE, never retried.
177
+ // Certificate → skip with the trust hint. Anything else → skip, named.
178
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
134
179
  let page;
135
- try {
136
- page = await open(browser.path, spec.appUrl, timeoutMs);
137
- }
138
- catch {
139
- return skipped(`preview server not reachable at ${spec.appUrl}; run npm run start first`);
180
+ for (let attempt = 1; !page; attempt++) {
181
+ try {
182
+ page = spec.capUser
183
+ ? await open(browser.path, spec.appUrl, timeoutMs, NAV_TIMEOUT_MS, { credentials: { username: spec.capUser, password: '' } })
184
+ : await open(browser.path, spec.appUrl, timeoutMs, NAV_TIMEOUT_MS);
185
+ }
186
+ catch (err) {
187
+ const nav = classifyNavError(err);
188
+ if (nav.kind === 'refused') {
189
+ if (attempt < REFUSED_ATTEMPTS) {
190
+ await sleep(REFUSED_BACKOFF_MS);
191
+ continue;
192
+ }
193
+ return skipped(`preview server not reachable at ${spec.appUrl}; run npm run start first`);
194
+ }
195
+ if (nav.kind === 'timeout') {
196
+ const shot = err.screenshotPath;
197
+ return {
198
+ verification: 'smoke',
199
+ passed: false,
200
+ checks: [{ name: 'boot', ok: false, detail: `page did not settle in ${NAV_TIMEOUT_MS / 1000} s` }],
201
+ ...(typeof shot === 'string' ? { screenshots: { failure: shot } } : {}),
202
+ };
203
+ }
204
+ if (nav.kind === 'cert') {
205
+ return skipped(`the browser does not trust the certificate at ${spec.appUrl} (${nav.message}); ` +
206
+ 'trust the SAP system certificate — run cspeach doctor for the steps');
207
+ }
208
+ return skipped(`could not open ${spec.appUrl}: ${nav.message}`);
209
+ }
140
210
  }
141
211
  // F-L2-10A evidence capture. The dir is created LAZILY on the first successful
142
212
  // capture, so a skip path (or a driver with no screenshot capability) writes
@@ -163,6 +233,10 @@ export async function runSmoke(spec, deps) {
163
233
  };
164
234
  // Hoisted so the finally block can decide whether a failure shot is owed.
165
235
  const checks = [];
236
+ // L1-8b: set once a real verdict exists — the finally block writes its evidence.
237
+ let result;
238
+ let batch = [];
239
+ const navMs = Date.now() - t0;
166
240
  try {
167
241
  // Skip path 4 — SAP logon page (deployed BSP / unauthenticated preview).
168
242
  // Detect via a logon-redirect URL OR the presence of a logon-form field, and
@@ -172,12 +246,37 @@ export async function runSmoke(spec, deps) {
172
246
  if (LOGON_URL.test(url) || logonHits.length > 0) {
173
247
  return skipped(`preview served a SAP logon page (not authenticated) at ${url}; run npm run start first`);
174
248
  }
249
+ // L1-2b F-2/F-3: a table is judged only once it has finished loading —
250
+ // data rows, or the empty-list marker. Navigation "settling" is not enough:
251
+ // UI5 loads the list after networkidle. A timeout falls through; the rows
252
+ // and odata checks then fail on what is (not) there.
253
+ if (isRowsControl(spec.controlKind) || spec.metadataAudit) {
254
+ try {
255
+ await page.waitForSelector(`${SMOKE_SELECTORS.tableRows}, ${SMOKE_SELECTORS.emptyList}`);
256
+ }
257
+ catch {
258
+ // judged below on what rendered
259
+ }
260
+ }
175
261
  // The app is open past the logon check — this is the boot evidence shot.
176
262
  await capture('boot', 'boot.png');
177
263
  // Real render — assert content. boot + odata always; columns only when the
178
264
  // spec asked for them; rows only for a rows-bearing (table) control.
179
- checks.push(checkBoot(page.consoleErrors()));
180
- checks.push(checkOData(page.failedRequests()));
265
+ // Dry run 2026-09-25: an "&amp;" in the header passed every check — boot
266
+ // also reads the visible text. A page that can't answer is judged as before.
267
+ let visibleText;
268
+ if (page.evalInPage) {
269
+ try {
270
+ const text = await page.evalInPage('function () { return document.body ? document.body.innerText : ""; }', null);
271
+ visibleText = typeof text === 'string' ? text : undefined;
272
+ }
273
+ catch {
274
+ visibleText = undefined;
275
+ }
276
+ }
277
+ checks.push(checkBoot(page.consoleErrors(), page.failedRequests(), visibleText));
278
+ batch = await innerBatchParts(page);
279
+ checks.push(checkOData(page.failedRequests(), page.observedRequests ? page.observedRequests() : [], batch, page.unansweredRequests ? page.unansweredRequests() : []));
181
280
  if (spec.expectedColumns && spec.expectedColumns.length > 0) {
182
281
  const headers = await page.queryTexts(SMOKE_SELECTORS.tableHeaders);
183
282
  checks.push(checkColumns(headers, spec.expectedColumns));
@@ -186,6 +285,28 @@ export async function runSmoke(spec, deps) {
186
285
  const rows = await page.queryTexts(SMOKE_SELECTORS.tableRows);
187
286
  checks.push(checkRows(rows.length, spec.allowEmptyRows === true));
188
287
  }
288
+ // L1-3: labels + no-raw-codes, judged against the audit (machine truth).
289
+ // Runs before the exercises, which may navigate away from the list.
290
+ if (spec.metadataAudit) {
291
+ const audit = spec.metadataAudit;
292
+ const expected = deriveExpectedColumns(audit);
293
+ let snapshot;
294
+ try {
295
+ snapshot = page.cellTextsByColumn ? await page.cellTextsByColumn(audit.entitySet) : undefined;
296
+ }
297
+ catch {
298
+ snapshot = undefined; // judged as "could not read the table" — a failure
299
+ }
300
+ checks.push(checkLabels(snapshot, expected, audit));
301
+ let truths;
302
+ try {
303
+ truths = snapshot ? await readRowTruths(page, audit, expected, snapshot) : undefined;
304
+ }
305
+ catch {
306
+ truths = undefined;
307
+ }
308
+ checks.push(judgeRawCodes(audit, expected, snapshot, truths));
309
+ }
189
310
  // Track 3 (dynamic A4): run each declared exercise through the SAME page
190
311
  // abstraction, diffing the driver's console-error collection around each
191
312
  // interaction. A press that throws or an exercise that emits a new console
@@ -200,17 +321,39 @@ export async function runSmoke(spec, deps) {
200
321
  // `screenshots` is the SAME object the finally block may still add the
201
322
  // failure entry to, so a failing run carries it even though the value is
202
323
  // built here. It rides along only when the driver can shoot at all.
203
- return {
324
+ result = {
204
325
  verification: 'smoke',
205
326
  passed: checks.every((c) => c.ok),
206
327
  checks,
207
328
  ...(canShoot ? { screenshots } : {}),
208
329
  };
330
+ return result;
209
331
  }
210
332
  finally {
211
333
  // A failing check earns a failure shot — captured BEFORE the page is closed.
212
334
  if (checks.some((c) => !c.ok))
213
335
  await capture('failure', 'failure.png');
214
336
  await page.close();
337
+ // L1-8b evidence pack — only for a real verdict (never on a skip path).
338
+ // `result` is the object the caller receives, so the path rides along.
339
+ if (result) {
340
+ const dir = shotDir ?? deps.artifactDir ?? path.join(os.homedir(), '.cspeach', 'smoke', `${appNameFromUrl(spec.appUrl)}-${runStamp()}`);
341
+ const file = writeEvidence(dir, {
342
+ tool: 'fiori_render_smoke',
343
+ appUrl: spec.appUrl,
344
+ startedAt: startedAt.toISOString(),
345
+ finishedAt: new Date().toISOString(),
346
+ viewport: SMOKE_VIEWPORT,
347
+ verification: result.verification,
348
+ passed: result.passed,
349
+ checks: result.checks,
350
+ audit: auditSummary(spec.metadataAudit),
351
+ batch,
352
+ screenshots: result.screenshots ?? {},
353
+ timings: { navigateMs: navMs, totalMs: Date.now() - t0 },
354
+ });
355
+ if (file)
356
+ result.evidence = file;
357
+ }
215
358
  }
216
359
  }