@aperant/framework 0.8.4 → 0.8.6

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 (108) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/agents/apt-planner.md +34 -3
  3. package/dist/cli/commands/coverage-check.d.mts.map +1 -1
  4. package/dist/cli/commands/coverage-check.mjs +74 -7
  5. package/dist/cli/commands/coverage-check.mjs.map +1 -1
  6. package/dist/cli/commands/detect-runtime.d.mts.map +1 -1
  7. package/dist/cli/commands/detect-runtime.mjs +18 -12
  8. package/dist/cli/commands/detect-runtime.mjs.map +1 -1
  9. package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
  10. package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
  11. package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
  12. package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
  13. package/dist/cli/host/detect.mjs +1 -1
  14. package/dist/cli/host/detect.mjs.map +1 -1
  15. package/dist/cli/install/install-from-source.mjs +3 -3
  16. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  17. package/dist/cli/install/legacy-paths.mjs +2 -0
  18. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  19. package/dist/cli/verify-proof/idl/index.d.mts +1 -1
  20. package/dist/cli/verify-proof/idl/index.mjs +7 -8
  21. package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
  22. package/dist/cli/verify-proof/idl/types.d.ts +5 -4
  23. package/dist/cli/verify-proof/idl/types.d.ts.map +1 -1
  24. package/dist/cli/verify-proof/idl/types.js +4 -3
  25. package/dist/cli/verify-proof/idl/types.js.map +1 -1
  26. package/dist/cli/verify-proof/resolver.d.mts +63 -1
  27. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  28. package/dist/cli/verify-proof/resolver.mjs +164 -4
  29. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  30. package/dist/cli/verify-proof/runtime-detect.d.mts +6 -3
  31. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
  32. package/dist/cli/verify-proof/runtime-detect.mjs +104 -11
  33. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
  34. package/dist/cli/verify-proof/trust.d.mts +1 -1
  35. package/dist/cli/verify-proof/trust.d.mts.map +1 -1
  36. package/dist/cli/verify-proof/trust.mjs +1 -1
  37. package/dist/driver-sdk/conformance.d.ts +41 -0
  38. package/dist/driver-sdk/conformance.d.ts.map +1 -0
  39. package/dist/driver-sdk/conformance.js +123 -0
  40. package/dist/driver-sdk/conformance.js.map +1 -0
  41. package/dist/driver-sdk/errors.d.ts +75 -0
  42. package/dist/driver-sdk/errors.d.ts.map +1 -0
  43. package/dist/driver-sdk/errors.js +80 -0
  44. package/dist/driver-sdk/errors.js.map +1 -0
  45. package/dist/driver-sdk/idl.d.ts +221 -0
  46. package/dist/driver-sdk/idl.d.ts.map +1 -0
  47. package/dist/driver-sdk/idl.js +140 -0
  48. package/dist/driver-sdk/idl.js.map +1 -0
  49. package/dist/driver-sdk/index.d.ts +28 -0
  50. package/dist/driver-sdk/index.d.ts.map +1 -0
  51. package/dist/driver-sdk/index.js +28 -0
  52. package/dist/driver-sdk/index.js.map +1 -0
  53. package/dist/driver-sdk/manifest.d.ts +93 -0
  54. package/dist/driver-sdk/manifest.d.ts.map +1 -0
  55. package/dist/driver-sdk/manifest.js +12 -0
  56. package/dist/driver-sdk/manifest.js.map +1 -0
  57. package/dist/driver-sdk/retry.d.ts +33 -0
  58. package/dist/driver-sdk/retry.d.ts.map +1 -0
  59. package/dist/driver-sdk/retry.js +50 -0
  60. package/dist/driver-sdk/retry.js.map +1 -0
  61. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  62. package/dist/plugin/agents/apt-planner.md +34 -3
  63. package/dist/plugin/skills/apt-plan/SKILL.md +50 -0
  64. package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
  65. package/dist/plugin/skills/apt-research/SKILL.md +526 -0
  66. package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
  67. package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  68. package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
  69. package/dist/plugin/skills/apt-spar/SKILL.md +19 -7
  70. package/dist/plugin/skills/apt-verify-proof/SKILL.md +41 -2
  71. package/dist/schemas/quick-task.d.ts +17 -17
  72. package/drivers/.gitkeep +0 -0
  73. package/drivers/api/README.md +40 -0
  74. package/drivers/api/driver.mjs +59 -0
  75. package/drivers/api/manifest.json +26 -0
  76. package/drivers/browser/README.md +105 -0
  77. package/drivers/browser/driver.mjs +134 -0
  78. package/drivers/browser/manifest.json +35 -0
  79. package/drivers/cli/README.md +44 -0
  80. package/drivers/cli/driver.mjs +62 -0
  81. package/drivers/cli/manifest.json +28 -0
  82. package/drivers/electron/README.md +64 -0
  83. package/drivers/electron/driver.mjs +87 -0
  84. package/drivers/electron/manifest.json +37 -0
  85. package/package.json +7 -3
  86. package/skills/apt-plan/SKILL.md +50 -0
  87. package/skills/apt-planner.md +16 -0
  88. package/skills/apt-pr-review/SKILL.md +1 -1
  89. package/skills/apt-research/SKILL.md +526 -0
  90. package/skills/apt-research/appendices/budget-loop.md +397 -0
  91. package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  92. package/skills/apt-research/appendices/domain-tools.md +236 -0
  93. package/skills/apt-spar/SKILL.md +19 -7
  94. package/skills/apt-verify-proof/SKILL.md +41 -2
  95. package/src/cli/commands/coverage-check.mjs +126 -44
  96. package/src/cli/commands/detect-runtime.mjs +23 -14
  97. package/src/cli/coverage-check/user-outcomes.mjs +273 -0
  98. package/src/cli/host/detect.mjs +1 -1
  99. package/src/cli/install/install-from-source.mjs +3 -3
  100. package/src/cli/install/legacy-paths.mjs +2 -0
  101. package/src/cli/verify-proof/idl/index.mjs +7 -8
  102. package/src/cli/verify-proof/idl/types.ts +5 -4
  103. package/src/cli/verify-proof/manifest-schema.json +2 -1
  104. package/src/cli/verify-proof/resolver.mjs +171 -4
  105. package/src/cli/verify-proof/runtime-detect.mjs +99 -10
  106. package/src/cli/verify-proof/trust.mjs +1 -1
  107. package/templates/proof-verification.md +32 -3
  108. package/workflows/verify-proof.md +78 -9
