@aperant/framework 0.8.1 → 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 (308) hide show
  1. package/CHANGELOG.md +443 -1
  2. package/agents/apt-planner.md +39 -7
  3. package/dist/cli/commands/adr.mjs +1 -1
  4. package/dist/cli/commands/audit-branch-current.d.mts +25 -0
  5. package/dist/cli/commands/audit-branch-current.d.mts.map +1 -0
  6. package/dist/cli/commands/audit-branch-current.mjs +252 -0
  7. package/dist/cli/commands/audit-branch-current.mjs.map +1 -0
  8. package/dist/cli/commands/audit.d.mts.map +1 -1
  9. package/dist/cli/commands/audit.mjs +13 -2
  10. package/dist/cli/commands/audit.mjs.map +1 -1
  11. package/dist/cli/commands/check-version.d.mts.map +1 -1
  12. package/dist/cli/commands/check-version.mjs +19 -1
  13. package/dist/cli/commands/check-version.mjs.map +1 -1
  14. package/dist/cli/commands/context.mjs +3 -3
  15. package/dist/cli/commands/context.mjs.map +1 -1
  16. package/dist/cli/commands/coverage-check.d.mts.map +1 -1
  17. package/dist/cli/commands/coverage-check.mjs +74 -7
  18. package/dist/cli/commands/coverage-check.mjs.map +1 -1
  19. package/dist/cli/commands/detect-runtime.d.mts +6 -0
  20. package/dist/cli/commands/detect-runtime.d.mts.map +1 -0
  21. package/dist/cli/commands/detect-runtime.mjs +142 -0
  22. package/dist/cli/commands/detect-runtime.mjs.map +1 -0
  23. package/dist/cli/commands/driver-doctor.d.mts +29 -0
  24. package/dist/cli/commands/driver-doctor.d.mts.map +1 -0
  25. package/dist/cli/commands/driver-doctor.mjs +291 -0
  26. package/dist/cli/commands/driver-doctor.mjs.map +1 -0
  27. package/dist/cli/commands/gate.mjs +14 -0
  28. package/dist/cli/commands/gate.mjs.map +1 -1
  29. package/dist/cli/commands/init.d.mts.map +1 -1
  30. package/dist/cli/commands/init.mjs +9 -6
  31. package/dist/cli/commands/init.mjs.map +1 -1
  32. package/dist/cli/commands/install-from-source.d.mts +2 -0
  33. package/dist/cli/commands/install-from-source.d.mts.map +1 -0
  34. package/dist/cli/commands/install-from-source.mjs +2 -0
  35. package/dist/cli/commands/install-from-source.mjs.map +1 -0
  36. package/dist/cli/commands/modes.mjs +1 -1
  37. package/dist/cli/commands/route.d.mts.map +1 -1
  38. package/dist/cli/commands/route.mjs +38 -1
  39. package/dist/cli/commands/route.mjs.map +1 -1
  40. package/dist/cli/commands/triage.mjs +1 -1
  41. package/dist/cli/config/load.d.mts.map +1 -1
  42. package/dist/cli/config/load.mjs +9 -3
  43. package/dist/cli/config/load.mjs.map +1 -1
  44. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +1 -1
  45. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  46. package/dist/cli/coordination/event-schema.mjs +18 -0
  47. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  48. package/dist/cli/coverage-check/user-outcomes.d.mts +84 -0
  49. package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -0
  50. package/dist/cli/coverage-check/user-outcomes.mjs +265 -0
  51. package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -0
  52. package/dist/cli/dispatch.d.mts.map +1 -1
  53. package/dist/cli/dispatch.mjs +18 -1
  54. package/dist/cli/dispatch.mjs.map +1 -1
  55. package/dist/cli/gate/context.d.mts +1 -0
  56. package/dist/cli/gate/context.d.mts.map +1 -1
  57. package/dist/cli/gate/context.mjs +14 -1
  58. package/dist/cli/gate/context.mjs.map +1 -1
  59. package/dist/cli/gate/gates/consistency-check.d.mts +1 -0
  60. package/dist/cli/gate/gates/consistency-check.d.mts.map +1 -1
  61. package/dist/cli/gate/gates/consistency-check.mjs +1 -0
  62. package/dist/cli/gate/gates/consistency-check.mjs.map +1 -1
  63. package/dist/cli/gate/gates/review-clean.d.mts +1 -0
  64. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  65. package/dist/cli/gate/gates/review-clean.mjs +1 -0
  66. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  67. package/dist/cli/gate/gates/verify-approved.d.mts +1 -0
  68. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  69. package/dist/cli/gate/gates/verify-approved.mjs +1 -0
  70. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  71. package/dist/cli/gate/registry.d.mts.map +1 -1
  72. package/dist/cli/gate/registry.mjs +9 -1
  73. package/dist/cli/gate/registry.mjs.map +1 -1
  74. package/dist/cli/host/detect.mjs +1 -1
  75. package/dist/cli/host/detect.mjs.map +1 -1
  76. package/dist/cli/install/find-workspace-framework-version.d.mts +16 -0
  77. package/dist/cli/install/find-workspace-framework-version.d.mts.map +1 -0
  78. package/dist/cli/install/find-workspace-framework-version.mjs +85 -0
  79. package/dist/cli/install/find-workspace-framework-version.mjs.map +1 -0
  80. package/dist/cli/install/install-from-source.d.mts +10 -0
  81. package/dist/cli/install/install-from-source.d.mts.map +1 -0
  82. package/dist/cli/install/install-from-source.mjs +271 -0
  83. package/dist/cli/install/install-from-source.mjs.map +1 -0
  84. package/dist/cli/install/install-kernel.d.mts.map +1 -1
  85. package/dist/cli/install/install-kernel.mjs +24 -0
  86. package/dist/cli/install/install-kernel.mjs.map +1 -1
  87. package/dist/cli/install/legacy-paths.d.mts +33 -11
  88. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  89. package/dist/cli/install/legacy-paths.mjs +163 -39
  90. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  91. package/dist/cli/install/runtime-detect.mjs +1 -1
  92. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  93. package/dist/cli/install/runtime-migrate.d.mts +34 -14
  94. package/dist/cli/install/runtime-migrate.d.mts.map +1 -1
  95. package/dist/cli/install/runtime-migrate.mjs +63 -30
  96. package/dist/cli/install/runtime-migrate.mjs.map +1 -1
  97. package/dist/cli/install/transforms/codex.d.mts.map +1 -1
  98. package/dist/cli/install/transforms/codex.mjs +28 -17
  99. package/dist/cli/install/transforms/codex.mjs.map +1 -1
  100. package/dist/cli/skill-author/contract.mjs +2 -2
  101. package/dist/cli/skill-author/contract.mjs.map +1 -1
  102. package/dist/cli/verify-proof/audit.d.mts +34 -0
  103. package/dist/cli/verify-proof/audit.d.mts.map +1 -0
  104. package/dist/cli/verify-proof/audit.mjs +53 -0
  105. package/dist/cli/verify-proof/audit.mjs.map +1 -0
  106. package/dist/cli/verify-proof/exec.d.mts +20 -0
  107. package/dist/cli/verify-proof/exec.d.mts.map +1 -0
  108. package/dist/cli/verify-proof/exec.mjs +74 -0
  109. package/dist/cli/verify-proof/exec.mjs.map +1 -0
  110. package/dist/cli/verify-proof/idl/index.d.mts +2 -0
  111. package/dist/cli/verify-proof/idl/index.d.mts.map +1 -0
  112. package/dist/cli/verify-proof/idl/index.mjs +13 -0
  113. package/dist/cli/verify-proof/idl/index.mjs.map +1 -0
  114. package/dist/cli/verify-proof/idl/types.d.ts +10 -0
  115. package/dist/cli/verify-proof/idl/types.d.ts.map +1 -0
  116. package/dist/cli/verify-proof/idl/types.js +10 -0
  117. package/dist/cli/verify-proof/idl/types.js.map +1 -0
  118. package/dist/cli/verify-proof/manifest-validator.d.mts +19 -0
  119. package/dist/cli/verify-proof/manifest-validator.d.mts.map +1 -0
  120. package/dist/cli/verify-proof/manifest-validator.mjs +182 -0
  121. package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -0
  122. package/dist/cli/verify-proof/resolver.d.mts +105 -0
  123. package/dist/cli/verify-proof/resolver.d.mts.map +1 -0
  124. package/dist/cli/verify-proof/resolver.mjs +320 -0
  125. package/dist/cli/verify-proof/resolver.mjs.map +1 -0
  126. package/dist/cli/verify-proof/runtime-detect.d.mts +30 -0
  127. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -0
  128. package/dist/cli/verify-proof/runtime-detect.mjs +209 -0
  129. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -0
  130. package/dist/cli/verify-proof/trust.d.mts +70 -0
  131. package/dist/cli/verify-proof/trust.d.mts.map +1 -0
  132. package/dist/cli/verify-proof/trust.mjs +174 -0
  133. package/dist/cli/verify-proof/trust.mjs.map +1 -0
  134. package/dist/driver-sdk/conformance.d.ts +41 -0
  135. package/dist/driver-sdk/conformance.d.ts.map +1 -0
  136. package/dist/driver-sdk/conformance.js +123 -0
  137. package/dist/driver-sdk/conformance.js.map +1 -0
  138. package/dist/driver-sdk/errors.d.ts +75 -0
  139. package/dist/driver-sdk/errors.d.ts.map +1 -0
  140. package/dist/driver-sdk/errors.js +80 -0
  141. package/dist/driver-sdk/errors.js.map +1 -0
  142. package/dist/driver-sdk/idl.d.ts +221 -0
  143. package/dist/driver-sdk/idl.d.ts.map +1 -0
  144. package/dist/driver-sdk/idl.js +140 -0
  145. package/dist/driver-sdk/idl.js.map +1 -0
  146. package/dist/driver-sdk/index.d.ts +28 -0
  147. package/dist/driver-sdk/index.d.ts.map +1 -0
  148. package/dist/driver-sdk/index.js +28 -0
  149. package/dist/driver-sdk/index.js.map +1 -0
  150. package/dist/driver-sdk/manifest.d.ts +93 -0
  151. package/dist/driver-sdk/manifest.d.ts.map +1 -0
  152. package/dist/driver-sdk/manifest.js +12 -0
  153. package/dist/driver-sdk/manifest.js.map +1 -0
  154. package/dist/driver-sdk/retry.d.ts +33 -0
  155. package/dist/driver-sdk/retry.d.ts.map +1 -0
  156. package/dist/driver-sdk/retry.js +50 -0
  157. package/dist/driver-sdk/retry.js.map +1 -0
  158. package/dist/plugin/.claude-plugin/plugin.json +4 -1
  159. package/dist/plugin/agents/apt-planner.md +39 -7
  160. package/dist/plugin/skills/apt/SKILL.md +75 -6
  161. package/dist/plugin/skills/apt-caveman/SKILL.md +64 -0
  162. package/dist/plugin/skills/apt-debug/SKILL.md +36 -4
  163. package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +2 -11
  164. package/dist/plugin/skills/apt-discuss/SKILL.md +4 -4
  165. package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +2 -11
  166. package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +0 -8
  167. package/dist/plugin/skills/apt-execute/SKILL.md +27 -4
  168. package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +2 -10
  169. package/dist/plugin/skills/apt-fan-out/SKILL.md +36 -1
  170. package/dist/plugin/skills/apt-handoff/SKILL.md +228 -0
  171. package/dist/plugin/skills/apt-improve/DEEPENING.md +0 -8
  172. package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +0 -8
  173. package/dist/plugin/skills/apt-improve/LANGUAGE.md +0 -8
  174. package/dist/plugin/skills/apt-improve/SKILL.md +4 -4
  175. package/dist/plugin/skills/apt-plan/SKILL.md +73 -5
  176. package/dist/plugin/skills/apt-plan/adapters/conductor.md +1 -1
  177. package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -1
  178. package/dist/plugin/skills/apt-prototype/LOGIC.md +0 -8
  179. package/dist/plugin/skills/apt-prototype/SKILL.md +4 -4
  180. package/dist/plugin/skills/apt-prototype/UI.md +0 -8
  181. package/dist/plugin/skills/apt-quick/SKILL.md +31 -1
  182. package/dist/plugin/skills/apt-research/SKILL.md +526 -0
  183. package/dist/plugin/skills/apt-research/appendices/budget-loop.md +397 -0
  184. package/dist/plugin/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  185. package/dist/plugin/skills/apt-research/appendices/domain-tools.md +236 -0
  186. package/dist/plugin/skills/apt-resume/SKILL.md +54 -31
  187. package/dist/plugin/skills/apt-run/SKILL.md +114 -16
  188. package/dist/plugin/skills/apt-setup/SKILL.md +97 -8
  189. package/dist/plugin/skills/apt-ship/SKILL.md +165 -9
  190. package/dist/plugin/skills/apt-spar/SKILL.md +84 -23
  191. package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +0 -8
  192. package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +0 -7
  193. package/dist/plugin/skills/apt-triage/SKILL.md +2 -2
  194. package/dist/plugin/skills/apt-update/SKILL.md +74 -17
  195. package/dist/plugin/skills/apt-verify-proof/SKILL.md +87 -1
  196. package/dist/plugin/skills/apt-zoom-out/SKILL.md +3 -3
  197. package/dist/schemas/quick-task.d.ts +17 -17
  198. package/dist/schemas/quick-task.d.ts.map +1 -1
  199. package/dist/schemas/quick-task.js +2 -2
  200. package/dist/schemas/quick-task.js.map +1 -1
  201. package/dist/types/config.d.ts +33 -0
  202. package/dist/types/config.d.ts.map +1 -1
  203. package/dist/types/state.d.ts +1 -1
  204. package/dist/types/state.d.ts.map +1 -1
  205. package/dist/types/task-record.d.ts +1 -1
  206. package/dist/types/task-record.d.ts.map +1 -1
  207. package/drivers/.gitkeep +0 -0
  208. package/drivers/api/README.md +40 -0
  209. package/drivers/api/driver.mjs +59 -0
  210. package/drivers/api/manifest.json +26 -0
  211. package/drivers/browser/README.md +105 -0
  212. package/drivers/browser/driver.mjs +134 -0
  213. package/drivers/browser/manifest.json +35 -0
  214. package/drivers/cli/README.md +44 -0
  215. package/drivers/cli/driver.mjs +62 -0
  216. package/drivers/cli/manifest.json +28 -0
  217. package/drivers/electron/README.md +64 -0
  218. package/drivers/electron/driver.mjs +87 -0
  219. package/drivers/electron/manifest.json +37 -0
  220. package/package.json +138 -133
  221. package/prompts/planner.md +1 -1
  222. package/prompts/spec_writer.md +1 -1
  223. package/skills/apt/SKILL.md +75 -6
  224. package/skills/apt-caveman/SKILL.md +64 -0
  225. package/skills/apt-debug/SKILL.md +36 -4
  226. package/skills/apt-debug/appendices/diagnose-discipline.md +2 -11
  227. package/skills/apt-discuss/SKILL.md +4 -4
  228. package/skills/apt-discuss/appendices/grill-discipline.md +2 -11
  229. package/skills/apt-discuss/appendices/zoom-out-helper.md +0 -8
  230. package/skills/apt-execute/SKILL.md +27 -4
  231. package/skills/apt-execute/appendices/tdd-mode.md +2 -10
  232. package/skills/apt-fan-out/SKILL.md +36 -1
  233. package/skills/apt-handoff/SKILL.md +228 -0
  234. package/skills/apt-improve/DEEPENING.md +0 -8
  235. package/skills/apt-improve/INTERFACE-DESIGN.md +0 -8
  236. package/skills/apt-improve/LANGUAGE.md +0 -8
  237. package/skills/apt-improve/SKILL.md +4 -4
  238. package/skills/apt-plan/SKILL.md +73 -5
  239. package/skills/apt-plan/adapters/conductor.md +1 -1
  240. package/skills/apt-planner.md +17 -1
  241. package/skills/apt-pr-review/SKILL.md +1 -1
  242. package/skills/apt-prototype/LOGIC.md +0 -8
  243. package/skills/apt-prototype/SKILL.md +4 -4
  244. package/skills/apt-prototype/UI.md +0 -8
  245. package/skills/apt-quick/SKILL.md +31 -1
  246. package/skills/apt-research/SKILL.md +526 -0
  247. package/skills/apt-research/appendices/budget-loop.md +397 -0
  248. package/skills/apt-research/appendices/claim-graph-schema.md +206 -0
  249. package/skills/apt-research/appendices/domain-tools.md +236 -0
  250. package/skills/apt-resume/SKILL.md +54 -31
  251. package/skills/apt-run/SKILL.md +114 -16
  252. package/skills/apt-setup/SKILL.md +97 -8
  253. package/skills/apt-ship/SKILL.md +165 -9
  254. package/skills/apt-spar/SKILL.md +84 -23
  255. package/skills/apt-triage/AGENT-BRIEF.md +0 -8
  256. package/skills/apt-triage/OUT-OF-SCOPE.md +0 -7
  257. package/skills/apt-triage/SKILL.md +2 -2
  258. package/skills/apt-update/SKILL.md +74 -17
  259. package/skills/apt-verify-proof/SKILL.md +87 -1
  260. package/skills/apt-zoom-out/SKILL.md +3 -3
  261. package/src/cli/commands/adr.mjs +1 -1
  262. package/src/cli/commands/audit-branch-current.mjs +250 -0
  263. package/src/cli/commands/audit.mjs +13 -2
  264. package/src/cli/commands/check-version.mjs +20 -1
  265. package/src/cli/commands/context.mjs +3 -3
  266. package/src/cli/commands/coverage-check.mjs +126 -44
  267. package/src/cli/commands/detect-runtime.mjs +143 -0
  268. package/src/cli/commands/driver-doctor.mjs +294 -0
  269. package/src/cli/commands/gate.mjs +15 -0
  270. package/src/cli/commands/init.mjs +9 -6
  271. package/src/cli/commands/install-from-source.mjs +1 -0
  272. package/src/cli/commands/modes.mjs +1 -1
  273. package/src/cli/commands/route.mjs +42 -1
  274. package/src/cli/commands/triage.mjs +1 -1
  275. package/src/cli/config/load.mjs +9 -3
  276. package/src/cli/coordination/auto-emit-artifact-ready.mjs +1 -1
  277. package/src/cli/coordination/event-schema.mjs +19 -0
  278. package/src/cli/coverage-check/user-outcomes.mjs +273 -0
  279. package/src/cli/dispatch.mjs +17 -1
  280. package/src/cli/gate/context.mjs +20 -1
  281. package/src/cli/gate/gates/consistency-check.mjs +1 -0
  282. package/src/cli/gate/gates/review-clean.mjs +1 -0
  283. package/src/cli/gate/gates/verify-approved.mjs +1 -0
  284. package/src/cli/gate/registry.mjs +11 -1
  285. package/src/cli/host/detect.mjs +1 -1
  286. package/src/cli/install/find-workspace-framework-version.mjs +88 -0
  287. package/src/cli/install/install-from-source.mjs +276 -0
  288. package/src/cli/install/install-kernel.mjs +24 -0
  289. package/src/cli/install/legacy-paths.mjs +164 -39
  290. package/src/cli/install/runtime-detect.mjs +1 -1
  291. package/src/cli/install/runtime-migrate.mjs +63 -30
  292. package/src/cli/install/transforms/codex.mjs +26 -17
  293. package/src/cli/skill-author/contract.mjs +2 -2
  294. package/src/cli/verify-proof/.gitkeep +0 -0
  295. package/src/cli/verify-proof/audit.mjs +55 -0
  296. package/src/cli/verify-proof/exec.mjs +74 -0
  297. package/src/cli/verify-proof/idl/index.mjs +23 -0
  298. package/src/cli/verify-proof/idl/types.ts +42 -0
  299. package/src/cli/verify-proof/manifest-schema.json +212 -0
  300. package/src/cli/verify-proof/manifest-validator.mjs +184 -0
  301. package/src/cli/verify-proof/resolver.mjs +330 -0
  302. package/src/cli/verify-proof/runtime-detect.mjs +211 -0
  303. package/src/cli/verify-proof/trust.mjs +187 -0
  304. package/templates/adr-format.md +0 -8
  305. package/templates/aperant-claude-md-appendix.md +1 -1
  306. package/templates/context-format.md +1 -9
  307. package/templates/proof-verification.md +51 -8
  308. package/workflows/verify-proof.md +130 -19
