@aperant/framework 0.8.4 → 0.8.7
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/CHANGELOG.md +185 -0
- package/agents/apt-planner.md +34 -3
- package/dist/cli/commands/coverage-check.d.mts.map +1 -1
- package/dist/cli/commands/coverage-check.mjs +74 -7
- package/dist/cli/commands/coverage-check.mjs.map +1 -1
- package/dist/cli/commands/detect-runtime.d.mts.map +1 -1
- package/dist/cli/commands/detect-runtime.mjs +18 -12
- package/dist/cli/commands/detect-runtime.mjs.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
- package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
- package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
- package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
- package/dist/cli/host/detect.mjs +1 -1
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/install-from-source.mjs +3 -3
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/install/manifest.d.mts +13 -0
- package/dist/cli/install/manifest.d.mts.map +1 -1
- package/dist/cli/install/manifest.mjs +5 -0
- package/dist/cli/install/manifest.mjs.map +1 -1
- package/dist/cli/install/pipeline.d.mts +15 -0
- package/dist/cli/install/pipeline.d.mts.map +1 -1
- package/dist/cli/install/pipeline.mjs +27 -0
- package/dist/cli/install/pipeline.mjs.map +1 -1
- package/dist/cli/install/transforms/codex.d.mts +1 -1
- package/dist/cli/install/transforms/codex.d.mts.map +1 -1
- package/dist/cli/install/transforms/codex.mjs +43 -2
- package/dist/cli/install/transforms/codex.mjs.map +1 -1
- package/dist/cli/install/version-header.d.mts.map +1 -1
- package/dist/cli/install/version-header.mjs +7 -1
- package/dist/cli/install/version-header.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/index.d.mts +1 -1
- package/dist/cli/verify-proof/idl/index.mjs +7 -8
- package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
- package/dist/cli/verify-proof/idl/types.d.ts +5 -4
- package/dist/cli/verify-proof/idl/types.d.ts.map +1 -1
- package/dist/cli/verify-proof/idl/types.js +4 -3
- package/dist/cli/verify-proof/idl/types.js.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts +63 -1
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +164 -4
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.d.mts +6 -3
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.mjs +104 -11
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
- package/dist/cli/verify-proof/trust.d.mts +1 -1
- package/dist/cli/verify-proof/trust.d.mts.map +1 -1
- package/dist/cli/verify-proof/trust.mjs +1 -1
- package/dist/driver-sdk/conformance.d.ts +41 -0
- package/dist/driver-sdk/conformance.d.ts.map +1 -0
- package/dist/driver-sdk/conformance.js +123 -0
- package/dist/driver-sdk/conformance.js.map +1 -0
- package/dist/driver-sdk/errors.d.ts +75 -0
- package/dist/driver-sdk/errors.d.ts.map +1 -0
- package/dist/driver-sdk/errors.js +80 -0
- package/dist/driver-sdk/errors.js.map +1 -0
- package/dist/driver-sdk/idl.d.ts +221 -0
- package/dist/driver-sdk/idl.d.ts.map +1 -0
- package/dist/driver-sdk/idl.js +140 -0
- package/dist/driver-sdk/idl.js.map +1 -0
- package/dist/driver-sdk/index.d.ts +28 -0
- package/dist/driver-sdk/index.d.ts.map +1 -0
- package/dist/driver-sdk/index.js +28 -0
- package/dist/driver-sdk/index.js.map +1 -0
- package/dist/driver-sdk/manifest.d.ts +93 -0
- package/dist/driver-sdk/manifest.d.ts.map +1 -0
- package/dist/driver-sdk/manifest.js +12 -0
- package/dist/driver-sdk/manifest.js.map +1 -0
- package/dist/driver-sdk/retry.d.ts +33 -0
- package/dist/driver-sdk/retry.d.ts.map +1 -0
- package/dist/driver-sdk/retry.js +50 -0
- package/dist/driver-sdk/retry.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-planner.md +34 -3
- package/dist/plugin/skills/apt-plan/SKILL.md +50 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
- package/dist/plugin/skills/apt-research/SKILL.md +526 -0
- package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
- package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +19 -7
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +41 -2
- package/dist/schemas/quick-task.d.ts +17 -17
- package/drivers/.gitkeep +0 -0
- package/drivers/api/README.md +40 -0
- package/drivers/api/driver.mjs +59 -0
- package/drivers/api/manifest.json +26 -0
- package/drivers/browser/README.md +105 -0
- package/drivers/browser/driver.mjs +134 -0
- package/drivers/browser/manifest.json +35 -0
- package/drivers/cli/README.md +44 -0
- package/drivers/cli/driver.mjs +62 -0
- package/drivers/cli/manifest.json +28 -0
- package/drivers/electron/README.md +64 -0
- package/drivers/electron/driver.mjs +87 -0
- package/drivers/electron/manifest.json +37 -0
- package/package.json +7 -3
- package/skills/apt-plan/SKILL.md +50 -0
- package/skills/apt-planner.md +16 -0
- package/skills/apt-pr-review/SKILL.md +1 -1
- package/skills/apt-research/SKILL.md +526 -0
- package/skills/apt-research/appendices/budget-loop.md +397 -0
- package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
- package/skills/apt-research/appendices/domain-tools.md +236 -0
- package/skills/apt-spar/SKILL.md +19 -7
- package/skills/apt-verify-proof/SKILL.md +41 -2
- package/src/cli/commands/coverage-check.mjs +126 -44
- package/src/cli/commands/detect-runtime.mjs +23 -14
- package/src/cli/coverage-check/user-outcomes.mjs +273 -0
- package/src/cli/host/detect.mjs +1 -1
- package/src/cli/install/install-from-source.mjs +3 -3
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/install/manifest.mjs +5 -0
- package/src/cli/install/pipeline.mjs +28 -0
- package/src/cli/install/transforms/codex.mjs +51 -2
- package/src/cli/install/version-header.mjs +7 -1
- package/src/cli/verify-proof/idl/index.mjs +7 -8
- package/src/cli/verify-proof/idl/types.ts +5 -4
- package/src/cli/verify-proof/manifest-schema.json +2 -1
- package/src/cli/verify-proof/resolver.mjs +171 -4
- package/src/cli/verify-proof/runtime-detect.mjs +99 -10
- package/src/cli/verify-proof/trust.mjs +1 -1
- package/templates/proof-verification.md +32 -3
- package/workflows/verify-proof.md +78 -9
|
@@ -16,12 +16,24 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Ties are broken by the next dimension; if every dimension is equal the
|
|
18
18
|
* driver with the lexicographically smaller driverId wins (deterministic).
|
|
19
|
+
*
|
|
20
|
+
* v0.8.6 — `resolveDriversForOutcomes(opts)` adds per-outcome driver
|
|
21
|
+
* dispatch for cross-surface specs (per spec §ID-03). It accepts the
|
|
22
|
+
* positional `outcomes[]` produced by `parseUserOutcomes` and returns
|
|
23
|
+
* `Array<{outcome_id, surface, driver}>` — one entry per
|
|
24
|
+
* (outcome, surface) pair. For `[electron+web]` outcomes it emits TWO
|
|
25
|
+
* entries with the same outcome_id and different drivers. The
|
|
26
|
+
* outcome_id field is passed through verbatim from parser → resolver
|
|
27
|
+
* → workflow runner with no name rewriting (the positional-ID
|
|
28
|
+
* contract).
|
|
29
|
+
*
|
|
30
|
+
* Existing `resolveDriver()` and `scoreDriver()` exports are unchanged.
|
|
19
31
|
*/
|
|
20
32
|
|
|
21
|
-
import { UnsatisfiedRequiredCapabilityError } from '@aperant/driver-sdk'
|
|
33
|
+
import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
|
-
* @typedef {import('@aperant/driver-sdk').DriverManifest} DriverManifest
|
|
36
|
+
* @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
|
|
25
37
|
* @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
|
|
26
38
|
*/
|
|
27
39
|
|
|
@@ -131,8 +143,29 @@ export function resolveDriver(opts) {
|
|
|
131
143
|
stableOnly.push(d)
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
//
|
|
135
|
-
|
|
146
|
+
// Override is hard precedence (per opts.override docstring "explicit
|
|
147
|
+
// driverId precedence"): when set, try override-listed drivers in
|
|
148
|
+
// override order — the first one that satisfies every required
|
|
149
|
+
// capability wins, regardless of compatibility/priority/stability
|
|
150
|
+
// ranking. Non-matching override entries land in fallback. Only when
|
|
151
|
+
// every override entry misses a required cap do we fall through to
|
|
152
|
+
// score-based pick on the remaining drivers.
|
|
153
|
+
const overrideListed = stableOnly.filter((m) => overrideSet.has(m.driverId))
|
|
154
|
+
for (const candidate of overrideListed) {
|
|
155
|
+
const caps = new Set(candidate.capabilities ?? [])
|
|
156
|
+
const missing = required.filter((c) => !caps.has(c))
|
|
157
|
+
if (missing.length === 0) {
|
|
158
|
+
return { selected: candidate, fallback_attempts: fallback }
|
|
159
|
+
}
|
|
160
|
+
fallback.push({
|
|
161
|
+
driver_id: candidate.driverId,
|
|
162
|
+
reason: `override-listed but missing required capabilities: ${missing.join(', ')}`,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Score every non-override survivor against required capabilities.
|
|
167
|
+
const nonOverride = stableOnly.filter((m) => !overrideSet.has(m.driverId))
|
|
168
|
+
const scored = nonOverride.map((m) => ({ manifest: m, ...scoreDriver(m, runtime, required) }))
|
|
136
169
|
|
|
137
170
|
// Sort highest-first, tie-break on driverId for determinism.
|
|
138
171
|
scored.sort((a, b) => {
|
|
@@ -161,3 +194,137 @@ export function resolveDriver(opts) {
|
|
|
161
194
|
fallback,
|
|
162
195
|
)
|
|
163
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Map a User Outcomes surface tag to the runtime-capability flag a driver
|
|
200
|
+
* must carry (via `matchesRuntime`) to be eligible for that surface.
|
|
201
|
+
*
|
|
202
|
+
* Closed surface set: `electron | web | cli | api | electron+web`.
|
|
203
|
+
* `electron+web` is NOT a single flag — it's a cross-surface tag the
|
|
204
|
+
* workflow runner expands into two single-surface dispatches.
|
|
205
|
+
*
|
|
206
|
+
* @param {('electron'|'web'|'cli'|'api')} surface
|
|
207
|
+
* @returns {('is_electron'|'is_web_frontend'|'has_node')}
|
|
208
|
+
*/
|
|
209
|
+
export function surfaceToRequiredFlag(surface) {
|
|
210
|
+
switch (surface) {
|
|
211
|
+
case 'electron':
|
|
212
|
+
return 'is_electron'
|
|
213
|
+
case 'web':
|
|
214
|
+
return 'is_web_frontend'
|
|
215
|
+
case 'cli':
|
|
216
|
+
case 'api':
|
|
217
|
+
return 'has_node'
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`surfaceToRequiredFlag: unknown surface "${surface}"`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a driver per outcome surface tag, NOT per verify-proof run
|
|
225
|
+
* (per spec §ID-03). For each input outcome the function returns one
|
|
226
|
+
* `{outcome_id, surface, driver}` entry; for `[electron+web]` outcomes
|
|
227
|
+
* it returns TWO entries (electron + web) sharing the same outcome_id.
|
|
228
|
+
*
|
|
229
|
+
* The `outcome_id` field on each returned entry is the positional ID
|
|
230
|
+
* produced by `parseUserOutcomes` (e.g. `O1`, `O2`). The resolver does
|
|
231
|
+
* NOT rewrite or regenerate IDs — they pass through verbatim so the
|
|
232
|
+
* workflow runner can correlate proof artifacts to spec lines.
|
|
233
|
+
*
|
|
234
|
+
* Each per-surface dispatch synthesizes a runtime-capabilities view
|
|
235
|
+
* that ONLY claims the surface's required flag (e.g. `is_electron`
|
|
236
|
+
* alone for an electron-surface outcome, even when the project also
|
|
237
|
+
* carries `is_web_frontend`). This forces the existing scoring to
|
|
238
|
+
* pick the per-surface driver — without it, a project with BOTH
|
|
239
|
+
* electron AND web capabilities would always select the highest-
|
|
240
|
+
* priority bundled driver (currently `browser`) for every outcome.
|
|
241
|
+
*
|
|
242
|
+
* Delegates to the existing `resolveDriver()` for the actual scoring
|
|
243
|
+
* — the scoring code stays in one place.
|
|
244
|
+
*
|
|
245
|
+
* @param {Object} opts
|
|
246
|
+
* @param {DriverManifest[]} opts.drivers
|
|
247
|
+
* @param {RuntimeCapabilities} opts.runtime
|
|
248
|
+
* @param {Array<{id: string, surface: 'electron'|'web'|'cli'|'api'|'electron+web'}>} opts.outcomes
|
|
249
|
+
* @param {string[]} [opts.override]
|
|
250
|
+
* @param {string[]} [opts.required_stability]
|
|
251
|
+
* @param {string[]} [opts.required_capabilities] — defaults to ['screenshot']
|
|
252
|
+
* @returns {Array<{outcome_id: string, surface: string, driver: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}>}
|
|
253
|
+
*/
|
|
254
|
+
export function resolveDriversForOutcomes(opts) {
|
|
255
|
+
const drivers = opts.drivers ?? []
|
|
256
|
+
const runtime = opts.runtime
|
|
257
|
+
const outcomes = opts.outcomes ?? []
|
|
258
|
+
const required = opts.required_capabilities ?? ['screenshot']
|
|
259
|
+
const override = opts.override
|
|
260
|
+
const stability = opts.required_stability
|
|
261
|
+
|
|
262
|
+
/** @type {Array<{outcome_id: string, surface: string, driver: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}>} */
|
|
263
|
+
const results = []
|
|
264
|
+
|
|
265
|
+
for (const outcome of outcomes) {
|
|
266
|
+
const surfacesToDispatch =
|
|
267
|
+
outcome.surface === 'electron+web' ? ['electron', 'web'] : [outcome.surface]
|
|
268
|
+
|
|
269
|
+
for (const surface of surfacesToDispatch) {
|
|
270
|
+
const flag = surfaceToRequiredFlag(/** @type {'electron'|'web'|'cli'|'api'} */ (surface))
|
|
271
|
+
// Synthesize a per-surface runtime view: only the surface's
|
|
272
|
+
// required flag is on; every other flag is masked off so the
|
|
273
|
+
// scoring prefers per-surface drivers (otherwise, on a
|
|
274
|
+
// multi-runtime project, the highest-priority bundled driver
|
|
275
|
+
// always wins). `has_node` stays on for cli/api since multiple
|
|
276
|
+
// drivers may target node.
|
|
277
|
+
const surfaceRuntime = maskRuntimeForSurface(runtime, flag)
|
|
278
|
+
const resolved = resolveDriver({
|
|
279
|
+
drivers,
|
|
280
|
+
runtime: surfaceRuntime,
|
|
281
|
+
required_capabilities: required,
|
|
282
|
+
...(override ? { override } : {}),
|
|
283
|
+
...(stability ? { required_stability: stability } : {}),
|
|
284
|
+
})
|
|
285
|
+
results.push({
|
|
286
|
+
outcome_id: outcome.id,
|
|
287
|
+
surface,
|
|
288
|
+
driver: resolved.selected,
|
|
289
|
+
fallback_attempts: resolved.fallback_attempts,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return results
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Return a copy of `runtime` with every flag false EXCEPT the one named
|
|
299
|
+
* by `flag`. Used to scope the resolver's compatibility check to a
|
|
300
|
+
* single surface during per-outcome dispatch.
|
|
301
|
+
*
|
|
302
|
+
* @param {RuntimeCapabilities} runtime
|
|
303
|
+
* @param {string} flag — one of: is_electron | is_web_frontend | has_node
|
|
304
|
+
* @returns {RuntimeCapabilities}
|
|
305
|
+
*/
|
|
306
|
+
function maskRuntimeForSurface(runtime, flag) {
|
|
307
|
+
/** @type {RuntimeCapabilities} */
|
|
308
|
+
const masked = {
|
|
309
|
+
is_electron: false,
|
|
310
|
+
is_tauri: false,
|
|
311
|
+
is_nextjs: false,
|
|
312
|
+
is_nuxt: false,
|
|
313
|
+
is_web_frontend: false,
|
|
314
|
+
is_expo: false,
|
|
315
|
+
is_react_native: false,
|
|
316
|
+
has_node: false,
|
|
317
|
+
}
|
|
318
|
+
// Only flip the surface's flag on if the project actually carries it
|
|
319
|
+
// (otherwise the resolver should genuinely fail — fail-closed).
|
|
320
|
+
if (flag === 'is_electron' && runtime.is_electron) masked.is_electron = true
|
|
321
|
+
if (flag === 'is_web_frontend' && runtime.is_web_frontend) {
|
|
322
|
+
masked.is_web_frontend = true
|
|
323
|
+
// Carry the framework hint too (Next.js / Nuxt) so drivers that
|
|
324
|
+
// target browser via those frameworks still match.
|
|
325
|
+
masked.is_nextjs = runtime.is_nextjs
|
|
326
|
+
masked.is_nuxt = runtime.is_nuxt
|
|
327
|
+
}
|
|
328
|
+
if (flag === 'has_node') masked.has_node = runtime.has_node
|
|
329
|
+
return masked
|
|
330
|
+
}
|
|
@@ -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
|
-
*
|
|
107
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
let
|
|
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
|
-
|
|
173
|
+
entries = readdirSync(dir)
|
|
118
174
|
} catch {
|
|
119
|
-
return
|
|
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
|
-
|
|
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.
|
|
289
|
-
|
|
290
|
-
|
|
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
|