@@ -8,9 +8,17 @@
8
8
  *
9
9
  * Pattern: pure data in → pure data out. No filesystem walking, no exec.
10
10
  * Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
11
+ *
12
+ * v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
13
+ * (configurable via the second arg `{monorepo_globs}`) and OR-merges
14
+ * each match's `package.json` capabilities with the root. The default
15
+ * `['apps/*']` matches the canonical Aperant monorepo layout; adopters
16
+ * whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
17
+ * custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
18
+ * walk entirely (root-only escape hatch).
11
19
  */
12
20
 
13
- import { existsSync, readFileSync } from 'node:fs'
21
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
14
22
  import { join, resolve } from 'node:path'
15
23
 
16
24
  /**
@@ -103,20 +111,101 @@ export function detectFromPackageJson(packageJson) {
103
111
 
104
112
  /**
105
113
  * Load and detect capabilities from a project directory's package.json.
106
- * Returns empty capabilities if no package.json exists (malformed or
107
- * missing both cases fall through to "nothing detected").
114
+ * v0.8.6 also walks `monorepo_globs` (default `['apps/*']`) and
115
+ * OR-merges each match's capabilities with the root.
108
116
  *
109
117
  * @param {string} projectDir
118
+ * @param {{monorepo_globs?: string[]}} [opts]
110
119
  * @returns {RuntimeCapabilities}
111
120
  */