@@ -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,330 @@
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
+ * 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.
31
+ */
32
+
33
+ import { UnsatisfiedRequiredCapabilityError } from '@aperant/framework/driver-sdk'
34
+
35
+ /**
36
+ * @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
37
+ * @typedef {import('./runtime-detect.mjs').RuntimeCapabilities} RuntimeCapabilities
38
+ */
39
+
40
+ const STABILITY_RANK = { ga: 3, beta: 2, experimental: 1 }
41
+ const LOCALITY_RANK = { bundled: 3, user: 2, registry: 1 }
42
+
43
+ /**
44
+ * Score a single driver against the project's runtime + required capabilities.
45
+ *
46
+ * @param {DriverManifest & {locality?: 'bundled'|'user'|'registry'}} manifest
47
+ * @param {RuntimeCapabilities} runtime
48
+ * @param {string[]} required
49
+ * @returns {{score: number[], reason?: string}}
50
+ */
51
+ export function scoreDriver(manifest, runtime, required) {
52
+ const caps = new Set(manifest.capabilities ?? [])
53
+ const coverage = required.filter((c) => caps.has(c)).length
54
+ const compatibility = matchesRuntime(manifest, runtime) ? 1 : 0
55
+ const stability = STABILITY_RANK[manifest.stability] ?? 0
56
+ const priority = manifest.priority ?? 0
57
+ const locality = LOCALITY_RANK[manifest.locality ?? 'bundled'] ?? 0
58
+ return { score: [coverage, compatibility, stability, priority, locality] }
59
+ }
60
+
61
+ /**
62
+ * Whether a manifest's targets[] mentions a runtime the project has.
63
+ *
64
+ * @param {DriverManifest} manifest
65
+ * @param {RuntimeCapabilities} runtime
66
+ * @returns {boolean}
67
+ */
68
+ function matchesRuntime(manifest, runtime) {
69
+ const targets = manifest.targets ?? []
70
+ for (const t of targets) {
71
+ // Strip any version range suffix (e.g. "electron@>=25" → "electron").
72
+ const stripped = String(t).split('@')[0].toLowerCase()
73
+ if (stripped === 'electron' && runtime.is_electron) return true
74
+ if (stripped === 'tauri' && runtime.is_tauri) return true
75
+ if ((stripped === 'browser' || stripped === 'web') && runtime.is_web_frontend) return true
76
+ if (stripped === 'next' && runtime.is_nextjs) return true
77
+ if (stripped === 'nuxt' && runtime.is_nuxt) return true
78
+ if (stripped === 'expo' && runtime.is_expo) return true
79
+ if (stripped === 'react-native' && runtime.is_react_native) return true
80
+ if (stripped === 'node' && runtime.has_node) return true
81
+ if (stripped === 'cli' && runtime.has_node) return true
82
+ if (stripped === 'api' && runtime.has_node) return true
83
+ if (stripped === 'fake') return true // fixture-only — always matches
84
+ }
85
+ return false
86
+ }
87
+
88
+ /**
89
+ * Compare two driver-score tuples lexicographically.
90
+ * @param {number[]} a
91
+ * @param {number[]} b
92
+ * @returns {number}
93
+ */
94
+ function compareScores(a, b) {
95
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
96
+ if (a[i] !== b[i]) return b[i] - a[i] // higher wins
97
+ }
98
+ return 0
99
+ }
100
+
101
+ /**
102
+ * Resolve the best driver for a given (runtime, drivers, override, required)
103
+ * tuple.
104
+ *
105
+ * @param {Object} opts
106
+ * @param {DriverManifest[]} opts.drivers — manifests of every discovered driver.
107
+ * @param {RuntimeCapabilities} opts.runtime — project's runtime capabilities.
108
+ * @param {string[]} opts.required_capabilities — IDL verbs the test needs.
109
+ * @param {string[]} [opts.required_stability] — accepted stability values; default ['ga'].
110
+ * @param {string[]} [opts.override] — config.json:verification.runtimes[]: explicit driverId precedence.
111
+ * @returns {{selected: DriverManifest, fallback_attempts: Array<{driver_id: string, reason: string}>}}
112
+ * @throws {UnsatisfiedRequiredCapabilityError} when no driver satisfies a REQUIRED capability.
113
+ */
114
+ export function resolveDriver(opts) {
115
+ const drivers = opts.drivers ?? []
116
+ const runtime = opts.runtime
117
+ const required = opts.required_capabilities ?? []
118
+ const stabilityFilter = new Set(opts.required_stability ?? ['ga'])
119
+ const override = opts.override ?? []
120
+
121
+ const fallback = []
122
+
123
+ // Apply override precedence first (config-listed driverIds come first).
124
+ const overrideOrdered = []
125
+ const overrideSet = new Set(override)
126
+ for (const id of override) {
127
+ const d = drivers.find((m) => m.driverId === id)
128
+ if (d) overrideOrdered.push(d)
129
+ }
130
+ const remaining = drivers.filter((m) => !overrideSet.has(m.driverId))
131
+ const ordered = [...overrideOrdered, ...remaining]
132
+
133
+ // Filter by stability; rejected drivers go into fallback_attempts.
134
+ const stableOnly = []
135
+ for (const d of ordered) {
136
+ if (!stabilityFilter.has(d.stability)) {
137
+ fallback.push({
138
+ driver_id: d.driverId,
139
+ reason: `stability ${d.stability} not in allowed set [${[...stabilityFilter].join(', ')}]`,
140
+ })
141
+ continue
142
+ }
143
+ stableOnly.push(d)
144
+ }
145
+
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) }))
169
+
170
+ // Sort highest-first, tie-break on driverId for determinism.
171
+ scored.sort((a, b) => {
172
+ const cmp = compareScores(a.score, b.score)
173
+ if (cmp !== 0) return cmp
174
+ return a.manifest.driverId.localeCompare(b.manifest.driverId)
175
+ })
176
+
177
+ // Pick the top scorer that covers EVERY required capability. Others go into fallback.
178
+ for (const candidate of scored) {
179
+ const caps = new Set(candidate.manifest.capabilities ?? [])
180
+ const missing = required.filter((c) => !caps.has(c))
181
+ if (missing.length === 0) {
182
+ return { selected: candidate.manifest, fallback_attempts: fallback }
183
+ }
184
+ fallback.push({
185
+ driver_id: candidate.manifest.driverId,
186
+ reason: `missing required capabilities: ${missing.join(', ')}`,
187
+ })
188
+ }
189
+
190
+ // Exhausted the chain — fail closed (ID-02).
191
+ throw new UnsatisfiedRequiredCapabilityError(
192
+ `No driver in the chain satisfies every required capability: [${required.join(', ')}]`,
193
+ required[0],
194
+ fallback,
195
+ )
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
+ }