@aperant/framework 0.8.0 → 0.8.4
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 +311 -1
- package/README.md +12 -0
- package/agents/apt-planner.md +5 -4
- package/dist/cli/commands/adr.mjs +1 -1
- package/dist/cli/commands/audit-branch-current.d.mts +25 -0
- package/dist/cli/commands/audit-branch-current.d.mts.map +1 -0
- package/dist/cli/commands/audit-branch-current.mjs +252 -0
- package/dist/cli/commands/audit-branch-current.mjs.map +1 -0
- package/dist/cli/commands/audit.d.mts.map +1 -1
- package/dist/cli/commands/audit.mjs +13 -2
- package/dist/cli/commands/audit.mjs.map +1 -1
- package/dist/cli/commands/check-version.d.mts.map +1 -1
- package/dist/cli/commands/check-version.mjs +19 -1
- package/dist/cli/commands/check-version.mjs.map +1 -1
- package/dist/cli/commands/context.mjs +3 -3
- package/dist/cli/commands/context.mjs.map +1 -1
- package/dist/cli/commands/detect-runtime.d.mts +6 -0
- package/dist/cli/commands/detect-runtime.d.mts.map +1 -0
- package/dist/cli/commands/detect-runtime.mjs +136 -0
- package/dist/cli/commands/detect-runtime.mjs.map +1 -0
- package/dist/cli/commands/driver-doctor.d.mts +29 -0
- package/dist/cli/commands/driver-doctor.d.mts.map +1 -0
- package/dist/cli/commands/driver-doctor.mjs +291 -0
- package/dist/cli/commands/driver-doctor.mjs.map +1 -0
- package/dist/cli/commands/gate.mjs +14 -0
- package/dist/cli/commands/gate.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +76 -33
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/install-from-source.d.mts +2 -0
- package/dist/cli/commands/install-from-source.d.mts.map +1 -0
- package/dist/cli/commands/install-from-source.mjs +2 -0
- package/dist/cli/commands/install-from-source.mjs.map +1 -0
- package/dist/cli/commands/modes.mjs +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +38 -1
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/triage.mjs +1 -1
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +9 -3
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +1 -1
- package/dist/cli/coordination/event-schema.d.mts.map +1 -1
- package/dist/cli/coordination/event-schema.mjs +18 -0
- package/dist/cli/coordination/event-schema.mjs.map +1 -1
- package/dist/cli/design/frontmatter-schema.d.mts +4 -4
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +18 -1
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/context.d.mts +1 -0
- package/dist/cli/gate/context.d.mts.map +1 -1
- package/dist/cli/gate/context.mjs +14 -1
- package/dist/cli/gate/context.mjs.map +1 -1
- package/dist/cli/gate/gates/consistency-check.d.mts +1 -0
- package/dist/cli/gate/gates/consistency-check.d.mts.map +1 -1
- package/dist/cli/gate/gates/consistency-check.mjs +1 -0
- package/dist/cli/gate/gates/consistency-check.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts +1 -0
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +1 -0
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +1 -0
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +1 -0
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/gate/registry.d.mts.map +1 -1
- package/dist/cli/gate/registry.mjs +9 -1
- package/dist/cli/gate/registry.mjs.map +1 -1
- package/dist/cli/install/cli-to-install-root.d.mts.map +1 -1
- package/dist/cli/install/cli-to-install-root.mjs +0 -2
- package/dist/cli/install/cli-to-install-root.mjs.map +1 -1
- package/dist/cli/install/find-workspace-framework-version.d.mts +16 -0
- package/dist/cli/install/find-workspace-framework-version.d.mts.map +1 -0
- package/dist/cli/install/find-workspace-framework-version.mjs +85 -0
- package/dist/cli/install/find-workspace-framework-version.mjs.map +1 -0
- package/dist/cli/install/install-from-source.d.mts +10 -0
- package/dist/cli/install/install-from-source.d.mts.map +1 -0
- package/dist/cli/install/install-from-source.mjs +271 -0
- package/dist/cli/install/install-from-source.mjs.map +1 -0
- package/dist/cli/install/install-kernel.d.mts.map +1 -1
- package/dist/cli/install/install-kernel.mjs +24 -0
- package/dist/cli/install/install-kernel.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts +33 -11
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +161 -39
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/install/runtime-detect.d.mts +2 -6
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +9 -4
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/runtime-migrate.d.mts +34 -14
- package/dist/cli/install/runtime-migrate.d.mts.map +1 -1
- package/dist/cli/install/runtime-migrate.mjs +63 -30
- package/dist/cli/install/runtime-migrate.mjs.map +1 -1
- package/dist/cli/install/transforms/codex.d.mts.map +1 -1
- package/dist/cli/install/transforms/codex.mjs +28 -17
- package/dist/cli/install/transforms/codex.mjs.map +1 -1
- package/dist/cli/install/transforms/pi.d.mts +6 -0
- package/dist/cli/install/transforms/pi.d.mts.map +1 -0
- package/dist/cli/install/transforms/pi.mjs +37 -0
- package/dist/cli/install/transforms/pi.mjs.map +1 -0
- package/dist/cli/personas/sidecar.d.mts +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts +1 -1
- package/dist/cli/roadmap/rollup.d.mts +2 -2
- package/dist/cli/skill-author/contract.mjs +2 -2
- package/dist/cli/skill-author/contract.mjs.map +1 -1
- package/dist/cli/verify-proof/audit.d.mts +34 -0
- package/dist/cli/verify-proof/audit.d.mts.map +1 -0
- package/dist/cli/verify-proof/audit.mjs +53 -0
- package/dist/cli/verify-proof/audit.mjs.map +1 -0
- package/dist/cli/verify-proof/exec.d.mts +20 -0
- package/dist/cli/verify-proof/exec.d.mts.map +1 -0
- package/dist/cli/verify-proof/exec.mjs +74 -0
- package/dist/cli/verify-proof/exec.mjs.map +1 -0
- package/dist/cli/verify-proof/idl/index.d.mts +2 -0
- package/dist/cli/verify-proof/idl/index.d.mts.map +1 -0
- package/dist/cli/verify-proof/idl/index.mjs +14 -0
- package/dist/cli/verify-proof/idl/index.mjs.map +1 -0
- package/dist/cli/verify-proof/idl/types.d.ts +9 -0
- package/dist/cli/verify-proof/idl/types.d.ts.map +1 -0
- package/dist/cli/verify-proof/idl/types.js +9 -0
- package/dist/cli/verify-proof/idl/types.js.map +1 -0
- package/dist/cli/verify-proof/manifest-validator.d.mts +19 -0
- package/dist/cli/verify-proof/manifest-validator.d.mts.map +1 -0
- package/dist/cli/verify-proof/manifest-validator.mjs +182 -0
- package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -0
- package/dist/cli/verify-proof/resolver.d.mts +43 -0
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -0
- package/dist/cli/verify-proof/resolver.mjs +160 -0
- package/dist/cli/verify-proof/resolver.mjs.map +1 -0
- package/dist/cli/verify-proof/runtime-detect.d.mts +27 -0
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -0
- package/dist/cli/verify-proof/runtime-detect.mjs +116 -0
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -0
- package/dist/cli/verify-proof/trust.d.mts +70 -0
- package/dist/cli/verify-proof/trust.d.mts.map +1 -0
- package/dist/cli/verify-proof/trust.mjs +174 -0
- package/dist/cli/verify-proof/trust.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +4 -1
- package/dist/plugin/agents/apt-executor.md +12 -0
- package/dist/plugin/agents/apt-planner.md +5 -4
- package/dist/plugin/agents/apt-team-docs-narrator.md +11 -0
- package/dist/plugin/skills/apt/SKILL.md +78 -8
- package/dist/plugin/skills/apt-caveman/SKILL.md +64 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +36 -4
- package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +2 -11
- package/dist/plugin/skills/apt-discuss/SKILL.md +4 -4
- package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +2 -11
- package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +0 -8
- package/dist/plugin/skills/apt-execute/SKILL.md +40 -4
- package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +2 -10
- package/dist/plugin/skills/apt-fan-out/SKILL.md +159 -0
- package/dist/plugin/skills/apt-handoff/SKILL.md +228 -0
- package/dist/plugin/skills/apt-improve/DEEPENING.md +0 -8
- package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +0 -8
- package/dist/plugin/skills/apt-improve/LANGUAGE.md +0 -8
- package/dist/plugin/skills/apt-improve/SKILL.md +4 -4
- package/dist/plugin/skills/apt-plan/SKILL.md +23 -5
- package/dist/plugin/skills/apt-plan/adapters/conductor.md +1 -1
- package/dist/plugin/skills/apt-pr-review/SKILL.md +8 -1
- package/dist/plugin/skills/apt-prototype/LOGIC.md +0 -8
- package/dist/plugin/skills/apt-prototype/SKILL.md +4 -4
- package/dist/plugin/skills/apt-prototype/UI.md +0 -8
- package/dist/plugin/skills/apt-quick/SKILL.md +31 -1
- package/dist/plugin/skills/apt-resume/SKILL.md +54 -31
- package/dist/plugin/skills/apt-review/SKILL.md +4 -0
- package/dist/plugin/skills/apt-run/SKILL.md +134 -21
- package/dist/plugin/skills/apt-setup/SKILL.md +97 -8
- package/dist/plugin/skills/apt-ship/SKILL.md +165 -9
- package/dist/plugin/skills/apt-spar/SKILL.md +77 -25
- package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +0 -8
- package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +0 -7
- package/dist/plugin/skills/apt-triage/SKILL.md +2 -2
- package/dist/plugin/skills/apt-update/SKILL.md +74 -17
- package/dist/plugin/skills/apt-verify/SKILL.md +11 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +48 -1
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +2 -2
- package/dist/plugin/skills/apt-zoom-out/SKILL.md +3 -3
- package/dist/schemas/quick-task.d.ts +17 -17
- package/dist/schemas/quick-task.d.ts.map +1 -1
- package/dist/schemas/quick-task.js +2 -2
- package/dist/schemas/quick-task.js.map +1 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/state.d.ts +1 -1
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/task-record.d.ts +1 -1
- package/dist/types/task-record.d.ts.map +1 -1
- package/package.json +134 -133
- package/prompts/planner.md +1 -1
- package/prompts/spec_writer.md +1 -1
- package/skills/apt/SKILL.md +78 -8
- package/skills/apt-caveman/SKILL.md +64 -0
- package/skills/apt-debug/SKILL.md +36 -4
- package/skills/apt-debug/appendices/diagnose-discipline.md +2 -11
- package/skills/apt-discuss/SKILL.md +4 -4
- package/skills/apt-discuss/appendices/grill-discipline.md +2 -11
- package/skills/apt-discuss/appendices/zoom-out-helper.md +0 -8
- package/skills/apt-execute/SKILL.md +27 -4
- package/skills/apt-execute/appendices/tdd-mode.md +2 -10
- package/skills/apt-fan-out/SKILL.md +36 -1
- package/skills/apt-handoff/SKILL.md +228 -0
- package/skills/apt-improve/DEEPENING.md +0 -8
- package/skills/apt-improve/INTERFACE-DESIGN.md +0 -8
- package/skills/apt-improve/LANGUAGE.md +0 -8
- package/skills/apt-improve/SKILL.md +4 -4
- package/skills/apt-plan/SKILL.md +23 -5
- package/skills/apt-plan/adapters/conductor.md +1 -1
- package/skills/apt-planner.md +1 -1
- package/skills/apt-prototype/LOGIC.md +0 -8
- package/skills/apt-prototype/SKILL.md +4 -4
- package/skills/apt-prototype/UI.md +0 -8
- package/skills/apt-quick/SKILL.md +31 -1
- package/skills/apt-resume/SKILL.md +54 -31
- package/skills/apt-run/SKILL.md +114 -16
- package/skills/apt-setup/SKILL.md +97 -8
- package/skills/apt-ship/SKILL.md +165 -9
- package/skills/apt-spar/SKILL.md +65 -16
- package/skills/apt-triage/AGENT-BRIEF.md +0 -8
- package/skills/apt-triage/OUT-OF-SCOPE.md +0 -7
- package/skills/apt-triage/SKILL.md +2 -2
- package/skills/apt-update/SKILL.md +74 -17
- package/skills/apt-verify-proof/SKILL.md +48 -1
- package/skills/apt-zoom-out/SKILL.md +3 -3
- package/src/cli/commands/adr.mjs +1 -1
- package/src/cli/commands/audit-branch-current.mjs +250 -0
- package/src/cli/commands/audit.mjs +13 -2
- package/src/cli/commands/check-version.mjs +20 -1
- package/src/cli/commands/context.mjs +3 -3
- package/src/cli/commands/detect-runtime.mjs +134 -0
- package/src/cli/commands/driver-doctor.mjs +294 -0
- package/src/cli/commands/gate.mjs +15 -0
- package/src/cli/commands/init.mjs +75 -33
- package/src/cli/commands/install-from-source.mjs +1 -0
- package/src/cli/commands/modes.mjs +1 -1
- package/src/cli/commands/route.mjs +42 -1
- package/src/cli/commands/triage.mjs +1 -1
- package/src/cli/config/load.mjs +9 -3
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +1 -1
- package/src/cli/coordination/event-schema.mjs +19 -0
- package/src/cli/dispatch.mjs +17 -1
- package/src/cli/gate/context.mjs +20 -1
- package/src/cli/gate/gates/consistency-check.mjs +1 -0
- package/src/cli/gate/gates/review-clean.mjs +1 -0
- package/src/cli/gate/gates/verify-approved.mjs +1 -0
- package/src/cli/gate/registry.mjs +11 -1
- package/src/cli/install/cli-to-install-root.mjs +0 -2
- package/src/cli/install/find-workspace-framework-version.mjs +88 -0
- package/src/cli/install/install-from-source.mjs +276 -0
- package/src/cli/install/install-kernel.mjs +24 -0
- package/src/cli/install/legacy-paths.mjs +162 -39
- package/src/cli/install/runtime-detect.mjs +9 -4
- package/src/cli/install/runtime-migrate.mjs +63 -30
- package/src/cli/install/transforms/codex.mjs +26 -17
- package/src/cli/install/transforms/pi.mjs +37 -0
- package/src/cli/skill-author/contract.mjs +2 -2
- package/src/cli/verify-proof/.gitkeep +0 -0
- package/src/cli/verify-proof/audit.mjs +55 -0
- package/src/cli/verify-proof/exec.mjs +74 -0
- package/src/cli/verify-proof/idl/index.mjs +24 -0
- package/src/cli/verify-proof/idl/types.ts +41 -0
- package/src/cli/verify-proof/manifest-schema.json +211 -0
- package/src/cli/verify-proof/manifest-validator.mjs +184 -0
- package/src/cli/verify-proof/resolver.mjs +163 -0
- package/src/cli/verify-proof/runtime-detect.mjs +122 -0
- package/src/cli/verify-proof/trust.mjs +187 -0
- package/templates/adr-format.md +0 -8
- package/templates/aperant-claude-md-appendix.md +1 -1
- package/templates/context-format.md +1 -9
- package/templates/proof-verification.md +19 -5
- package/workflows/verify-proof.md +56 -14
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exec.mjs — execFile-only execution layer for local_command transports.
|
|
3
|
+
*
|
|
4
|
+
* ID-03 security invariant: drivers with `transport: { local_command }`
|
|
5
|
+
* MUST be invoked through execFile (NO shell, NO pipes, NO redirects).
|
|
6
|
+
*
|
|
7
|
+
* This module exposes ONE function — execLocalCommand — which:
|
|
8
|
+
* 1. Asserts the transport is { local_command: { binary, args[] } }
|
|
9
|
+
* (the manifest validator already structurally enforces this; this
|
|
10
|
+
* is defense-in-depth at runtime).
|
|
11
|
+
* 2. Rejects any args entry that is not a plain string.
|
|
12
|
+
* 3. Invokes node:child_process.execFile (NOT child_process.exec, NOT
|
|
13
|
+
* spawn with shell:true).
|
|
14
|
+
*
|
|
15
|
+
* Grep guard (TD-05): the test verify-proof-local-command-security.test.ts
|
|
16
|
+
* statically asserts that exec.mjs imports execFile from
|
|
17
|
+
* 'node:child_process' and never references child_process.exec / spawn
|
|
18
|
+
* with shell:true.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFile } from 'node:child_process'
|
|
22
|
+
import { promisify } from 'node:util'
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type-check that a transport is the structured local_command variant.
|
|
28
|
+
* @param {unknown} transport
|
|
29
|
+
* @returns {transport is {local_command: {binary: string, args: string[]}}}
|
|
30
|
+
*/
|
|
31
|
+
function isStructuredLocalCommand(transport) {
|
|
32
|
+
if (transport === null || typeof transport !== 'object' || Array.isArray(transport)) return false
|
|
33
|
+
const obj = /** @type {Record<string, unknown>} */ (transport)
|
|
34
|
+
if (obj.local_command === null || typeof obj.local_command !== 'object') return false
|
|
35
|
+
const lc = /** @type {Record<string, unknown>} */ (obj.local_command)
|
|
36
|
+
if (typeof lc.binary !== 'string') return false
|
|
37
|
+
if (!Array.isArray(lc.args)) return false
|
|
38
|
+
for (const a of lc.args) {
|
|
39
|
+
if (typeof a !== 'string') return false
|
|
40
|
+
}
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a manifest's local_command transport via execFile (no shell).
|
|
46
|
+
* Throws if the transport is not the structured variant.
|
|
47
|
+
*
|
|
48
|
+
* @param {{transport: unknown}} manifest
|
|
49
|
+
* @param {string[]} [extraArgs] — appended to manifest.transport.local_command.args.
|
|
50
|
+
* @param {{cwd?: string, env?: Record<string,string>, timeout?: number}} [options]
|
|
51
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
52
|
+
*/
|
|
53
|
+
export async function execLocalCommand(manifest, extraArgs = [], options = {}) {
|
|
54
|
+
const transport = manifest.transport
|
|
55
|
+
if (!isStructuredLocalCommand(transport)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`execLocalCommand: transport is not a structured local_command — refusing to invoke. Got: ${JSON.stringify(transport)}`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
const { binary, args } = transport.local_command
|
|
61
|
+
for (const arg of extraArgs) {
|
|
62
|
+
if (typeof arg !== 'string') {
|
|
63
|
+
throw new Error(`execLocalCommand: extraArgs must be strings, got ${typeof arg}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return execFileAsync(binary, [...args, ...extraArgs], {
|
|
67
|
+
cwd: options.cwd,
|
|
68
|
+
env: options.env,
|
|
69
|
+
timeout: options.timeout,
|
|
70
|
+
// Explicit — never pass shell:true. execFile defaults to no shell, but
|
|
71
|
+
// we surface the invariant in code for grep-guard purposes.
|
|
72
|
+
shell: false,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* idl/index.mjs — framework-side re-export of the IDL v1 runtime registry.
|
|
3
|
+
*
|
|
4
|
+
* The SINGLE SOURCE OF TRUTH lives in `@aperant/driver-sdk` (idl.ts) — see
|
|
5
|
+
* ID-04 in spec.md. This file just re-exports those runtime values so
|
|
6
|
+
* framework-internal callers (bundled drivers, resolver, driver-doctor,
|
|
7
|
+
* conformance kit) can import from the existing relative path without
|
|
8
|
+
* paying the cost of refactoring every callsite.
|
|
9
|
+
*
|
|
10
|
+
* If you add a verb, edit packages/framework-driver-sdk/src/idl.ts. This
|
|
11
|
+
* file requires no maintenance.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
ASSERTION_VERBS,
|
|
16
|
+
ASYNC_VERBS,
|
|
17
|
+
EVIDENCE_VERBS,
|
|
18
|
+
IDL_VERB_ARGS,
|
|
19
|
+
IDL_VERB_NAMES,
|
|
20
|
+
INTERACTION_VERBS,
|
|
21
|
+
isVerbResult,
|
|
22
|
+
LIFECYCLE_VERBS,
|
|
23
|
+
validateVerbArgs,
|
|
24
|
+
} from '@aperant/driver-sdk'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* idl/types.ts — re-export the SDK's IDL types for framework-internal use.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth lives in `@aperant/driver-sdk/idl.ts`. Framework
|
|
5
|
+
* code imports from here so future moves (e.g. an internal-only IDL
|
|
6
|
+
* extension) only touch this re-export, not every call site.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
AssertNetworkArgs,
|
|
11
|
+
AssertOutputArgs,
|
|
12
|
+
AssertTextArgs,
|
|
13
|
+
AssertVisibleArgs,
|
|
14
|
+
AwaitSignalArgs,
|
|
15
|
+
CaptureLogsArgs,
|
|
16
|
+
ClickArgs,
|
|
17
|
+
DomDumpArgs,
|
|
18
|
+
DragDropArgs,
|
|
19
|
+
DriverModule,
|
|
20
|
+
DumpStateArgs,
|
|
21
|
+
ExecuteJsArgs,
|
|
22
|
+
Handle,
|
|
23
|
+
IdlVerbName,
|
|
24
|
+
KeyArgs,
|
|
25
|
+
LaunchArgs,
|
|
26
|
+
NavigateArgs,
|
|
27
|
+
RecordVideoArgs,
|
|
28
|
+
ScreenshotArgs,
|
|
29
|
+
SelectArgs,
|
|
30
|
+
SwipeArgs,
|
|
31
|
+
SwitchContextArgs,
|
|
32
|
+
Target,
|
|
33
|
+
TargetKind,
|
|
34
|
+
TeardownArgs,
|
|
35
|
+
TriggerArgs,
|
|
36
|
+
TypeArgs,
|
|
37
|
+
UploadFileArgs,
|
|
38
|
+
VerbResult,
|
|
39
|
+
WaitForEventArgs,
|
|
40
|
+
WaitForIdleArgs,
|
|
41
|
+
} from '@aperant/driver-sdk'
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://aperant.dev/schemas/driver-manifest-v1.json",
|
|
4
|
+
"title": "Aperant Driver Manifest v1",
|
|
5
|
+
"description": "Structural contract for a verify-proof driver manifest. Validates at publish-time (apt-tools driver doctor) and at load-time (resolver). The transport oneOf rejects free-form shell strings — local_command MUST be a structured {binary, args[]} object per ID-03.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"driverId",
|
|
9
|
+
"version",
|
|
10
|
+
"targets",
|
|
11
|
+
"capabilities",
|
|
12
|
+
"frameworkApiRange",
|
|
13
|
+
"priority",
|
|
14
|
+
"stability",
|
|
15
|
+
"trust",
|
|
16
|
+
"security",
|
|
17
|
+
"transport"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"driverId": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-z][a-z0-9_-]*$"
|
|
23
|
+
},
|
|
24
|
+
"version": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"targets": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "type": "string" }
|
|
30
|
+
},
|
|
31
|
+
"capabilities": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": { "type": "string" }
|
|
34
|
+
},
|
|
35
|
+
"evidence_capabilities": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": { "type": "string" }
|
|
38
|
+
},
|
|
39
|
+
"frameworkApiRange": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"requires": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"allOf": { "type": "array", "items": { "type": "string" } },
|
|
46
|
+
"anyOf": { "type": "array", "items": { "type": "string" } }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"os": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": { "type": "string" }
|
|
52
|
+
},
|
|
53
|
+
"arch": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": { "type": "string" }
|
|
56
|
+
},
|
|
57
|
+
"container_support": { "type": "boolean" },
|
|
58
|
+
"conflicts_with": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "type": "string" }
|
|
61
|
+
},
|
|
62
|
+
"priority": { "type": "number" },
|
|
63
|
+
"stability": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": ["experimental", "beta", "ga"]
|
|
66
|
+
},
|
|
67
|
+
"trust": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"required": ["tier"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"tier": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"enum": [
|
|
74
|
+
"first_party_signed",
|
|
75
|
+
"community_verified",
|
|
76
|
+
"community_unverified",
|
|
77
|
+
"local_unverified"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"publisher": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["name"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"name": { "type": "string" },
|
|
85
|
+
"npm_scope": { "type": "string" },
|
|
86
|
+
"github_org": { "type": "string" },
|
|
87
|
+
"verified": { "type": "boolean" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"provenance": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"scheme": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"enum": ["sigstore", "npm-provenance", "gpg", "none"]
|
|
96
|
+
},
|
|
97
|
+
"identity": { "type": "string" },
|
|
98
|
+
"signature": { "type": "string" },
|
|
99
|
+
"certificate": { "type": "string" }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"integrity": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"manifest_sha256": { "type": "string" },
|
|
106
|
+
"artifact_digest": { "type": "string" }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"security": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"required": ["sandbox_profile", "network"],
|
|
114
|
+
"properties": {
|
|
115
|
+
"sandbox_profile": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"enum": ["read_only", "workspace_write", "system_write"]
|
|
118
|
+
},
|
|
119
|
+
"network": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"enum": ["deny", "allowlist", "allow"]
|
|
122
|
+
},
|
|
123
|
+
"allowed_executables": {
|
|
124
|
+
"type": "array",
|
|
125
|
+
"items": { "type": "string" }
|
|
126
|
+
},
|
|
127
|
+
"allow_shell": { "type": "boolean" },
|
|
128
|
+
"requires_user_approval": { "type": "boolean" }
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"audit": {
|
|
132
|
+
"type": "object",
|
|
133
|
+
"required": ["log_invocations", "log_manifest_hash"],
|
|
134
|
+
"properties": {
|
|
135
|
+
"log_invocations": { "type": "boolean" },
|
|
136
|
+
"log_manifest_hash": { "type": "boolean" }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"preflight": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"items": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"required": ["name", "required"],
|
|
144
|
+
"properties": {
|
|
145
|
+
"name": { "type": "string" },
|
|
146
|
+
"command": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"required": ["binary", "args"],
|
|
149
|
+
"properties": {
|
|
150
|
+
"binary": { "type": "string" },
|
|
151
|
+
"args": {
|
|
152
|
+
"type": "array",
|
|
153
|
+
"items": { "type": "string" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"required": { "type": "boolean" }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"transport": {
|
|
162
|
+
"oneOf": [
|
|
163
|
+
{
|
|
164
|
+
"type": "object",
|
|
165
|
+
"required": ["mcp_server_id"],
|
|
166
|
+
"properties": {
|
|
167
|
+
"mcp_server_id": { "type": "string" }
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"type": "object",
|
|
172
|
+
"required": ["local_command"],
|
|
173
|
+
"properties": {
|
|
174
|
+
"local_command": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"required": ["binary", "args"],
|
|
177
|
+
"properties": {
|
|
178
|
+
"binary": { "type": "string" },
|
|
179
|
+
"args": {
|
|
180
|
+
"type": "array",
|
|
181
|
+
"items": { "type": "string" }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"type": "object",
|
|
189
|
+
"required": ["http_endpoint"],
|
|
190
|
+
"properties": {
|
|
191
|
+
"http_endpoint": { "type": "string" }
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"type": "object",
|
|
196
|
+
"required": ["npm_package"],
|
|
197
|
+
"properties": {
|
|
198
|
+
"npm_package": { "type": "string" }
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"type": "object",
|
|
203
|
+
"required": ["builtin"],
|
|
204
|
+
"properties": {
|
|
205
|
+
"builtin": { "const": true }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manifest-validator.mjs — hand-rolled JSON-Schema subset validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates driver manifests against manifest-schema.json. Implements
|
|
5
|
+
* exactly the JSON-Schema keywords we need:
|
|
6
|
+
*
|
|
7
|
+
* type / required / properties / items / enum / const / pattern / oneOf
|
|
8
|
+
*
|
|
9
|
+
* Returns { valid: true } or { valid: false, errors: [{ path, message }] }
|
|
10
|
+
* where `path` is a JSON-pointer (`/transport/local_command/binary`).
|
|
11
|
+
*
|
|
12
|
+
* No external deps — keeps the validator installable as `dist/` with the
|
|
13
|
+
* framework package and avoids dragging ajv (or any v5+ JSON-Schema
|
|
14
|
+
* validator) into the runtime surface for ~80 lines of validator code per
|
|
15
|
+
* AC15. We don't need allOf / anyOf / definitions — manifest-schema.json
|
|
16
|
+
* is intentionally a thin contract.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
|
|
25
|
+
/** Lazy-loaded JSON-Schema (read once on first call). */
|
|
26
|
+
let cachedSchema = null
|
|
27
|
+
|
|
28
|
+
/** Read the bundled manifest-schema.json. */
|
|
29
|
+
export function loadManifestSchema() {
|
|
30
|
+
if (cachedSchema !== null) return cachedSchema
|
|
31
|
+
const schemaPath = join(__dirname, 'manifest-schema.json')
|
|
32
|
+
cachedSchema = JSON.parse(readFileSync(schemaPath, 'utf-8'))
|
|
33
|
+
return cachedSchema
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate a manifest object against the bundled schema.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} manifest — usually JSON.parse(manifest.json contents).
|
|
40
|
+
* @param {object} [schema] — override the bundled schema (used by tests).
|
|
41
|
+
* @returns {{valid: true} | {valid: false, errors: Array<{path: string, message: string}>}}
|
|
42
|
+
*/
|
|
43
|
+
export function validateManifest(manifest, schema) {
|
|
44
|
+
const root = schema ?? loadManifestSchema()
|
|
45
|
+
const errors = []
|
|
46
|
+
validateNode(manifest, root, '', errors)
|
|
47
|
+
if (errors.length === 0) return { valid: true }
|
|
48
|
+
return { valid: false, errors }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Type check — JSON-Schema's union of "string", "number", "object", "array", "boolean", "null". */
|
|
52
|
+
function checkType(value, type) {
|
|
53
|
+
if (type === 'string') return typeof value === 'string'
|
|
54
|
+
if (type === 'number') return typeof value === 'number' && !Number.isNaN(value)
|
|
55
|
+
if (type === 'integer') return typeof value === 'number' && Number.isInteger(value)
|
|
56
|
+
if (type === 'boolean') return typeof value === 'boolean'
|
|
57
|
+
if (type === 'null') return value === null
|
|
58
|
+
if (type === 'array') return Array.isArray(value)
|
|
59
|
+
if (type === 'object') {
|
|
60
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Human-readable name for the actual shape of a value. Distinguishes
|
|
67
|
+
* `null`, `array`, and plain `object` — JS `typeof` reports all three as
|
|
68
|
+
* `'object'`, which makes error messages like "expected type object, got
|
|
69
|
+
* object" misleading.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} value
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function describeType(value) {
|
|
75
|
+
if (value === null) return 'null'
|
|
76
|
+
if (Array.isArray(value)) return 'array'
|
|
77
|
+
return typeof value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recursive validator. `errors` is mutated.
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} value
|
|
84
|
+
* @param {object} schema
|
|
85
|
+
* @param {string} path — JSON-pointer
|
|
86
|
+
* @param {Array<{path: string, message: string}>} errors
|
|
87
|
+
*/
|
|
88
|
+
function validateNode(value, schema, path, errors) {
|
|
89
|
+
// const
|
|
90
|
+
if ('const' in schema) {
|
|
91
|
+
if (value !== schema.const) {
|
|
92
|
+
errors.push({
|
|
93
|
+
path: path || '/',
|
|
94
|
+
message: `expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`,
|
|
95
|
+
})
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// type
|
|
101
|
+
if (schema.type !== undefined) {
|
|
102
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
103
|
+
if (!types.some((t) => checkType(value, t))) {
|
|
104
|
+
errors.push({
|
|
105
|
+
path: path || '/',
|
|
106
|
+
message: `expected type ${types.join('|')}, got ${describeType(value)}`,
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// enum
|
|
113
|
+
if (Array.isArray(schema.enum)) {
|
|
114
|
+
if (!schema.enum.includes(value)) {
|
|
115
|
+
errors.push({
|
|
116
|
+
path: path || '/',
|
|
117
|
+
message: `value ${JSON.stringify(value)} not in enum [${schema.enum.join(', ')}]`,
|
|
118
|
+
})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// pattern
|
|
124
|
+
if (typeof schema.pattern === 'string' && typeof value === 'string') {
|
|
125
|
+
const re = new RegExp(schema.pattern)
|
|
126
|
+
if (!re.test(value)) {
|
|
127
|
+
errors.push({
|
|
128
|
+
path: path || '/',
|
|
129
|
+
message: `value ${JSON.stringify(value)} does not match pattern ${schema.pattern}`,
|
|
130
|
+
})
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// oneOf
|
|
136
|
+
if (Array.isArray(schema.oneOf)) {
|
|
137
|
+
let matchCount = 0
|
|
138
|
+
const branchErrors = []
|
|
139
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
140
|
+
const sub = schema.oneOf[i]
|
|
141
|
+
const subErrors = []
|
|
142
|
+
validateNode(value, sub, path, subErrors)
|
|
143
|
+
if (subErrors.length === 0) matchCount++
|
|
144
|
+
else branchErrors.push({ branch: i, errors: subErrors })
|
|
145
|
+
}
|
|
146
|
+
if (matchCount !== 1) {
|
|
147
|
+
errors.push({
|
|
148
|
+
path: path || '/',
|
|
149
|
+
message:
|
|
150
|
+
matchCount === 0
|
|
151
|
+
? `oneOf: value matched no branch (${branchErrors.length} branches tried)`
|
|
152
|
+
: `oneOf: value matched ${matchCount} branches (must match exactly 1)`,
|
|
153
|
+
})
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// object validation
|
|
159
|
+
if (checkType(value, 'object')) {
|
|
160
|
+
const obj = /** @type {Record<string, unknown>} */ (value)
|
|
161
|
+
if (Array.isArray(schema.required)) {
|
|
162
|
+
for (const key of schema.required) {
|
|
163
|
+
if (!(key in obj)) {
|
|
164
|
+
errors.push({ path: `${path}/${key}`, message: `required property "${key}" missing` })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
169
|
+
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
170
|
+
if (key in obj) {
|
|
171
|
+
validateNode(obj[key], /** @type {object} */ (subSchema), `${path}/${key}`, errors)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// array validation
|
|
178
|
+
if (checkType(value, 'array') && schema.items) {
|
|
179
|
+
const arr = /** @type {unknown[]} */ (value)
|
|
180
|
+
for (let i = 0; i < arr.length; i++) {
|
|
181
|
+
validateNode(arr[i], schema.items, `${path}/${i}`, errors)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolver.mjs — driver selection + fallback chain.
|
|
3
|
+
*
|
|
4
|
+
* Given a project's runtime capabilities, the discovered drivers, and the
|
|
5
|
+
* test's required IDL capabilities, return the best driver — or throw
|
|
6
|
+
* UnsatisfiedRequiredCapabilityError if no driver in the chain satisfies
|
|
7
|
+
* a REQUIRED capability (ID-02 fail-closed).
|
|
8
|
+
*
|
|
9
|
+
* Scoring (ID-02, ordered high → low):
|
|
10
|
+
* 1. capability_coverage — how many required capabilities the driver has
|
|
11
|
+
* 2. compatibility — does the runtime match the manifest.targets?
|
|
12
|
+
* 3. stability — ga > beta > experimental
|
|
13
|
+
* 4. priority — higher wins (manifest.priority)
|
|
14
|
+
* 5. locality — bundled (packages/framework/drivers/) > user-installed
|
|
15
|
+
* (.aperant/drivers/) > registry (.aperant/registry-cache/)
|
|
16
|
+
*
|
|
17
|
+
* Ties are broken by the next dimension; if every dimension is equal the
|
|
18
|
+
* driver with the lexicographically smaller driverId wins (deterministic).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { UnsatisfiedRequiredCapabilityError } from '@aperant/driver-sdk'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {import('@aperant/driver-sdk').DriverManifest} DriverManifest
|
|
25
|
+
* @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const STABILITY_RANK = { ga: 3, beta: 2, experimental: 1 }
|
|
29
|
+
const LOCALITY_RANK = { bundled: 3, user: 2, registry: 1 }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Score a single driver against the project's runtime + required capabilities.
|
|
33
|
+
*
|
|
34
|
+
* @param {DriverManifest & {locality?: 'bundled'|'user'|'registry'}} manifest
|
|
35
|
+
* @param {RuntimeCapabilities} runtime
|
|
36
|
+
* @param {string[]} required
|
|
37
|
+
* @returns {{score: number[], reason?: string}}
|
|
38
|
+
*/
|
|
39
|
+
export function scoreDriver(manifest, runtime, required) {
|
|
40
|
+
const caps = new Set(manifest.capabilities ?? [])
|
|
41
|
+
const coverage = required.filter((c) => caps.has(c)).length
|
|
42
|
+
const compatibility = matchesRuntime(manifest, runtime) ? 1 : 0
|
|
43
|
+
const stability = STABILITY_RANK[manifest.stability] ?? 0
|
|
44
|
+
const priority = manifest.priority ?? 0
|
|
45
|
+
const locality = LOCALITY_RANK[manifest.locality ?? 'bundled'] ?? 0
|
|
46
|
+
return { score: [coverage, compatibility, stability, priority, locality] }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a manifest's targets[] mentions a runtime the project has.
|
|
51
|
+
*
|
|
52
|
+
* @param {DriverManifest} manifest
|
|
53
|
+
* @param {RuntimeCapabilities} runtime
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function matchesRuntime(manifest, runtime) {
|
|
57
|
+
const targets = manifest.targets ?? []
|
|
58
|
+
for (const t of targets) {
|
|
59
|
+
// Strip any version range suffix (e.g. "electron@>=25" → "electron").
|
|
60
|
+
const stripped = String(t).split('@')[0].toLowerCase()
|
|
61
|
+
if (stripped === 'electron' && runtime.is_electron) return true
|
|
62
|
+
if (stripped === 'tauri' && runtime.is_tauri) return true
|
|
63
|
+
if ((stripped === 'browser' || stripped === 'web') && runtime.is_web_frontend) return true
|
|
64
|
+
if (stripped === 'next' && runtime.is_nextjs) return true
|
|
65
|
+
if (stripped === 'nuxt' && runtime.is_nuxt) return true
|
|
66
|
+
if (stripped === 'expo' && runtime.is_expo) return true
|
|
67
|
+
if (stripped === 'react-native' && runtime.is_react_native) return true
|
|
68
|
+
if (stripped === 'node' && runtime.has_node) return true
|
|
69
|
+
if (stripped === 'cli' && runtime.has_node) return true
|
|
70
|
+
if (stripped === 'api' && runtime.has_node) return true
|
|
71
|
+
if (stripped === 'fake') return true // fixture-only — always matches
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compare two driver-score tuples lexicographically.
|
|
78
|
+
* @param {number[]} a
|
|
79
|
+
* @param {number[]} b
|
|
80
|
+
* @returns {number}
|
|
81
|
+
*/
|
|
82
|
+
function compareScores(a, b) {
|
|
83
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
84
|
+
if (a[i] !== b[i]) return b[i] - a[i] // higher wins
|
|
85
|
+
}
|
|
86
|
+
return 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the best driver for a given (runtime, drivers, override, required)
|
|
91
|
+
* tuple.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} opts
|
|
94
|
+
* @param {DriverManifest[]} opts.drivers — manifests of every discovered driver.
|
|
95
|
+
* @param {RuntimeCapabilities} opts.runtime — project's runtime capabilities.
|
|
96
|
+
* @param {string[]} opts.required_capabilities — IDL verbs the test needs.
|
|
97
|
+
* @param {string[]} [opts.required_stability] — accepted stability values; default ['ga'].
|
|
98
|
+
* @param {string[]} [opts.override] — config.json:verification.runtimes[]: explicit driverId precedence.
|
|
99
|
+
* @returns {{selected: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}}
|
|
100
|
+
* @throws {UnsatisfiedRequiredCapabilityError} when no driver satisfies a REQUIRED capability.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveDriver(opts) {
|
|
103
|
+
const drivers = opts.drivers ?? []
|
|
104
|
+
const runtime = opts.runtime
|
|
105
|
+
const required = opts.required_capabilities ?? []
|
|
106
|
+
const stabilityFilter = new Set(opts.required_stability ?? ['ga'])
|
|
107
|
+
const override = opts.override ?? []
|
|
108
|
+
|
|
109
|
+
const fallback = []
|
|
110
|
+
|
|
111
|
+
// Apply override precedence first (config-listed driverIds come first).
|
|
112
|
+
const overrideOrdered = []
|
|
113
|
+
const overrideSet = new Set(override)
|
|
114
|
+
for (const id of override) {
|
|
115
|
+
const d = drivers.find((m) => m.driverId === id)
|
|
116
|
+
if (d) overrideOrdered.push(d)
|
|
117
|
+
}
|
|
118
|
+
const remaining = drivers.filter((m) => !overrideSet.has(m.driverId))
|
|
119
|
+
const ordered = [...overrideOrdered, ...remaining]
|
|
120
|
+
|
|
121
|
+
// Filter by stability; rejected drivers go into fallback_attempts.
|
|
122
|
+
const stableOnly = []
|
|
123
|
+
for (const d of ordered) {
|
|
124
|
+
if (!stabilityFilter.has(d.stability)) {
|
|
125
|
+
fallback.push({
|
|
126
|
+
driver_id: d.driverId,
|
|
127
|
+
reason: `stability ${d.stability} not in allowed set [${[...stabilityFilter].join(', ')}]`,
|
|
128
|
+
})
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
stableOnly.push(d)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Score every survivor against required capabilities.
|
|
135
|
+
const scored = stableOnly.map((m) => ({ manifest: m, ...scoreDriver(m, runtime, required) }))
|
|
136
|
+
|
|
137
|
+
// Sort highest-first, tie-break on driverId for determinism.
|
|
138
|
+
scored.sort((a, b) => {
|
|
139
|
+
const cmp = compareScores(a.score, b.score)
|
|
140
|
+
if (cmp !== 0) return cmp
|
|
141
|
+
return a.manifest.driverId.localeCompare(b.manifest.driverId)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Pick the top scorer that covers EVERY required capability. Others go into fallback.
|
|
145
|
+
for (const candidate of scored) {
|
|
146
|
+
const caps = new Set(candidate.manifest.capabilities ?? [])
|
|
147
|
+
const missing = required.filter((c) => !caps.has(c))
|
|
148
|
+
if (missing.length === 0) {
|
|
149
|
+
return { selected: candidate.manifest, fallback_attempts: fallback }
|
|
150
|
+
}
|
|
151
|
+
fallback.push({
|
|
152
|
+
driver_id: candidate.manifest.driverId,
|
|
153
|
+
reason: `missing required capabilities: ${missing.join(', ')}`,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Exhausted the chain — fail closed (ID-02).
|
|
158
|
+
throw new UnsatisfiedRequiredCapabilityError(
|
|
159
|
+
`No driver in the chain satisfies every required capability: [${required.join(', ')}]`,
|
|
160
|
+
required[0],
|
|
161
|
+
fallback,
|
|
162
|
+
)
|
|
163
|
+
}
|