112
- export function loadRuntimeCapabilities(projectDir) {
113
- const pkgPath = join(resolve(projectDir), 'package.json')
114
- if (!existsSync(pkgPath)) return emptyCapabilities()
115
- let parsed
121
+ export function loadRuntimeCapabilities(projectDir, opts) {
122
+ const root = resolve(projectDir)
123
+ const rootPkgPath = join(root, 'package.json')
124
+ let rootCaps = emptyCapabilities()
125
+ if (existsSync(rootPkgPath)) {
126
+ try {
127
+ rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')))
128
+ } catch {
129
+ /* fall through with empty caps */
130
+ }
131
+ }
132
+
133
+ // Default to the canonical Aperant monorepo layout. Passing `[]`
134
+ // disables the walk entirely (root-only escape hatch per spec ID-05).
135
+ const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*']
136
+ if (globs.length === 0) return rootCaps
137
+
138
+ let merged = rootCaps
139
+ for (const glob of globs) {
140
+ const matches = expandSimpleGlob(root, glob)
141
+ for (const dir of matches) {
142
+ const childPkgPath = join(dir, 'package.json')
143
+ if (!existsSync(childPkgPath)) continue
144
+ try {
145
+ const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')))
146
+ merged = orMergeCapabilities(merged, childCaps)
147
+ } catch {
148
+ /* skip malformed package.json */
149
+ }
150
+ }
151
+ }
152
+ return merged
153
+ }
154
+
155
+ /**
156
+ * Expand a simple `prefix/*` glob against the root. Supports ONLY the
157
+ * `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
158
+ * glob syntax is intentionally rejected — v1 keeps the dep surface
159
+ * small. Fails closed on malformed patterns (returns []).
160
+ *
161
+ * @param {string} root
162
+ * @param {string} glob
163
+ * @returns {string[]}
164
+ */
165
+ function expandSimpleGlob(root, glob) {
166
+ if (typeof glob !== 'string' || !glob.endsWith('/*')) return []
167
+ const prefix = glob.slice(0, -2)
168
+ if (prefix.length === 0 || prefix.includes('*')) return []
169
+ const dir = join(root, prefix)
170
+ if (!existsSync(dir)) return []
171
+ let entries
116
172
  try {
117
- parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'))
173
+ entries = readdirSync(dir)
118
174
  } catch {
119
- return emptyCapabilities()
175
+ return []
176
+ }
177
+ const matches = []
178
+ for (const name of entries) {
179
+ const sub = join(dir, name)
180
+ try {
181
+ if (statSync(sub).isDirectory()) matches.push(sub)
182
+ } catch {
183
+ /* skip unreadable entries */
184
+ }
185
+ }
186
+ return matches
187
+ }
188
+
189
+ /**
190
+ * Boolean OR-merge of two capability records. Any flag true in either
191
+ * input is true in the output. The "Electron wins over web" rule from
192
+ * `detectFromPackageJson` is preserved when this is called repeatedly —
193
+ * each child's flags are honest about that child's own shape, and the
194
+ * OR-merge just unions them.
195
+ *
196
+ * @param {RuntimeCapabilities} a
197
+ * @param {RuntimeCapabilities} b
198
+ * @returns {RuntimeCapabilities}
199
+ */
200
+ function orMergeCapabilities(a, b) {
201
+ return {
202
+ is_electron: a.is_electron || b.is_electron,
203
+ is_tauri: a.is_tauri || b.is_tauri,
204
+ is_nextjs: a.is_nextjs || b.is_nextjs,
205
+ is_nuxt: a.is_nuxt || b.is_nuxt,
206
+ is_web_frontend: a.is_web_frontend || b.is_web_frontend,
207
+ is_expo: a.is_expo || b.is_expo,
208
+ is_react_native: a.is_react_native || b.is_react_native,
209
+ has_node: a.has_node || b.has_node,
120
210
  }
121
- return detectFromPackageJson(parsed)
122
211
  }
@@ -23,7 +23,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
23
23
  import { dirname, join, resolve } from 'node:path'
24
24
 
25
25
  /**
26
- * @typedef {import('@aperant/driver-sdk').DriverManifest} DriverManifest
26
+ * @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
27
27
  */
28
28
 
29
29
  const TOFU_PATH = '.aperant/driver-tofu.json'
@@ -53,7 +53,13 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
53
53
  "tested_count": 0,
54
54
  "skipped_count": 0,
55
55
  "percent": 0,
56
- "gate_result": "pass | warn | block"
56
+ "gate_result": "pass | warn | block",
57
+ "by_surface": {
58
+ "electron": { "tested": 0, "total": 0 },
59
+ "web": { "tested": 0, "total": 0 },
60
+ "cli": { "tested": 0, "total": 0 },
61
+ "api": { "tested": 0, "total": 0 }
62
+ }
57
63
  },
58
64
 
