@aperant/framework 0.11.0 → 0.12.0

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 (147) hide show
  1. package/CHANGELOG.md +213 -30
  2. package/agents/apt-planner.md +15 -0
  3. package/agents/apt-verifier.md +15 -10
  4. package/bin/apt-proof-video.mjs +256 -76
  5. package/dist/cli/commands/apply-recipe.d.mts +6 -0
  6. package/dist/cli/commands/apply-recipe.d.mts.map +1 -0
  7. package/dist/cli/commands/apply-recipe.mjs +98 -0
  8. package/dist/cli/commands/apply-recipe.mjs.map +1 -0
  9. package/dist/cli/commands/mobile-prepare.d.mts +53 -0
  10. package/dist/cli/commands/mobile-prepare.d.mts.map +1 -0
  11. package/dist/cli/commands/mobile-prepare.mjs +139 -0
  12. package/dist/cli/commands/mobile-prepare.mjs.map +1 -0
  13. package/dist/cli/commands/recipe-diff.d.mts +6 -0
  14. package/dist/cli/commands/recipe-diff.d.mts.map +1 -0
  15. package/dist/cli/commands/recipe-diff.mjs +75 -0
  16. package/dist/cli/commands/recipe-diff.mjs.map +1 -0
  17. package/dist/cli/commands/roadmap.d.mts.map +1 -1
  18. package/dist/cli/commands/roadmap.mjs +53 -0
  19. package/dist/cli/commands/roadmap.mjs.map +1 -1
  20. package/dist/cli/commands/validate-evidence.d.mts +1 -1
  21. package/dist/cli/commands/verify-sandbox.d.mts +6 -0
  22. package/dist/cli/commands/verify-sandbox.d.mts.map +1 -0
  23. package/dist/cli/commands/verify-sandbox.mjs +200 -0
  24. package/dist/cli/commands/verify-sandbox.mjs.map +1 -0
  25. package/dist/cli/consistency/parse-qa.d.mts.map +1 -1
  26. package/dist/cli/consistency/parse-qa.mjs +18 -4
  27. package/dist/cli/consistency/parse-qa.mjs.map +1 -1
  28. package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
  29. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +11 -10
  30. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
  31. package/dist/cli/coverage-check/user-outcomes.d.mts +7 -2
  32. package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -1
  33. package/dist/cli/coverage-check/user-outcomes.mjs +50 -17
  34. package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -1
  35. package/dist/cli/design/frontmatter-schema.d.mts +8 -8
  36. package/dist/cli/dispatch.d.mts.map +1 -1
  37. package/dist/cli/dispatch.mjs +28 -0
  38. package/dist/cli/dispatch.mjs.map +1 -1
  39. package/dist/cli/gate/gates/verify-approved.d.mts +0 -48
  40. package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
  41. package/dist/cli/gate/gates/verify-approved.mjs +23 -80
  42. package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
  43. package/dist/cli/install/mcp-server-specs.d.mts +6 -5
  44. package/dist/cli/install/mcp-server-specs.d.mts.map +1 -1
  45. package/dist/cli/install/mcp-server-specs.mjs +22 -4
  46. package/dist/cli/install/mcp-server-specs.mjs.map +1 -1
  47. package/dist/cli/personas/sidecar.d.mts +1 -1
  48. package/dist/cli/roadmap/backend-adapter.d.mts +43 -0
  49. package/dist/cli/roadmap/backend-adapter.d.mts.map +1 -0
  50. package/dist/cli/roadmap/backend-adapter.mjs +222 -0
  51. package/dist/cli/roadmap/backend-adapter.mjs.map +1 -0
  52. package/dist/cli/roadmap/lifecycle.d.mts +1 -1
  53. package/dist/cli/roadmap/rollup.d.mts +2 -2
  54. package/dist/cli/util/runtime-capabilities.d.mts +7 -0
  55. package/dist/cli/util/runtime-capabilities.d.mts.map +1 -1
  56. package/dist/cli/util/runtime-capabilities.mjs +65 -0
  57. package/dist/cli/util/runtime-capabilities.mjs.map +1 -1
  58. package/dist/cli/verify-proof/manifest-validator.mjs +13 -0
  59. package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -1
  60. package/dist/cli/verify-proof/resolver.d.mts +10 -7
  61. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  62. package/dist/cli/verify-proof/resolver.mjs +28 -7
  63. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  64. package/dist/cli/verify-proof/sandbox/apply.d.mts +64 -0
  65. package/dist/cli/verify-proof/sandbox/apply.d.mts.map +1 -0
  66. package/dist/cli/verify-proof/sandbox/apply.mjs +379 -0
  67. package/dist/cli/verify-proof/sandbox/apply.mjs.map +1 -0
  68. package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts +63 -0
  69. package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts.map +1 -0
  70. package/dist/cli/verify-proof/sandbox/gate-predicate.mjs +115 -0
  71. package/dist/cli/verify-proof/sandbox/gate-predicate.mjs.map +1 -0
  72. package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts +57 -0
  73. package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts.map +1 -0
  74. package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs +124 -0
  75. package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs.map +1 -0
  76. package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts +28 -0
  77. package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts.map +1 -0
  78. package/dist/cli/verify-proof/sandbox/recipe-diff.mjs +190 -0
  79. package/dist/cli/verify-proof/sandbox/recipe-diff.mjs.map +1 -0
  80. package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts +25 -0
  81. package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts.map +1 -0
  82. package/dist/cli/verify-proof/sandbox/sandbox-config.mjs +85 -0
  83. package/dist/cli/verify-proof/sandbox/sandbox-config.mjs.map +1 -0
  84. package/dist/cli/verify-proof/sandbox/self-test.d.mts +39 -0
  85. package/dist/cli/verify-proof/sandbox/self-test.d.mts.map +1 -0
  86. package/dist/cli/verify-proof/sandbox/self-test.mjs +248 -0
  87. package/dist/cli/verify-proof/sandbox/self-test.mjs.map +1 -0
  88. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  89. package/dist/plugin/agents/apt-planner.md +15 -0
  90. package/dist/plugin/agents/apt-verifier.md +15 -10
  91. package/dist/plugin/skills/apt-plan/SKILL.md +23 -0
  92. package/dist/plugin/skills/apt-run/SKILL.md +1 -1
  93. package/dist/plugin/skills/apt-setup/SKILL.md +56 -0
  94. package/dist/plugin/skills/apt-verify/SKILL.md +20 -15
  95. package/dist/plugin/skills/apt-verify-proof/SKILL.md +146 -11
  96. package/dist/proof-report.d.ts.map +1 -1
  97. package/dist/proof-report.js +4 -1
  98. package/dist/proof-report.js.map +1 -1
  99. package/dist/types/config.d.ts +42 -4
  100. package/dist/types/config.d.ts.map +1 -1
  101. package/dist/types/qa-scoring.d.ts +45 -9
  102. package/dist/types/qa-scoring.d.ts.map +1 -1
  103. package/dist/types/qa-scoring.js +34 -13
  104. package/dist/types/qa-scoring.js.map +1 -1
  105. package/dist/types/state.d.ts +7 -2
  106. package/dist/types/state.d.ts.map +1 -1
  107. package/dist/types/task-record.d.ts +10 -2
  108. package/dist/types/task-record.d.ts.map +1 -1
  109. package/drivers/mobile/README.md +40 -0
  110. package/drivers/mobile/driver.mjs +106 -0
  111. package/drivers/mobile/manifest.json +49 -0
  112. package/package.json +138 -138
  113. package/prompts/inbox-clarification.md +11 -0
  114. package/prompts/inbox-triage.md +20 -0
  115. package/prompts/qa_orchestrator_agentic.md +18 -17
  116. package/prompts/qa_reviewer.md +17 -15
  117. package/skills/apt-plan/SKILL.md +23 -0
  118. package/skills/apt-run/SKILL.md +1 -1
  119. package/skills/apt-setup/SKILL.md +56 -0
  120. package/skills/apt-verifier.md +7 -4
  121. package/skills/apt-verify/SKILL.md +20 -15
  122. package/skills/apt-verify-proof/SKILL.md +146 -11
  123. package/src/cli/commands/apply-recipe.mjs +105 -0
  124. package/src/cli/commands/mobile-prepare.mjs +151 -0
  125. package/src/cli/commands/recipe-diff.mjs +81 -0
  126. package/src/cli/commands/roadmap.mjs +54 -0
  127. package/src/cli/commands/verify-sandbox.mjs +231 -0
  128. package/src/cli/consistency/parse-qa.mjs +20 -4
  129. package/src/cli/consistency/rules/r5-verdict-consistency.mjs +11 -13
  130. package/src/cli/coverage-check/user-outcomes.mjs +52 -17
  131. package/src/cli/dispatch.mjs +27 -0
  132. package/src/cli/gate/gates/verify-approved.mjs +22 -81
  133. package/src/cli/install/mcp-server-specs.mjs +24 -4
  134. package/src/cli/roadmap/backend-adapter.mjs +231 -0
  135. package/src/cli/util/runtime-capabilities.mjs +67 -0
  136. package/src/cli/verify-proof/manifest-validator.mjs +15 -0
  137. package/src/cli/verify-proof/resolver.mjs +27 -7
  138. package/src/cli/verify-proof/sandbox/apply.mjs +401 -0
  139. package/src/cli/verify-proof/sandbox/gate-predicate.mjs +126 -0
  140. package/src/cli/verify-proof/sandbox/pattern-matcher.mjs +127 -0
  141. package/src/cli/verify-proof/sandbox/recipe-diff.mjs +208 -0
  142. package/src/cli/verify-proof/sandbox/sandbox-config.mjs +82 -0
  143. package/src/cli/verify-proof/sandbox/sandbox-schema.json +33 -0
  144. package/src/cli/verify-proof/sandbox/self-test.mjs +265 -0
  145. package/templates/config.json +10 -1
  146. package/templates/proof-verification.md +27 -8
  147. package/workflows/verify-proof.md +376 -302