59
65
  "tests": [
@@ -61,14 +67,33 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
61
67
  "id": "TEST-01",
62
68
  "registry_id": "terminals-grid-auto-tile",
63
69
  "name": "Three Terminal Grid Auto-Tile",
70
+ "outcome_id": "O3",
71
+ "surface": "electron",
72
+ "transport": "mcp",
64
73
  "precondition": "2 terminals active",
65
74
  "action": "Press Cmd+T",
66
75
  "expected": "Grid reflows to 2+1 layout",
67
76
  "actual": "Grid correctly tiled to 2+1",
68
- "evidence": "screenshots/03-grid.png",
77
+ "evidence": "electron/screenshots/03-grid.png",
69
78
  "analysis": "Screenshot shows three terminal panes. Top row has 2 equal-width panes, bottom row has 1 centered pane. Grid lines are clean, no overlap or gap artifacts. Each pane shows an active shell prompt with correct path.",
70
79
  "result": "pass | fail | blocked | skip",
71
80
  "severity": null
81
+ },
82
+ {
83
+ "id": "TEST-02",
84
+ "registry_id": "settings-theme-picker",
85
+ "name": "Theme picker — cross-surface",
86
+ "outcome_id": "O5",
87
+ "surface": "web",
88
+ "transport": "skill",
89
+ "precondition": "App loaded in web shell",
90
+ "action": "Click 'Dark mode' in the theme picker",
91
+ "expected": "Background flips to dark; persists across reload",
92
+ "actual": "Dark mode applied, persisted across reload",
93
+ "evidence": "web/screenshots/05-dark-mode.png",
94
+ "analysis": "Screenshot shows app body with #111 background, all text legible on dark surface. After reload, dark mode is still active — localStorage entry confirmed.",
95
+ "result": "pass",
96
+ "severity": null
72
97
  }
73
98
  ],
74
99
 
@@ -175,6 +200,7 @@ Each check has `status` (pass/fail) and `output` (captured terminal output). The
175
200
  | `skipped_count` | number | Number of entries in the skipped array |
176
201
  | `percent` | number | `tested_count / registered_count * 100` |
177
202
  | `gate_result` | string | `block` if < 50%, `warn` if < 80%, `pass` if >= 80% |
203
+ | `by_surface` | object | v0.8.6 — per-surface rollup. Each surface key (`electron`, `web`, `cli`, `api`) maps to `{tested, total}` so reviewers can tell which surface fell short. |
178
204
 
179
205
  ### tests
180
206
  Each test entry tracks one sub-feature verification. Fields:
@@ -184,11 +210,14 @@ Each test entry tracks one sub-feature verification. Fields:
184
210
  | `id` | string | IMMUTABLE | Sequential ID: TEST-01, TEST-02, etc. |
185
211
  | `registry_id` | string | IMMUTABLE | Must match a sub-feature `id` from `.aperant/features/{area}.json` |
186
212
  | `name` | string | IMMUTABLE | Human-readable test name |
213
+ | `outcome_id` | string | IMMUTABLE | v0.8.6 — positional ID from `## User Outcomes` (e.g. `O1`, `O2`). Synthesized by `parseUserOutcomes`; passed through verbatim by `resolveDriversForOutcomes`. Optional for legacy specs without `## User Outcomes`. |
214
+ | `surface` | string | IMMUTABLE | v0.8.6 — one of `electron | web | cli | api`. Drives the per-surface screenshot path and the `coverage.by_surface` rollup. Cross-surface outcomes (`[electron+web]`) produce TWO test entries with the same `outcome_id` and different `surface` values. |
215
+ | `transport` | string | OVERWRITE | v0.8.6 — `skill | mcp | binary | http`. Populated by the workflow runner from the dispatcher's directive (per Step 7a of `workflows/verify-proof.md`). For browser-driver web outcomes on Skill-capable hosts the value is `"skill"`; on legacy MCP hosts it falls back to `"mcp"`. |
187
216
  | `precondition` | string | IMMUTABLE | What must be true before testing |
188
217
  | `action` | string | IMMUTABLE | Specific action to perform |
189
218
  | `expected` | string | IMMUTABLE | Specific observable result expected |
190
219
  | `actual` | string | OVERWRITE | What actually happened (filled after testing) |
191
- | `evidence` | string | OVERWRITE | Screenshot filename or video timestamp |
220
+ | `evidence` | string | OVERWRITE | Screenshot filename or video timestamp. v0.8.6 — paths are PER-SURFACE: `{surface}/screenshots/{outcome-id}.png`. |
192
221
  | `analysis` | string | OVERWRITE | Adversarial analysis of evidence — describe what you SEE, not what you expect |
193
222
  | `result` | string | OVERWRITE | `pass`, `fail`, `blocked`, or `skip` |
194
223
  | `severity` | string/null | OVERWRITE | `blocker`, `major`, `minor`, `cosmetic`, or `null` if pass |
@@ -43,13 +43,25 @@ cat implementation_plan.json 2>/dev/null
43
43
  cat testable_features.json 2>/dev/null
44
44
  ```
45
45
 
46
- Generate run ID and create proof directory:
46
+ Generate run ID and create proof directory. v0.8.6 — when the spec
47
+ carries `## User Outcomes` with cross-surface tags (e.g.
48
+ `[electron+web]`), proof artifacts MUST land under PER-SURFACE
49
+ subdirectories so reviewers can tell at a glance which surface failed.
50
+ Set up the path template here:
51
+
47
52
  ```bash
48
53
  RUN_ID="$(date +%H%M%S)"
49
54
  APP_NAME=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).name)}catch{console.log('app')}" 2>/dev/null)
50
55
  PROOF_DIR=".aperant/proof/${APP_NAME}/$(date +%Y%m%d)/${RUN_ID}"
51
- mkdir -p "${PROOF_DIR}/screenshots"
56
+ # Per-surface screenshot path template — Step 7c populates ${surface}
57
+ # from the per-outcome resolver dispatch:
58
+ # ${PROOF_DIR}/${surface}/screenshots/${outcome-id}.png
59
+ mkdir -p "${PROOF_DIR}"
52
60
  ```
61
+
62
+ Sub-directories under `${PROOF_DIR}/${surface}/screenshots/` are
63
+ created lazily in Step 7c when the workflow runner takes the first
64
+ screenshot for that surface.
53
65
  </step>
54
66
 
55
67
  <step name="pre_check">
@@ -83,6 +95,18 @@ If no driver satisfies a REQUIRED capability, the resolver throws
83
95
  `unsatisfied_required_capability` — fail-closed, NOT a silent
84
96
  `blocked_by_platform` skip (per ID-02).
85
97
 
98
+ **v0.8.6 — per-outcome dispatch.** When the spec carries
99
+ `## User Outcomes`, the workflow runner picks a driver PER OUTCOME
100
+ SURFACE via `resolveDriversForOutcomes()` instead of a single
101
+ per-run driver pick. Each outcome's `[surface]` tag maps to the
102
+ runtime-capability flag a driver must carry; `[electron+web]`
103
+ outcomes dispatch to TWO drivers (one per surface) sharing the same
104
+ `outcome_id`. The legacy `apt-tools detect-runtime .` call above
105
+ still works for the per-run "what driver would be picked?" preview
106
+ — it answers the OLD question. For the new per-outcome question,
107
+ the workflow runner imports `resolveDriversForOutcomes` directly
108
+ from the resolver module in Step 7c.
109
+
86
110
  ### 2b. Probe optional evidence-capture tools
87
111
 
88
112
  ```bash
@@ -142,6 +166,16 @@ If no feature registry exists:
142
166
  - Suggest: "No feature registry found. Consider running `/apt:scan {area}` first for exhaustive coverage."
143
167
  - Proceed with manual discovery below, but warn that coverage may be incomplete
144
168
 
169
+ **v0.8.6 — read `## User Outcomes` from spec.md FIRST when present.**
170
+ The PRD-shaped spec.md (STANDARD/DEEP tracks) ships a
171
+ `## User Outcomes` section that is the verify-proof input contract:
172
+ `**Epic:**` headline + N atomic outcomes tagged by `[surface]`. When
173
+ present, each outcome maps 1:1 to a test entry — the workflow runner
174
+ does NOT have to invent test units from the feature registry. Fall
175
+ back to the feature-registry-based discovery below ONLY when the spec
176
+ lacks `## User Outcomes` (the v0.8.x warn-not-block migration window
177
+ per spec ID-06; the section becomes a hard block in v0.9.0).
178
+
145
179
  ### 4b. Read the source code
146
180
 
147
181
  You MUST read the actual source code for the features under test. Do not guess.
@@ -278,19 +312,49 @@ a local binary, or a built-in HTTP request). Workflow prose names ONLY
278
312
  IDL verbs per ID-01 — concrete tool names live in