@@ -646,13 +646,16 @@ function renderMarkdown(md) {
646
646
  }
647
647
 
648
648
  function statusBadge(status) {
649
+ // v0.9.0 — verdict enum is {approved, rejected, needs_human}.
650
+ // `approved-with-notes` is DELETED (no shim). `needs_human` is amber.
649
651
  const colors = {
650
652
  pass: '#22c55e',
651
653
  approved: '#22c55e',
652
654
  fail: '#ef4444',
653
655
  rejected: '#ef4444',
654
656
  partial: '#f59e0b',
655
- 'approved-with-notes': '#f59e0b',
657
+ needs_human: '#f59e0b',
658
+ needs_human_with_transport: '#f59e0b',
656
659
  skip: '#6b7280',
657
660
  error: '#ef4444',
658
661
  }
@@ -810,7 +813,7 @@ function renderVideoCard(filename, mimeType, analysisData, index) {
810
813
  </div>`
811
814
  }
812
815
 
813
- function generateReportHtml(opts) {
816
+ export function generateReportHtml(opts) {
814
817
  const {
815
818
  title,
816
819
  runDate,
@@ -950,29 +953,41 @@ function generateReportHtml(opts) {
950
953
  </div>`
951
954
  }
952
955
 
953
- const featuresHtml =
954
- v?.features?.length > 0
956
+ // v0.9.0 — outcome-rooted rendering. One card per outcome with
957
+ // `data-outcome-id` + `data-surface` attributes on the root <div>
958
+ // (load-bearing for AC8). Parent prose + indented inferred children
959
+ // + per-child evidence rendered inside.
960
+ const outcomeResultColors = {
961
+ pass: '#22c55e',
962
+ fail: '#ef4444',
963
+ blocked: '#f59e0b',
964
+ partial: '#f59e0b',
965
+ needs_human_with_transport: '#f59e0b',
966
+ }
967
+ const outcomesSource = Array.isArray(v?.outcomes) && v.outcomes.length > 0 ? v.outcomes : []
968
+ const outcomesHtml =
969
+ outcomesSource.length > 0
955
970
  ? `<div class="section">
956
- <h2>Feature Verification</h2>
957
- ${v.features
958
- .map((f) => {
959
- const featureResultColors = {
960
- pass: '#22c55e',
961
- fail: '#ef4444',
962
- blocked: '#f59e0b',
963
- partial: '#f59e0b',
964
- }
965
- const fColor = featureResultColors[f.result] ?? '#6b7280'
966
- const passCount = f.tests.filter((t) => t.result === 'pass').length
967
- const totalCount = f.tests.length
968
- return `<div class="card" style="border-left:3px solid ${fColor};">
971
+ <h2>Outcome Verification</h2>
972
+ ${outcomesSource
973
+ .map((o) => {
974
+ const fColor = outcomeResultColors[o.result] ?? '#6b7280'
975
+ const passCount = o.tests.filter((t) => t.result === 'pass').length
976
+ const totalCount = o.tests.length
977
+ const heading = o.prose ? `${o.outcome_id}: ${o.prose}` : (o.outcome_id ?? '')
978
+ return `<div class="card" data-outcome-id="${escapeHtml(o.outcome_id ?? '')}" data-surface="${escapeHtml(o.surface ?? '')}" style="border-left:3px solid ${fColor};">
969
979
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
970
- <h3 style="font-size:15px;color:#f1f5f9;margin:0;">${escapeHtml(f.name)}</h3>
971
- ${statusBadge(f.result ?? 'pending')}
980
+ <h3 style="font-size:15px;color:#f1f5f9;margin:0;">${escapeHtml(heading)}</h3>
981
+ ${o.surface ? `<span style="font-size:11px;color:#0B0B0F;background:#D6D876;padding:2px 8px;border-radius:8px;font-weight:600;">${escapeHtml(o.surface)}</span>` : ''}
982
+ ${statusBadge(o.result ?? 'pending')}
972
983
  <span style="font-size:12px;color:#64748b;margin-left:auto;">${passCount}/${totalCount} tests</span>
973
984
  </div>
974
- ${f.route ? `<div style="font-size:11px;color:#64748b;margin-bottom:8px;">Route: <code style="color:#D6D876;">${escapeHtml(f.route)}</code></div>` : ''}
975
- ${f.tests.map((t) => renderTestRow(t, f.id)).join('')}
985
+ ${
986
+ o.inferred && o.inferred.length > 0
987
+ ? `<details style="margin-bottom:8px;"><summary style="font-size:12px;color:#94a3b8;cursor:pointer;">Inferred sub-checks (${o.inferred.length})</summary><ul style="margin:6px 0 0 20px;font-size:12px;color:#cbd5e1;list-style:disc;">${o.inferred.map((s) => `<li style="margin:2px 0;">${escapeHtml(s)}</li>`).join('')}</ul></details>`
988
+ : ''
989
+ }
990
+ ${o.tests.map((t) => renderTestRow(t, o.outcome_id)).join('')}
976
991
  </div>`
977
992
  })
978
993
  .join('')}
@@ -1207,9 +1222,17 @@ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') close
1207
1222
  <h1>${escapeHtml(title)}</h1>
1208
1223
  <div class="meta">Generated: ${escapeHtml(runDate)}</div>
1209
1224
  <div class="hero-status">${statusBadge(overallVerdict)}</div>
1225
+ ${
1226
+ v?.gates
1227
+ ? `<div style="display:flex;justify-content:center;gap:12px;margin-top:8px;">
1228
+ <span style="font-size:11px;color:#94a3b8;">automation_gate:</span>${statusBadge(v.gates.automation_gate ?? 'pending')}
1229
+ <span style="font-size:11px;color:#94a3b8;">proof_gate:</span>${statusBadge(v.gates.proof_gate ?? 'pending')}
1230
+ </div>`
1231
+ : ''
1232
+ }
1210
1233
  <div style="margin-top:12px;">
1211
1234
  <div style="display:flex;justify-content:center;gap:32px;">
1212
- <div class="stat"><div class="stat-value">${v?.features?.length ?? screenshots.length}</div><div class="stat-label">Features</div></div>
1235
+ <div class="stat"><div class="stat-value">${v?.outcomes?.length ?? v?.features?.length ?? screenshots.length}</div><div class="stat-label">${v?.outcomes?.length ? 'Outcomes' : 'Features'}</div></div>
1213
1236
  <div class="stat"><div class="stat-value">${passedTests}</div><div class="stat-label">Passed</div></div>
1214
1237
  <div class="stat"><div class="stat-value">${failedTests}</div><div class="stat-label">Failed</div></div>
1215
1238
  <div class="stat"><div class="stat-value">${blockedTests}</div><div class="stat-label">Blocked</div></div>
@@ -1224,7 +1247,7 @@ ${summaryHtml}
1224
1247
  ${automatedChecksHtml}
1225
1248
  ${coverageHtml}
1226
1249
  ${dimensionsHtml}
1227
- ${featuresHtml}
1250
+ ${outcomesHtml}
1228
1251
  ${gapsHtml}
1229
1252
  ${skippedHtml}
1230
1253
  ${videosHtml}
@@ -1280,10 +1303,96 @@ function loadVerificationData(proofDir) {
1280
1303
  return null
1281
1304
  }
1282
1305
 
1306
+ /**
1307
+ * Closed-set validator for `automated_checks.{tests,typecheck,lint}.status`.
1308
+ *
1309
+ * v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) — `status` MUST be in
1310
+ * `{pass, fail}`. Anything else (e.g. the legacy "enforced_at_commit"
1311
+ * dodge string) is coerced to `'fail'` so downstream gate derivation
1312
+ * surfaces the issue. Returns the canonical lowercase value.
1313
+ *
1314
+ * Exported for testing.
1315
+ */
1316
+ export function normalizeAutomatedCheckStatus(status) {
1317
+ if (status === 'pass' || status === 'fail') return status
1318
+ return 'fail'
1319
+ }
1320
+
1321
+ /**
1322
+ * Closed set of UI surfaces (electron, web, electron+web). Used by
1323
+ * `deriveProofGate` to detect the fail-closed branch: a UI-surface test
1324
+ * carrying `evidence: "file-evidence: ..."` is REJECTED as proof and
1325
+ * its effective result is forced to `needs_human_with_transport`.
1326
+ */
1327
+ const UI_SURFACES = new Set(['electron', 'web', 'electron+web'])
1328
+
1329
+ /**
1330
+ * Compute the effective `result` for a single test entry per AC9.
1331
+ *
1332
+ * - If `surface` is a UI surface AND `evidence` matches `^file-evidence:`,
1333
+ * the effective result is `needs_human_with_transport` regardless of
1334
+ * the written `result`.
1335
+ * - Otherwise, the written `result` is honored as-is.
1336
+ *
1337
+ * Exported for testing.
1338
+ */
1339
+ export function effectiveTestResult(test) {
1340
+ if (!test || typeof test !== 'object') return 'pending'
1341
+ const surface = typeof test.surface === 'string' ? test.surface : ''
1342
+ const evidence = typeof test.evidence === 'string' ? test.evidence : ''
1343
+ if (UI_SURFACES.has(surface) && /^file-evidence:/i.test(evidence)) {
1344
+ return 'needs_human_with_transport'
1345
+ }
1346
+ return test.result ?? 'pending'
1347
+ }
1348
+
1349
+ /**
1350
+ * Derive `proof_gate` (`pass | fail`) from the per-test evidence story.
1351
+ *
1352
+ * v0.9.0 — `proof_gate = fail` iff ANY test's effective result is `fail`.
1353
+ * A `needs_human_with_transport` result does NOT fail the proof_gate on
1354
+ * its own — it bubbles up to the overall verdict as `needs_human` via
1355
+ * `deriveVerdict`. Missing surface evidence on a UI outcome (caught by
1356
+ * `effectiveTestResult`) becomes `needs_human_with_transport`, not `fail`.
1357
+ *
1358
+ * Exported for testing.
1359
+ */
1360
+ export function deriveProofGate(verification) {
1361
+ const tests = Array.isArray(verification?.tests) ? verification.tests : []
1362
+ if (tests.length === 0) return 'fail'
1363
+ for (const t of tests) {
1364
+ if (effectiveTestResult(t) === 'fail') return 'fail'
1365
+ }
1366
+ return 'pass'
1367
+ }
1368
+
1369
+ /**
1370
+ * Derive `automation_gate` (`pass | fail`) from the three automated checks.
1371
+ *
1372
+ * v0.9.0 — `automation_gate = pass` iff ALL THREE of `tests`, `typecheck`,
1373
+ * `lint` are `pass`. Missing checks count as `fail`. Per-check status is
1374
+ * normalized via `normalizeAutomatedCheckStatus` first so the legacy
1375
+ * `enforced_at_commit` string surfaces as `fail` rather than slipping
1376
+ * through.
1377
+ *
1378
+ * Exported for testing.
1379
+ */
1380
+ export function deriveAutomationGate(automatedChecks) {
1381
+ if (!automatedChecks || typeof automatedChecks !== 'object') return 'fail'
1382
+ const statuses = ['tests', 'typecheck', 'lint'].map((key) => {
1383
+ const entry = automatedChecks[key]
1384
+ if (!entry || typeof entry !== 'object') return 'fail'
1385
+ return normalizeAutomatedCheckStatus(entry.status)
1386
+ })
1387
+ return statuses.every((s) => s === 'pass') ? 'pass' : 'fail'
1388
+ }
1389
+
1283
1390
  /**
1284
1391
  * Map verification.json schema to the internal shape used by generateReportHtml().
1392
+ *
1393
+ * Exported for testing (verify-proof-renderer-outcome-grouping.test.ts).
1285
1394
  */
1286
- function mapVerificationJson(json) {
1395
+ export function mapVerificationJson(json) {
1287
1396
  const meta = json.meta ?? {}
1288
1397
  const checks = json.automated_checks ?? {}
1289
1398
  const tests = json.tests ?? []
@@ -1292,39 +1401,73 @@ function mapVerificationJson(json) {
1292
1401
  const skipped = json.skipped ?? []
1293
1402
  const coverage = json.coverage ?? null
1294
1403
 
1295
- // Map automated checks
1404
+ // Map automated checks. v0.9.0 (verify-proof-v2-outcome-walker_27-05-26)
1405
+ // — `status` MUST be in the closed set {pass, fail}. The legacy
1406
+ // `enforced_at_commit` dodge string is no longer valid; if the input
1407
+ // carries it (or any other non-{pass,fail} value), the per-check status
1408
+ // becomes `fail` so the proof-gate derivation surfaces the issue.
1296
1409
  const automatedChecks = {
1297
1410
  tests: checks.tests
1298
1411
  ? {
1299
- status: checks.tests.status,
1412
+ status: normalizeAutomatedCheckStatus(checks.tests.status),
1300
1413
  output: checks.tests.output ?? '',
1301
1414
  count: checks.tests.count,
1302
1415
  }
1303
1416
  : null,
1304
1417
  typecheck: checks.typecheck
1305
- ? { status: checks.typecheck.status, output: checks.typecheck.output ?? '' }
1418
+ ? {
1419
+ status: normalizeAutomatedCheckStatus(checks.typecheck.status),
1420
+ output: checks.typecheck.output ?? '',
1421
+ }
1306
1422
  : null,
1307
- lint: checks.lint ? { status: checks.lint.status, output: checks.lint.output ?? '' } : null,
1423
+ lint: checks.lint
1424
+ ? {
1425
+ status: normalizeAutomatedCheckStatus(checks.lint.status),
1426
+ output: checks.lint.output ?? '',
1427
+ }
1428
+ : null,
1429
+ }
1430
+
1431
+ // v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) — group tests by
1432
+ // `outcome_id` instead of the legacy `registry_id.split('-')[0]` prefix.
1433
+ // `meta.outcomes[]` (when present) provides the parent prose + inferred
1434
+ // children for each card. Per-test mapping preserves `outcome_id`,
1435
+ // `surface`, and `transport` so the renderer can render them.
1436
+ const metaOutcomes = Array.isArray(meta.outcomes) ? meta.outcomes : []
1437
+ const outcomeMeta = new Map()
1438
+ for (const o of metaOutcomes) {
1439
+ if (o && typeof o === 'object' && typeof o.id === 'string') {
1440
+ outcomeMeta.set(o.id, o)
1441
+ }
1308
1442
  }
1309
1443
 
1310
- // Group tests into features by registry_id prefix or as a single group
1311
- const featureGroups = new Map()
1444
+ const outcomeGroups = new Map()
1312
1445
  for (const t of tests) {
1313
- const groupKey = t.registry_id ? t.registry_id.split('-').slice(0, 1).join('-') : 'all'
1314
- if (!featureGroups.has(groupKey)) {
1315
- featureGroups.set(groupKey, {
1316
- name: groupKey,
1317
- id: groupKey,
1318
- route: '',
1319
- mcp: '',
1446
+ const groupKey = t.outcome_id ?? 'unassigned'
1447
+ if (!outcomeGroups.has(groupKey)) {
1448
+ const m = outcomeMeta.get(groupKey)
1449
+ outcomeGroups.set(groupKey, {
1450
+ outcome_id: groupKey,
1451
+ surface: m?.surface ?? t.surface ?? '',
1452
+ prose: typeof m?.text === 'string' ? m.text : '',
1453
+ inferred: Array.isArray(m?.inferred) ? m.inferred.slice() : [],
1320
1454
  result: '',
1321
1455
  tests: [],
1322
1456
  })
1323
1457
  }
1324
- featureGroups.get(groupKey).tests.push({
1458
+ const group = outcomeGroups.get(groupKey)
1459
+ // First test on the group establishes the surface if meta didn't.
1460
+ if (!group.surface && typeof t.surface === 'string') {
1461
+ group.surface = t.surface
1462
+ }
1463
+ group.tests.push({
1325
1464
  name: t.name ?? t.id,
1326
1465
  id: t.id,
1327
1466
  registry_id: t.registry_id,
1467
+ outcome_id: t.outcome_id ?? null,
1468
+ surface: t.surface ?? null,
1469
+ transport: t.transport ?? null,
1470
+ inferred_index: typeof t.inferred_index === 'number' ? t.inferred_index : null,
1328
1471
  precondition: t.precondition ?? '',
1329
1472
  action: t.action ?? '',
1330
1473
  expected: t.expected ?? '',
@@ -1337,19 +1480,31 @@ function mapVerificationJson(json) {
1337
1480
  })
1338
1481
  }
1339
1482
 
1340
- // Derive feature-level results
1341
- const features = []
1342
- for (const [, f] of featureGroups) {
1343
- const results = f.tests.map((t) => t.result)
1344
- if (results.every((r) => r === 'pass')) f.result = 'pass'
1345
- else if (results.some((r) => r === 'fail')) f.result = 'fail'
1346
- else if (results.some((r) => r === 'blocked')) f.result = 'partial'
1347
- else f.result = 'partial'
1348
- // Capitalize feature name
1349
- f.name = f.name.charAt(0).toUpperCase() + f.name.slice(1) + ' Features'
1350
- features.push(f)
1483
+ // Derive per-outcome result roll-up.
1484
+ const outcomes = []
1485
+ for (const [, o] of outcomeGroups) {
1486
+ const results = o.tests.map((t) => t.result)
1487
+ if (results.some((r) => r === 'fail')) o.result = 'fail'
1488
+ else if (results.some((r) => r === 'needs_human_with_transport'))
1489
+ o.result = 'needs_human_with_transport'
1490
+ else if (results.every((r) => r === 'pass')) o.result = 'pass'
1491
+ else if (results.some((r) => r === 'blocked')) o.result = 'partial'
1492
+ else o.result = 'partial'
1493
+ outcomes.push(o)
1351
1494
  }
1352
1495
 
1496
+ // Legacy `features` array — kept as a deprecated alias for one release
1497
+ // per CHANGELOG. Same payload, but keyed `name`/`id` for the legacy
1498
+ // HTML renderer until ST8's rewrite lands.
1499
+ const features = outcomes.map((o) => ({
1500
+ name: o.outcome_id,
1501
+ id: o.outcome_id,
1502
+ route: '',
1503
+ mcp: '',
1504
+ result: o.result,
1505
+ tests: o.tests,
1506
+ }))
1507
+
1353
1508
  // Map dimensions
1354
1509
  const dimensions = []
1355
1510
  for (const [key, val] of Object.entries(dims)) {
@@ -1373,6 +1528,22 @@ function mapVerificationJson(json) {
1373
1528
  fix_plan: g.fix_plan ?? null,
1374
1529
  }))
1375
1530
 
1531
+ // v0.9.0 — propagate the two-gate envelope to the renderer. Also
1532
+ // derive automation_gate from automated_checks as a fallback when the
1533
+ // input doesn't carry it explicitly (forward-compat for older
1534
+ // verification.json that pre-dates the schema extension).
1535
+ const gatesInput = json.gates && typeof json.gates === 'object' ? json.gates : {}
1536
+ const gates = {
1537
+ automation_gate:
1538
+ gatesInput.automation_gate === 'pass' || gatesInput.automation_gate === 'fail'
1539
+ ? gatesInput.automation_gate
1540
+ : deriveAutomationGate(automatedChecks),
1541
+ proof_gate:
1542
+ gatesInput.proof_gate === 'pass' || gatesInput.proof_gate === 'fail'
1543
+ ? gatesInput.proof_gate
1544
+ : deriveProofGate({ tests }),
1545
+ }
1546
+
1376
1547
  return {
1377
1548
  frontmatter: {
1378
1549
  run_id: meta.run_id,
@@ -1387,6 +1558,8 @@ function mapVerificationJson(json) {
1387
1558
  verdictText: meta.verdict ?? '',
1388
1559
  },
1389
1560
  automatedChecks,
1561
+ gates,
1562
+ outcomes,
1390
1563
  features,
1391
1564
  dimensions,
1392
1565
  gaps: mappedGaps,
@@ -1876,6 +2049,9 @@ async function cmdReport(args) {
1876
2049
  }
1877
2050
  }
1878
2051
 
2052
+ // v0.9.0 — emit `outcomesCount` (the new field); `featuresCount` is
2053
+ // retained for one release as a deprecated sentinel (always `0`) so
2054
+ // downstream consumers don't crash on missing key. See CHANGELOG.
1879
2055
  output({
1880
2056
  status: 'ok',
1881
2057
  report: {
@@ -1884,7 +2060,8 @@ async function cmdReport(args) {
1884
2060
  screenshotCount: scanned.images.length,
1885
2061
  videoCount: scanned.videos.length,
1886
2062
  verdictCount: verdicts.length,
1887
- featuresCount: verification?.features?.length ?? 0,
2063
+ outcomesCount: verification?.outcomes?.length ?? 0,
2064
+ featuresCount: 0,
1888
2065
  gapsCount: verification?.gaps?.length ?? 0,
1889
2066
  mode: isArchive ? 'archive' : 'relative',
1890
2067
  title,
@@ -2044,35 +2221,38 @@ All output is JSON to stdout. Progress goes to stderr.`
2044
2221
  }
2045
2222
 
2046
2223
  // ---------------------------------------------------------------------------
2047
- // Dispatch
2224
+ // Dispatch (only runs when invoked as the CLI entry point, not when imported)
2048
2225
  // ---------------------------------------------------------------------------
2049
2226
 
2050
- const args = process.argv.slice(2)
2051
- const command = args[0]
2227
+ const isCliEntry = process.argv[1] === __filename
2228
+ if (isCliEntry) {
2229
+ const args = process.argv.slice(2)
2230
+ const command = args[0]
2052
2231
 
2053
- if (!command || command === '--help' || command === '-h') {
2054
- printHelp()
2055
- process.exit(0)
2056
- }
2232
+ if (!command || command === '--help' || command === '-h') {
2233
+ printHelp()
2234
+ process.exit(0)
2235
+ }
2057
2236
 
2058
- if (command === '--version' || command === '-v') {
2059
- process.stdout.write(getVersion() + '\n')
2060
- process.exit(0)
2061
- }
2237
+ if (command === '--version' || command === '-v') {
2238
+ process.stdout.write(getVersion() + '\n')
2239
+ process.exit(0)
2240
+ }
2062
2241
 
2063
- switch (command) {
2064
- case 'analyze':
2065
- await cmdAnalyze(args.slice(1))
2066
- break
2067
- case 'capture':
2068
- await cmdCapture(args.slice(1))
2069
- break
2070
- case 'report':
2071
- await cmdReport(args.slice(1))
2072
- break
2073
- case 'models':
2074
- await cmdModels()
2075
- break
2076
- default:
2077
- fail(`Unknown command: ${command}. Run apt-proof-video --help for usage.`)
2242
+ switch (command) {
2243
+ case 'analyze':
2244
+ await cmdAnalyze(args.slice(1))
2245
+ break
2246
+ case 'capture':
2247
+ await cmdCapture(args.slice(1))
2248
+ break
2249
+ case 'report':
2250
+ await cmdReport(args.slice(1))
2251
+ break
2252
+ case 'models':
2253
+ await cmdModels()
2254
+ break
2255
+ default:
2256
+ fail(`Unknown command: ${command}. Run apt-proof-video --help for usage.`)
2257
+ }
2078
2258
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @param {string} projectDir
3
+ * @param {string[]} [extra]
4
+ */
5
+ export function cmdApplyRecipe(projectDir: string, extra?: string[]): Promise<import("../util/result.mjs").CommandResult>;
6
+ //# sourceMappingURL=apply-recipe.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-recipe.d.mts","sourceRoot":"","sources":["../../../src/cli/commands/apply-recipe.mjs"],"names":[],"mappings":"AA+BA;;;GAGG;AACH,2CAHW,MAAM,UACN,MAAM,EAAE,uDAuElB"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * commands/apply-recipe.mjs — `apt-tools apply-recipe <projectDir> [--confirm] [--accept-all] [--force]`.
3
+ *
4
+ * Wraps applyRecipe() at the CLI surface.
5
+ *
6
+ * --confirm switches from dry-run (the default) to live writes.
7
+ * --accept-all bypasses per-change confirmation (pipeline/scripted use).
8
+ * --force overrides the TOFU user_modified guard — overwrites files
9
+ * whose SHA diverges from the recipe's expected before value.
10
+ * Files containing the recipe's marker are still skipped.
11
+ *
12
+ * The two flags compose: `--confirm` alone enters live mode but defers
13
+ * per-change accept/skip to the caller — without `--accept-all` and
14
+ * without a stdin-driven prompt, every change is SKIPPED (fail-safe per
15
+ * ID-02). Adopters using the interactive apt-setup SKILL.md mini-flow
16
+ * walk each change one at a time before invoking the CLI with
17
+ * `--accept-all` once they've reviewed the diff. Adopters in CI /
18
+ * fan-out / scripted contexts pass both flags up front.
19
+ *
20
+ * Exit codes:
21
+ * 0 — applyRecipe completed (dry-run or confirmed)
22
+ * 1 — usage error / no-recipe-match / applyRecipe threw
23
+ */
24
+ import { parseFlags } from '../util/args.mjs';
25
+ import { err, exitWith, ok } from '../util/result.mjs';
26
+ import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs';
27
+ import { applyRecipe } from '../verify-proof/sandbox/apply.mjs';
28
+ import { matchPatternForProject } from '../verify-proof/sandbox/pattern-matcher.mjs';
29
+ import { discoverBundledRecipes } from '../verify-proof/sandbox/sandbox-config.mjs';
30
+ /**
31
+ * @param {string} projectDir
32
+ * @param {string[]} [extra]
33
+ */
34
+ export async function cmdApplyRecipe(projectDir, extra = []) {
35
+ if (!projectDir) {
36
+ return err('Usage: apt-tools apply-recipe <project-dir> [--confirm] [--accept-all]');
37
+ }
38
+ const flags = parseFlags(extra);
39
+ const confirm = flags.has('confirm');
40
+ const acceptAll = flags.has('accept-all');
41
+ const force = flags.has('force');
42
+ const recipes = discoverBundledRecipes();
43
+ const runtime = loadRuntimeCapabilities(projectDir);
44
+ const explicitRecipeId = flags.get('recipe') ?? null;
45
+ let recipeId;
46
+ let matchReason;
47
+ if (explicitRecipeId) {
48
+ recipeId = explicitRecipeId;
49
+ matchReason = 'explicit';
50
+ }
51
+ else {
52
+ const match = matchPatternForProject(projectDir, runtime, recipes);
53
+ recipeId = match.recipeId;
54
+ matchReason = match.reason;
55
+ }
56
+ if (!recipeId) {
57
+ return exitWith({
58
+ status: 'error',
59
+ command: 'apply-recipe',
60
+ code: 'E_NO_RECIPE',
61
+ reason: matchReason,
62
+ runtime,
63
+ docs: 'docs/verify-proof-sandbox-patterns.md',
64
+ }, 1);
65
+ }
66
+ try {
67
+ const result = await applyRecipe({
68
+ projectDir,
69
+ recipeId,
70
+ recipes,
71
+ confirm,
72
+ // AC8 + ID-02 — only auto-accept when the caller passed
73
+ // --accept-all explicitly. Without the flag the underlying
74
+ // applyRecipe defaults to skip (every change recorded as
75
+ // `user_declined`); callers driving per-change UX wrap the
76
+ // applier directly with their own confirmer.
77
+ perChangeConfirm: acceptAll ? async () => 'accept' : undefined,
78
+ force,
79
+ });
80
+ return ok({
81
+ status: 'ok',
82
+ command: 'apply-recipe',
83
+ project_dir: projectDir,
84
+ recipe_id: recipeId,
85
+ recipe_version: result.recipe_version,
86
+ confirm,
87
+ accept_all: acceptAll,
88
+ aborted: result.aborted,
89
+ writes: result.writes,
90
+ skips: result.skips,
91
+ audit_path: result.audit_path,
92
+ });
93
+ }
94
+ catch (e) {
95
+ return err(`apply-recipe: ${e?.message ?? String(e)}`);
96
+ }
97
+ }
98
+ //# sourceMappingURL=apply-recipe.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-recipe.mjs","sourceRoot":"","sources":["../../../src/cli/commands/apply-recipe.mjs"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @typedef {{ binary: string, args: string[] }} LocalCommand
3
+ */
4
+ /**
5
+ * Resolve the ordered launch sequence for a (platform, mode, device).
6
+ *
7
+ * - mode 'expo' — `npx expo run:ios|run:android` builds, installs, launches,
8
+ * and starts Metro in one step (the common Expo/RN path).
9
+ * - mode 'native' — device-prep only (`xcrun simctl boot` / `adb
10
+ * wait-for-device`); the native build+install+launch is delegated (gradle /
11
+ * the XcodeBuildMCP escalation per D-02), out of scope for v1's planner.
12
+ *
13
+ * @param {{ platform?: string, mode?: string, device?: string }} [opts]
14
+ * @returns {LocalCommand[]}
15
+ */
16
+ export function planLaunchSequence(opts?: {
17
+ platform?: string;
18
+ mode?: string;
19
+ device?: string;
20
+ }): LocalCommand[];
21
+ /**
22
+ * Launch-readiness predicate (D-04). A successful screenshot alone is NOT
23
+ * enough — for RN/Expo it can capture a native splash, an Expo loading
24
+ * overlay, a redbox, or the dev-client launcher. ALL signals must hold before
25
+ * UI driving starts. For a pure-native app (no JS bundle) the caller passes
26
+ * `bundleConnected: true` and `launcherState: false`.
27
+ *
28
+ * @param {{ foregrounded?: boolean, bundleConnected?: boolean, redbox?: boolean, launcherState?: boolean }} [signals]
29
+ * @returns {boolean}
30
+ */
31
+ export function isAppReady(signals?: {
32
+ foregrounded?: boolean;
33
+ bundleConnected?: boolean;
34
+ redbox?: boolean;
35
+ launcherState?: boolean;
36
+ }): boolean;
37
+ /**
38
+ * `apt-tools mobile prepare <project-dir> --platform <ios|android>
39
+ * [--mode <expo|native>] [--device <id>]`
40
+ *
41
+ * @param {string} projectDir
42
+ * @param {string[]} args
43
+ * @returns {{ envelope: object, exitCode: number }}
44
+ */
45
+ export function cmdMobilePrepare(projectDir: string, args: string[]): {
46
+ envelope: object;
47
+ exitCode: number;
48
+ };
49
+ export type LocalCommand = {
50
+ binary: string;
51
+ args: string[];
52
+ };
53
+ //# sourceMappingURL=mobile-prepare.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-prepare.d.mts","sourceRoot":"","sources":["../../../src/cli/commands/mobile-prepare.mjs"],"names":[],"mappings":"AAoBA;;GAEG;AAEH;;;;;;;;;;;GAWG;AACH,0CAHW;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,YAAY,EAAE,CAsB1B;AAED;;;;;;;;;GASG;AACH,qCAHW;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,OAAO,CAUnB;AA+BD;;;;;;;GAOG;AACH,6CAJW,MAAM,QACN,MAAM,EAAE,GACN;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAqClD;2BAjIY;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE"}