279
313
  `packages/framework/drivers/<driver>/driver.mjs`.
280
314
 
315
+ **v0.8.6 — transport dispatch directive (MUST honor).** When
316
+ `driver.transport.skill_id` is set AND the host CLI exposes the
317
+ Skill tool AND the outcome surface is `web`, the workflow runner
318
+ MUST invoke `Skill({skill: driver.transport.skill_id, args: ...})`
319
+ (passing the IDL verb args) and MUST record `transport: "skill"` on
320
+ the resulting test entry in verification.json. When the host CLI
321
+ does NOT expose the Skill tool, fall back to the MCP transport via
322
+ the existing puppeteer MCP server and record `transport: "mcp"`.
323
+ The driver's `chooseTransport({has_skill_tool})` helper computes
324
+ the choice; the workflow runner is the site that knows
325
+ `has_skill_tool`. This directive is LLM-honored (the v1 verify-proof
326
+ runner is the host LLM following this workflow as prose); a
327
+ text-level grep guard (TD-08) pins the `MUST` language so future
328
+ edits cannot silently soften it to `SHOULD`.
329
+
281
330
  ### 7b. Wait for Result
282
331
  Wait for the action to complete. If it involves loading, wait for the load.
283
332
  Add a short delay (1-2 seconds) so the CDP capture also captures this state.
284
333
 
285
334
  ### 7c. Capture Evidence
286
- Take a screenshot AFTER the action completes:
335
+ Take a screenshot AFTER the action completes. v0.8.6 — screenshots
336
+ land under PER-SURFACE subdirectories, with the per-outcome driver
337
+ returned by `resolveDriversForOutcomes()`:
338
+
287
339
  ```
288
- driver.screenshot({
289
- target: { kind: "css", value: "body" },
290
- output_path: "${PROOF_DIR}/screenshots/{feature-id}-test-{n}.png"
291
- })
340
+ // Pick a driver per outcome surface (NOT per run). `entries[]` is
341
+ // the resolver's per-(outcome, surface) dispatch — for an
342
+ // [electron+web] outcome you get TWO entries sharing outcome_id
343
+ // and you loop both, capturing one screenshot per surface.
344
+ for (const entry of entries) {
345
+ await entry.driver.screenshot({
346
+ target: { kind: "css", value: "body" },
347
+ output_path: `${PROOF_DIR}/${entry.surface}/screenshots/${outcome.id}.png`
348
+ })
349
+ }
292
350
  ```
293
351
 
352
+ Single-surface outcomes (`[electron]`, `[web]`, `[cli]`, `[api]`)
353
+ produce one screenshot under `${PROOF_DIR}/${surface}/screenshots/`;
354
+ cross-surface (`[electron+web]`) produces two — one per surface tree
355
+ — with both test entries in `verification.json:tests[]` carrying the
356
+ same `id` but distinct `surface` and `evidence` fields.
357
+
294
358
  ### 7d. Analyze Evidence
295
359
  **MANDATORY and CRITICAL.** Every screenshot must be analyzed in detail.
296
360
 
@@ -313,18 +377,23 @@ apt-proof-video analyze "${PROOF_DIR}/screenshots/{feature-id}-test-{n}.png" \
313
377
  ```
314
378
 
315
379
  ### 7e. Record Result to verification.json
316
- After each test, update the test entry in verification.json `tests` array:
380
+ After each test, update the test entry in verification.json `tests` array.
381
+ v0.8.6 adds `surface` and `transport` fields per test entry — the workflow
382
+ runner populates them from the per-outcome dispatch directive:
317
383
 
318
384
  ```json
319
385
  {
320
386
  "id": "TEST-01",
321
387
  "registry_id": "terminals-grid-auto-tile",
322
388
  "name": "Three Terminal Grid Auto-Tile",
389
+ "outcome_id": "O3",
390
+ "surface": "electron",
391
+ "transport": "mcp",
323
392
  "precondition": "2 terminals active",
324
393
  "action": "Press Cmd+T",
325
394
  "expected": "Grid reflows to 2+1 layout",
326
395
  "actual": "Grid correctly tiled",
327
- "evidence": "screenshots/03-grid.png",
396
+ "evidence": "electron/screenshots/03-grid.png",
328
397
  "analysis": "Detailed adversarial analysis of what is actually visible in the screenshot...",
329
398
  "result": "pass",
330
399
  "severity": null