sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -12,9 +12,11 @@ import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-disc
12
12
  import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
13
13
  import { registerImageArtifact } from '../image/image-artifact-registry.js';
14
14
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
15
+ export const DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS = 180000;
16
+ const responseDeadlines = new WeakMap();
15
17
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
16
18
  return [
17
- 'Review this UI screenshot as a senior product design lead.',
19
+ 'Review this UI screenshot as a senior Toss UI/UX product designer.',
18
20
  `Screenshot source id: ${sourceScreenId}.`,
19
21
  'Output must be a new image artifact, not prose.',
20
22
  'Text-only response is invalid.',
@@ -322,7 +324,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
322
324
  const responsesOutputSource = codexLbProviderSelected ? CODEX_LB_PROVIDER_OUTPUT_SOURCE : null;
323
325
  const validation = await validateGptImage2Request({
324
326
  provider: 'openai_images_api',
325
- endpoint: effectiveEndpoint,
327
+ endpoint: String(effectiveEndpoint || ''),
326
328
  model: 'gpt-image-2',
327
329
  prompt: input.prompt,
328
330
  source_image_path: sourcePath,
@@ -362,7 +364,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
362
364
  });
363
365
  return { ok: false, status: 'blocked', generated_image_path: null, output_id: null, blocker: 'gpt_image_2_request_validation_failed', provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
364
366
  }
365
- if (!auth.apiKey) {
367
+ if (auth.blocker || !auth.apiKey) {
366
368
  const blocked = {
367
369
  schema: 'sks.image-ux-gpt-image-2-response.v1',
368
370
  created_at: nowIso(),
@@ -373,7 +375,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
373
375
  status: 'blocked',
374
376
  blocker: auth.blocker,
375
377
  setup_guidance: auth.auth_source === 'CODEX_LB_API_KEY'
376
- ? 'CODEX_LB_API_KEY is only a non-Codex API fallback when explicitly enabled; it does not satisfy Codex App $imagegen evidence. Attach a real Codex App $imagegen output image for full SKS verification.'
378
+ ? 'Repair the selected codex-lb credential binding. Completed output from a selected provider can satisfy full evidence; codex-lb used only as a fallback while another provider is selected cannot.'
377
379
  : 'Set OPENAI_API_KEY only for an explicit non-Codex Images API fallback, or attach a real Codex App $imagegen output image for full SKS verification.',
378
380
  local_only: true
379
381
  };
@@ -463,6 +465,8 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
463
465
  output_id: meta.output_id,
464
466
  output_source: responsesOutputSource,
465
467
  image_output_recovered_from_stream: payload?.image_output_recovered_from_stream === true,
468
+ image_output_provenance: payload?.image_output_provenance || 'response.output',
469
+ image_output_partial_frame: false,
466
470
  image_artifact_path_contract: imageContract?.artifact_path || null,
467
471
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
468
472
  latency_ms: Date.now() - started,
@@ -605,6 +609,8 @@ function codexLbTargetInputs(source = {}) {
605
609
  const inputs = {};
606
610
  if (source?.home)
607
611
  inputs.home = source.home;
612
+ if (source?.codexHome)
613
+ inputs.codexHome = source.codexHome;
608
614
  if (source?.env)
609
615
  inputs.env = source.env;
610
616
  if (typeof source?.configText === 'string')
@@ -625,15 +631,22 @@ export function imagegenCapabilityBlocker(surface = 'Codex App $imagegen') {
625
631
  blocker: 'imagegen_capability_missing',
626
632
  surface,
627
633
  model: 'gpt-image-2',
628
- guidance: 'Run the request in Codex App with $imagegen/gpt-image-2 and attach the generated annotated review image path. OPENAI_API_KEY or CODEX_LB_API_KEY may be used only for an explicitly requested non-Codex API fallback, and that fallback does not satisfy Codex App imagegen evidence. SKS must not fabricate or substitute a text-only review.'
634
+ guidance: 'Run the request with gpt-image-2 through Codex App $imagegen or a selected, ready codex-lb provider and bind the completed generated image path. OPENAI_API_KEY, or codex-lb used while another provider is selected, remains non-Codex fallback evidence. SKS must not fabricate or substitute a text-only review.'
629
635
  };
630
636
  }
631
637
  async function resolveImagesApiAuth(opts = {}) {
638
+ const codexLb = opts.codexLb?.available === true ? opts.codexLb : null;
639
+ const target = opts.codexLbTarget || (codexLb
640
+ ? await resolveCodexLbImagegenTarget(codexLbTargetInputs(opts)).catch(() => null)
641
+ : null);
642
+ if (target?.selected === true)
643
+ return codexLbImagesApiAuth(opts, codexLb, target);
632
644
  const openAiKey = String(opts.apiKey || process.env.OPENAI_API_KEY || '').trim();
633
645
  if (openAiKey) {
634
646
  return {
635
647
  apiKey: openAiKey,
636
648
  auth_source: 'OPENAI_API_KEY',
649
+ api_key_source: null,
637
650
  responses_model: responsesImagegenModel(opts),
638
651
  responses_model_source: responsesImagegenModel(opts) ? 'explicit' : null,
639
652
  endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
@@ -641,11 +654,11 @@ async function resolveImagesApiAuth(opts = {}) {
641
654
  blocker: null
642
655
  };
643
656
  }
644
- const codexLb = opts.codexLb?.available === true ? opts.codexLb : null;
645
657
  if (!codexLb) {
646
658
  return {
647
659
  apiKey: null,
648
660
  auth_source: null,
661
+ api_key_source: null,
649
662
  responses_model: responsesImagegenModel(opts),
650
663
  responses_model_source: null,
651
664
  endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
@@ -653,19 +666,26 @@ async function resolveImagesApiAuth(opts = {}) {
653
666
  blocker: 'openai_api_key_missing'
654
667
  };
655
668
  }
656
- const envKey = codexLb.env_key || 'CODEX_LB_API_KEY';
657
- const target = opts.codexLbTarget || await resolveCodexLbImagegenTarget(codexLbTargetInputs(opts)).catch(() => null);
669
+ return codexLbImagesApiAuth(opts, codexLb, target);
670
+ }
671
+ function codexLbImagesApiAuth(opts, codexLb, target) {
672
+ const envKey = codexLb?.env_key || 'CODEX_LB_API_KEY';
658
673
  const codexLbKey = String(opts.codexLbApiKey || target?.api_key || '').trim();
659
- const baseUrl = target?.base_url || codexLb.base_url || opts.baseUrl || '';
674
+ const baseUrl = target?.base_url || codexLb?.base_url || opts.baseUrl || '';
675
+ const blocker = !baseUrl
676
+ ? 'codex_lb_base_url_missing'
677
+ : codexLbKey
678
+ ? null
679
+ : target?.blocker || 'codex_lb_api_key_missing';
660
680
  return {
661
681
  apiKey: codexLbKey || null,
662
682
  auth_source: envKey,
663
683
  api_key_source: target?.api_key_source || null,
664
684
  responses_model: responsesImagegenModel(opts) || target?.model || '',
665
685
  responses_model_source: responsesImagegenModel(opts) ? 'explicit' : target?.model_source || null,
666
- endpoint: imageEditsEndpoint(baseUrl),
667
- responses_endpoint: responsesEndpoint(baseUrl),
668
- blocker: codexLbKey ? null : target?.blocker || 'codex_lb_api_key_missing'
686
+ endpoint: baseUrl ? imageEditsEndpoint(baseUrl) : null,
687
+ responses_endpoint: baseUrl ? responsesEndpoint(baseUrl) : null,
688
+ blocker
669
689
  };
670
690
  }
671
691
  function imageEditsEndpoint(baseUrl = '') {
@@ -677,23 +697,27 @@ function responsesEndpoint(baseUrl = '') {
677
697
  return /\/responses$/i.test(base) ? base : `${base}/responses`;
678
698
  }
679
699
  function responsesImagegenModel(opts = {}) {
680
- return String(opts.responsesModel || process.env.SKS_IMAGEGEN_RESPONSES_MODEL || process.env.OPENAI_MODEL || '').trim();
700
+ return String(opts.responsesModel || process.env.SKS_IMAGEGEN_RESPONSES_MODEL || '').trim();
681
701
  }
682
702
  function imagegenFetchTimeoutMs(opts = {}) {
683
- const value = Number(opts.fetchTimeoutMs || process.env.SKS_IMAGEGEN_FETCH_TIMEOUT_MS || 90000);
684
- return Number.isFinite(value) && value > 0 ? value : 90000;
703
+ const value = Number(opts.fetchTimeoutMs || process.env.SKS_IMAGEGEN_FETCH_TIMEOUT_MS || DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS);
704
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS;
685
705
  }
686
706
  async function fetchWithTimeout(url, init, timeoutMs) {
687
707
  const controller = new AbortController();
708
+ const startedAt = Date.now();
688
709
  const timeout = setTimeout(() => controller.abort(new Error(`imagegen_fetch_timeout_${timeoutMs}ms`)), timeoutMs);
689
710
  try {
690
- return await fetch(url, { ...init, signal: controller.signal });
711
+ const response = await fetch(url, { ...init, signal: controller.signal });
712
+ responseDeadlines.set(response, { controller, startedAt, timeoutMs, timer: timeout });
713
+ return response;
691
714
  }
692
- finally {
715
+ catch (err) {
693
716
  clearTimeout(timeout);
717
+ throw err;
694
718
  }
695
719
  }
696
- async function readResponsePayload(response, timeoutMs = 90000) {
720
+ async function readResponsePayload(response, timeoutMs = DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS) {
697
721
  const text = await textWithTimeout(response, timeoutMs);
698
722
  if (!text)
699
723
  return {};
@@ -708,23 +732,39 @@ async function readResponsePayload(response, timeoutMs = 90000) {
708
732
  }
709
733
  }
710
734
  async function textWithTimeout(response, timeoutMs) {
735
+ const deadline = responseDeadlines.get(response);
736
+ if (deadline)
737
+ clearTimeout(deadline.timer);
738
+ const remainingMs = deadline
739
+ ? Math.max(1, deadline.timeoutMs - (Date.now() - deadline.startedAt))
740
+ : timeoutMs;
711
741
  let timeout = null;
712
742
  try {
713
743
  return await Promise.race([
714
744
  response.text(),
715
745
  new Promise((_, reject) => {
716
- timeout = setTimeout(() => reject(new Error(`imagegen_response_read_timeout_${timeoutMs}ms`)), timeoutMs);
746
+ timeout = setTimeout(() => {
747
+ const error = new Error(`imagegen_response_read_timeout_${timeoutMs}ms`);
748
+ deadline?.controller.abort(error);
749
+ void response.body?.cancel(error).catch(() => { });
750
+ reject(error);
751
+ }, remainingMs);
717
752
  })
718
753
  ]);
719
754
  }
720
755
  finally {
721
756
  if (timeout)
722
757
  clearTimeout(timeout);
758
+ responseDeadlines.delete(response);
723
759
  }
724
760
  }
725
761
  function findResponsesImageGenerationOutput(payload) {
762
+ if (payload?.image_output_partial_frame === true)
763
+ return null;
726
764
  for (const output of Array.isArray(payload?.output) ? payload.output : []) {
727
765
  if (String(output?.type || '') === 'image_generation_call') {
766
+ if (String(output?.status || '') === 'partial')
767
+ continue;
728
768
  const b64 = typeof output?.result === 'string' ? output.result : output?.result?.b64_json || output?.b64_json || null;
729
769
  if (b64)
730
770
  return { b64: String(b64), id: output?.id || payload?.id || null };
@@ -813,10 +853,8 @@ function imagegenRetryOptions(opts = {}) {
813
853
  return {
814
854
  sleep: opts.retrySleep,
815
855
  classifyError: (err) => {
816
- const code = String(err?.code
817
- || err?.name
818
- || err?.message
819
- || '');
856
+ const row = err;
857
+ const code = [row?.code, row?.name, row?.message].filter(Boolean).join(' ');
820
858
  if (/AbortError|timeout|abort/i.test(code))
821
859
  return { code: 'ETIMEDOUT', status: null };
822
860
  if (/ECONNRESET|EAI_AGAIN|ETIMEDOUT|ENOTFOUND|ECONNREFUSED|fetch failed/i.test(code))
@@ -834,6 +872,9 @@ function summarizeImagegenPayload(payload) {
834
872
  model: payload?.model || null,
835
873
  error_type: payload?.error?.type || null,
836
874
  error_code: payload?.error?.code || null,
875
+ image_output_partial_frame: payload?.image_output_partial_frame === true,
876
+ partial_image_output_present: payload?.partial_image_output_present === true,
877
+ image_output_provenance: payload?.image_output_provenance || null,
837
878
  output_count: outputs.length,
838
879
  output: outputs.slice(0, 8).map((output) => ({
839
880
  id: output?.id || null,
@@ -130,6 +130,7 @@ export function buildRealCalloutExtractionPrompt(input) {
130
130
  const source = input.sourceScreenshot || {};
131
131
  return [
132
132
  'Analyze the generated UX review image pixels directly.',
133
+ 'The source screenshot is context only; do not analyze it instead of the generated annotated image.',
133
134
  'Return only visible numbered callouts from the generated image.',
134
135
  'Do not invent issues, requirements, or invisible callouts.',
135
136
  'Return bbox coordinates in the generated image coordinate system as [x,y,width,height].',
@@ -12,7 +12,7 @@ import { buildIssueLedgerFromGeneratedCallouts } from './image-ux-review/callout
12
12
  import { planImageUxFixTasks } from './image-ux-review/fix-task-planner.js';
13
13
  import { runImageUxFixLoop } from './image-ux-review/fix-loop.js';
14
14
  import { buildRecapturePlan } from './image-ux-review/recapture.js';
15
- import { addVisualAnchor, ingestImage } from './wiki-image/image-voxel-ledger.js';
15
+ import { addImageRelation, addVisualAnchor, ingestImage } from './wiki-image/image-voxel-ledger.js';
16
16
  import { validateFinalHonestModeReport } from './artifact-schemas.js';
17
17
  import { imagegenEvidenceClassBlockers, isFullImagegenOutputSource } from './imagegen/imagegen-evidence.js';
18
18
  export const IMAGE_UX_REVIEW_GATE_ARTIFACT = 'image-ux-review-gate.json';
@@ -69,6 +69,35 @@ export function imageUxReviewGateAllowsReferenceCloseout(gate = {}) {
69
69
  && gate?.verified_level === 'verified_partial'
70
70
  && gate?.full_review_passed !== true;
71
71
  }
72
+ export function imageUxReviewCommandOutcome(gate = {}, proofOk = false, opts = {}) {
73
+ const reviewReportCompleted = gate?.review_report_completed === true;
74
+ const reviewOnlyCompletionAllowed = opts.allowReviewOnlyCompletion === true;
75
+ const reviewOnlyCompleted = reviewOnlyCompletionAllowed && reviewReportCompleted;
76
+ const gateAndProofPassed = gate?.passed === true && proofOk === true;
77
+ const fullReviewPassed = gate?.full_review_passed === true && gateAndProofPassed;
78
+ return {
79
+ ok: gateAndProofPassed || reviewOnlyCompleted,
80
+ status: fullReviewPassed
81
+ ? 'passed'
82
+ : reviewOnlyCompleted
83
+ ? 'review_completed'
84
+ : gateAndProofPassed
85
+ ? gate?.status || 'passed'
86
+ : 'blocked',
87
+ completion_scope: fullReviewPassed
88
+ ? 'full_review_and_optional_fix_verification'
89
+ : reviewOnlyCompleted
90
+ ? 'capture_imagegen_ocr_and_ux_report'
91
+ : null,
92
+ review_report_completed: reviewReportCompleted,
93
+ review_only_completion_allowed: reviewOnlyCompletionAllowed,
94
+ full_review_passed: gate?.full_review_passed === true,
95
+ requires_human_review: gate?.requires_human_review === true,
96
+ unresolved_p0_p1_count: Number(gate?.unresolved_p0_p1_count || 0),
97
+ safety_blockers: Array.isArray(gate?.blockers) ? gate.blockers : [],
98
+ proof_ok: proofOk === true
99
+ };
100
+ }
72
101
  function cleanText(value, fallback = '') {
73
102
  const text = String(value ?? '').replace(/\s+/g, ' ').trim();
74
103
  return text || fallback;
@@ -257,9 +286,21 @@ export async function hydrateImageUxScreenInventory(root, inventory) {
257
286
  export function buildImageUxGeneratedReviewLedger(contract = {}, inventory = buildImageUxScreenInventory(contract), existing = null, opts = {}) {
258
287
  const existingImages = Array.isArray(existing?.generated_review_images) ? existing.generated_review_images : [];
259
288
  const sourceScreens = inventory.source_screens || [];
260
- const normalizedImages = existingImages.map((image, index) => normalizeGeneratedReviewImage(image, sourceScreens[index] || {}, opts));
289
+ const normalizedImages = existingImages.map((image, index) => {
290
+ const declaredSourceId = image.source_screen_id
291
+ || (Array.isArray(image.source_screen_ids) ? image.source_screen_ids[0] : null);
292
+ const sourceScreen = sourceScreens.find((screen) => screen.id === declaredSourceId)
293
+ || sourceScreens[index]
294
+ || {};
295
+ return normalizeGeneratedReviewImage(image, sourceScreen, opts);
296
+ });
261
297
  const realGeneratedCount = normalizedImages.filter((image) => image.real_generated === true && image.mock !== true).length;
262
- const missingRealGeneratedScreens = sourceScreens.filter((screen) => !normalizedImages.some((image) => image.source_screen_id === screen.id && image.real_generated === true && image.mock !== true));
298
+ const coveredSourceScreenIds = sourceScreens
299
+ .filter((screen) => normalizedImages.some((image) => generatedImageCoversScreen(image, screen.id)
300
+ && image.real_generated === true
301
+ && image.mock !== true))
302
+ .map((screen) => screen.id);
303
+ const missingRealGeneratedScreens = sourceScreens.filter((screen) => !coveredSourceScreenIds.includes(screen.id));
263
304
  const nonRealGeneratedCount = normalizedImages.filter((image) => image.real_generated !== true || image.mock === true).length;
264
305
  const textOnlyCount = normalizedImages.filter((image) => image.text_only === true).length;
265
306
  const evidenceBlockers = Array.from(new Set(normalizedImages.flatMap((image) => image.evidence_blockers || []).map((blocker) => String(blocker))));
@@ -295,7 +336,7 @@ export function buildImageUxGeneratedReviewLedger(contract = {}, inventory = bui
295
336
  source_screen_id: screen.id,
296
337
  source_sha256: screen.sha256 || null,
297
338
  prompt: buildCalloutPrompt(screen.id, { target: inventory.target }),
298
- status: normalizedImages.some((image) => image.source_screen_id === screen.id) ? 'generated_or_attached' : 'pending_imagegen',
339
+ status: normalizedImages.some((image) => generatedImageCoversScreen(image, screen.id)) ? 'generated_or_attached' : 'pending_imagegen',
299
340
  required_output: 'annotated_review_image_with_numbered_callouts_severity_labels_markers_arrows_and_mini_comp',
300
341
  requested_fidelity: 'high_fidelity_automatic',
301
342
  image_input_fidelity_note: 'gpt-image-2 processes image inputs at high fidelity automatically; SKS omits unsupported input_fidelity.',
@@ -303,6 +344,8 @@ export function buildImageUxGeneratedReviewLedger(contract = {}, inventory = bui
303
344
  })),
304
345
  generated_count: normalizedImages.length,
305
346
  real_generated_count: realGeneratedCount,
347
+ covered_source_screen_count: coveredSourceScreenIds.length,
348
+ covered_source_screen_ids: coveredSourceScreenIds,
306
349
  non_real_generated_count: nonRealGeneratedCount,
307
350
  required_count: sourceScreens.length,
308
351
  text_only_count: textOnlyCount,
@@ -313,7 +356,9 @@ export function buildImageUxGeneratedReviewLedger(contract = {}, inventory = bui
313
356
  && textOnlyCount === 0
314
357
  && missingRealGeneratedScreens.length > 0,
315
358
  blockers: uniqueBlockers,
316
- passed: sourceScreens.length > 0 && blockers.length === 0 && realGeneratedCount === sourceScreens.length,
359
+ passed: sourceScreens.length > 0
360
+ && blockers.length === 0
361
+ && coveredSourceScreenIds.length === sourceScreens.length,
317
362
  imagegen_blocker: uniqueBlockers.includes('imagegen_capability_missing')
318
363
  ? imagegenCapabilityBlocker()
319
364
  : uniqueBlockers.includes('generated_review_image_missing')
@@ -340,6 +385,17 @@ export function buildImageUxIssueLedger(contract = {}, generatedReviewLedger = b
340
385
  };
341
386
  }
342
387
  export function buildImageUxIterationReport(contract = {}, policy = buildImageUxReviewPolicy(contract), generatedReviewLedger = buildImageUxGeneratedReviewLedger(contract), issueLedger = buildImageUxIssueLedger(contract, generatedReviewLedger), fixTaskPlan = planImageUxFixTasks(issueLedger), fixLoop = runImageUxFixLoop(issueLedger, fixTaskPlan), recapturePlan = buildRecapturePlan(fixLoop)) {
388
+ const generatedImage = (generatedReviewLedger.generated_review_images || [])[0] || null;
389
+ const issues = Array.isArray(issueLedger.issues) ? issueLedger.issues : [];
390
+ const basedOnGeneratedImage = issueLedger.extracted_from_generated_callout === true
391
+ && issues.length > 0
392
+ && issues.every((issue) => issue.extracted_from_generated_image === true
393
+ && issue.generated_image_sha256 === generatedImage?.sha256);
394
+ const requiresHumanReview = (fixTaskPlan.tasks || []).some((task) => task.requires_human_review === true);
395
+ const severityCounts = Object.fromEntries(['P0', 'P1', 'P2', 'P3'].map((severity) => [
396
+ severity,
397
+ issues.filter((issue) => issue.severity === severity).length
398
+ ]));
343
399
  const passed = generatedReviewLedger.passed === true
344
400
  && issueLedger.passed === true
345
401
  && fixLoop.passed === true
@@ -351,6 +407,43 @@ export function buildImageUxIterationReport(contract = {}, policy = buildImageUx
351
407
  schema_version: 2,
352
408
  created_at: nowIso(),
353
409
  contract_hash: contract.sealed_hash || null,
410
+ report_type: 'ux_ui_change_report',
411
+ review_report_completed: generatedReviewLedger.passed === true
412
+ && issueLedger.validation?.ok === true
413
+ && basedOnGeneratedImage,
414
+ full_review_passed: passed,
415
+ requires_human_review: requiresHumanReview,
416
+ unresolved_p0_p1_count: Number(issueLedger.blocking_issue_count || 0),
417
+ analysis_basis: {
418
+ primary_evidence: 'generated_annotated_review_image',
419
+ generated_image_path: generatedImage?.path || null,
420
+ generated_image_sha256: generatedImage?.sha256 || null,
421
+ generated_image_id: generatedImage?.id || null,
422
+ source_screen_id: generatedImage?.source_screen_id || 'screen-1',
423
+ issue_extraction_source: issueLedger.extraction_source || IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT,
424
+ based_on_generated_image: basedOnGeneratedImage
425
+ },
426
+ executive_summary: {
427
+ issue_count: issues.length,
428
+ severity_counts: severityCounts,
429
+ highest_priority: issues.find((issue) => ['P0', 'P1'].includes(issue.severity))?.severity
430
+ || issues[0]?.severity
431
+ || null
432
+ },
433
+ recommendations: issues.map((issue) => ({
434
+ issue_id: issue.id,
435
+ callout_id: issue.callout_id,
436
+ severity: issue.severity,
437
+ title: issue.title,
438
+ observed_problem: issue.detail,
439
+ likely_cause: issue.likely_cause,
440
+ recommended_ux_ui_change: issue.fix_action,
441
+ target_surface: issue.target_surface,
442
+ generated_image_bbox: issue.bbox,
443
+ generated_image_sha256: issue.generated_image_sha256,
444
+ confidence: issue.confidence,
445
+ verification_status: issue.fix_verification_status
446
+ })),
354
447
  loop_policy: {
355
448
  max_full_surface_passes: policy.max_full_surface_passes,
356
449
  max_screen_retries: policy.max_screen_retries,
@@ -416,6 +509,15 @@ export function defaultImageUxReviewGate(contract = {}, parts = {}) {
416
509
  const wrongnessChecked = parts.wrongnessChecked === true || generatedReviewLedger.blockers?.length === 0;
417
510
  const honestModeEvidence = parts.honestModeEvidence || null;
418
511
  const honestModeComplete = parts.honestModeComplete === true && (honestModeEvidence?.ok === true || parts.honestModeEvidenceRequired !== true);
512
+ const requiresHumanReview = (fixTaskPlan.tasks || []).some((task) => task.requires_human_review === true);
513
+ const reviewReportCompleted = realSourceScreenshotPresent
514
+ && officialOrUserScreenshotSource
515
+ && generatedReviewLedger.passed === true
516
+ && realGeneratedCount > 0
517
+ && calloutExtractionSchemaValid
518
+ && issueLedger.extracted_from_generated_callout === true
519
+ && iterationReport.review_report_completed === true
520
+ && iterationReport.analysis_basis?.based_on_generated_image === true;
419
521
  const referenceCloseoutPassed = generatedImageUnavailable
420
522
  && realSourceScreenshotPresent
421
523
  && officialOrUserScreenshotSource
@@ -436,6 +538,10 @@ export function defaultImageUxReviewGate(contract = {}, parts = {}) {
436
538
  status: 'blocked',
437
539
  verified_level: 'blocked',
438
540
  full_review_passed: false,
541
+ review_report_completed: reviewReportCompleted,
542
+ completion_scope: reviewReportCompleted ? 'capture_imagegen_ocr_and_ux_report' : null,
543
+ requires_human_review: requiresHumanReview,
544
+ unresolved_p0_p1_count: Number(issueLedger.blocking_issue_count || 0),
439
545
  reference_only: false,
440
546
  real_source_screenshot_present: realSourceScreenshotPresent,
441
547
  computer_use_or_user_screenshot_source: officialOrUserScreenshotSource,
@@ -448,7 +554,8 @@ export function defaultImageUxReviewGate(contract = {}, parts = {}) {
448
554
  fix_loop_executed_or_not_needed: fixLoopExecutedOrNotNeeded,
449
555
  changed_screens_rechecked: changedScreensRechecked,
450
556
  image_voxel_reference_anchor_created: imageVoxelReferenceAnchorCreated,
451
- image_voxel_relations_created: parts.imageVoxelRelationsCreated === true || realGeneratedImages.some((image) => image.image_voxel_relation === 'generated_callout_review_of') === true,
557
+ image_voxel_relations_created: parts.imageVoxelRelationsCreated === true,
558
+ image_voxel_relation_evidence: parts.imageVoxelRelationEvidence || null,
452
559
  wrongness_checked: wrongnessChecked,
453
560
  honest_mode_complete: honestModeComplete,
454
561
  required_artifacts: [
@@ -497,8 +604,18 @@ export function defaultImageUxReviewGate(contract = {}, parts = {}) {
497
604
  return {
498
605
  ...gate,
499
606
  passed: fullPassed || referenceCloseoutPassed,
500
- status: fullPassed ? 'passed' : referenceCloseoutPassed ? 'verified_partial_reference' : 'blocked',
501
- verified_level: fullPassed ? 'verified' : referenceCloseoutPassed ? 'verified_partial' : 'blocked',
607
+ status: fullPassed
608
+ ? 'passed'
609
+ : referenceCloseoutPassed
610
+ ? 'verified_partial_reference'
611
+ : reviewReportCompleted
612
+ ? 'review_completed'
613
+ : 'blocked',
614
+ verified_level: fullPassed
615
+ ? 'verified'
616
+ : referenceCloseoutPassed || reviewReportCompleted
617
+ ? 'verified_partial'
618
+ : 'blocked',
502
619
  full_review_passed: fullPassed,
503
620
  reference_only: referenceCloseoutPassed
504
621
  };
@@ -513,12 +630,14 @@ export async function writeImageUxReviewRouteArtifacts(dir, contract = {}, opts
513
630
  const extractionReport = await readExistingJson(dir, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT);
514
631
  const existingImagegenRequest = await readExistingJson(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_REQUEST_ARTIFACT);
515
632
  const existingImagegenResponse = await readExistingJson(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT);
633
+ const existingRecapture = await readExistingJson(dir, IMAGE_UX_REVIEW_RECAPTURE_ARTIFACT);
516
634
  const generatedReviewLedger = buildImageUxGeneratedReviewLedger(contract, inventory, existingGenerated, { root });
517
635
  const sourceReferenceEvidence = await ensureImageUxSourceReferenceEvidence(root, dir, inventory);
636
+ const imageVoxelRelationEvidence = await ensureImageUxGeneratedRelationEvidence(root, dir, inventory, generatedReviewLedger, sourceReferenceEvidence);
518
637
  const issueLedger = buildImageUxIssueLedger(contract, generatedReviewLedger, existingIssues);
519
638
  const fixTaskPlan = planImageUxFixTasks(issueLedger);
520
639
  const fixLoop = runImageUxFixLoop(issueLedger, fixTaskPlan, opts.fixLoop || {});
521
- const recapturePlan = buildRecapturePlan(fixLoop, opts.recapture || {});
640
+ const recapturePlan = selectImageUxRecapturePlan(fixLoop, opts, existingRecapture);
522
641
  const iterationReport = buildImageUxIterationReport(contract, policy, generatedReviewLedger, issueLedger, fixTaskPlan, fixLoop, recapturePlan);
523
642
  const outputSchema = await detectCodexExecResumeOutputSchema().catch((err) => ({ ok: true, status: 'integration_optional', warnings: [err.message] }));
524
643
  const honestModeEvidence = await ensureImageUxHonestModeEvidence(dir, {
@@ -540,7 +659,8 @@ export async function writeImageUxReviewRouteArtifacts(dir, contract = {}, opts
540
659
  recapturePlan,
541
660
  iterationReport,
542
661
  imageVoxelReferenceAnchorCreated: sourceReferenceEvidence.ok === true,
543
- imageVoxelRelationsCreated: opts.imageVoxelRelationsCreated === true,
662
+ imageVoxelRelationsCreated: opts.imageVoxelRelationsCreated === true || imageVoxelRelationEvidence.ok === true,
663
+ imageVoxelRelationEvidence,
544
664
  wrongnessChecked: opts.wrongnessChecked === true,
545
665
  honestModeComplete: honestModeEvidence.ok === true,
546
666
  honestModeEvidenceRequired: true,
@@ -567,6 +687,7 @@ export async function writeImageUxReviewRouteArtifacts(dir, contract = {}, opts
567
687
  policy,
568
688
  inventory,
569
689
  imagegen_request: imagegenRequest,
690
+ gpt_image_2_request: imagegenRequest,
570
691
  imagegen_response: imagegenResponse,
571
692
  generated_review_ledger: generatedReviewLedger,
572
693
  issue_ledger: issueLedger,
@@ -580,6 +701,12 @@ export async function writeImageUxReviewRouteArtifacts(dir, contract = {}, opts
580
701
  gate
581
702
  };
582
703
  }
704
+ export function selectImageUxRecapturePlan(fixLoop = {}, opts = {}, existing = null) {
705
+ if (opts.preserveExistingRecapture === true && existing?.schema === 'sks.image-ux-recapture-plan.v2') {
706
+ return existing;
707
+ }
708
+ return buildRecapturePlan(fixLoop, opts.recapture || {});
709
+ }
583
710
  export function imageUxReviewProofEvidence(gate = {}, artifacts = {}) {
584
711
  const issueLedger = artifacts.issue_ledger || {};
585
712
  const generated = artifacts.generated_review_ledger || {};
@@ -599,7 +726,11 @@ export function imageUxReviewProofEvidence(gate = {}, artifacts = {}) {
599
726
  open_p0_p1_count: issueLedger.blocking_issue_count || 0,
600
727
  fixed_p0_p1_count: (issueLedger.issues || []).filter((issue) => ['P0', 'P1'].includes(issue.severity) && issue.status === 'fixed').length,
601
728
  recapture_re_review_status: artifacts.recapture_plan?.changed_screens_rechecked_or_not_applicable ? 'complete_or_not_applicable' : 'blocked',
602
- image_voxel_relation_count: referenceOnly ? 0 : realGeneratedImages.filter((image) => image.image_voxel_relation).length,
729
+ image_voxel_relation_count: referenceOnly
730
+ ? 0
731
+ : gate.image_voxel_relations_created === true
732
+ ? Number(gate.image_voxel_relation_evidence?.relations || generated.covered_source_screen_count || realGeneratedImages.length)
733
+ : 0,
603
734
  computer_use_evidence_mode: artifacts.inventory?.source_screens?.some((screen) => screen.capture_source === 'codex_native_computer_use_screenshot' || screen.capture_source === 'codex_computer_use_screenshot') ? 'native_computer_use_source_screenshot' : artifacts.inventory?.source_screens?.some((screen) => screen.capture_source === 'codex_chrome_extension_screenshot') ? 'chrome_extension_source_screenshot' : 'user_or_static_screenshot',
604
735
  claims: {
605
736
  ux_review_source_screenshot_verified: artifacts.inventory?.passed === true,
@@ -609,7 +740,7 @@ export function imageUxReviewProofEvidence(gate = {}, artifacts = {}) {
609
740
  ux_review_changed_screens_rechecked: artifacts.recapture_plan?.changed_screens_rechecked_or_not_applicable === true,
610
741
  ux_review_image_voxel_relations_verified: referenceOnly
611
742
  ? gate.source_reference_evidence?.ok === true
612
- : realGeneratedImages.some((image) => image.image_voxel_relation)
743
+ : gate.image_voxel_relations_created === true
613
744
  },
614
745
  full_verification_blockers: gate.full_verification_blockers || gate.blockers || [],
615
746
  blockers: gate.blockers || []
@@ -668,6 +799,77 @@ async function ensureImageUxSourceReferenceEvidence(root, dir, inventory = {}) {
668
799
  issues
669
800
  };
670
801
  }
802
+ async function ensureImageUxGeneratedRelationEvidence(root, dir, inventory = {}, generatedReviewLedger = {}, sourceReferenceEvidence = {}) {
803
+ const missionId = path.basename(String(dir));
804
+ const sourceScreens = (inventory.source_screens || []).filter((screen) => screen.status === 'captured' && screen.source);
805
+ const generatedImages = (generatedReviewLedger.generated_review_images || [])
806
+ .filter((image) => image.real_generated === true && image.mock !== true && image.path);
807
+ if (!missionId || !sourceScreens.length || !generatedImages.length || sourceReferenceEvidence.ok !== true) {
808
+ return {
809
+ schema: 'sks.image-ux-generated-relation-evidence.v1',
810
+ ok: false,
811
+ reason: !missionId
812
+ ? 'mission_id_missing'
813
+ : !sourceScreens.length
814
+ ? 'captured_source_screenshot_missing'
815
+ : !generatedImages.length
816
+ ? 'real_generated_image_missing'
817
+ : 'source_reference_anchor_missing',
818
+ relations: 0,
819
+ issues: []
820
+ };
821
+ }
822
+ const anchorIds = Array.isArray(sourceReferenceEvidence.anchor_ids) ? sourceReferenceEvidence.anchor_ids : [];
823
+ const relationIds = [];
824
+ const issues = [];
825
+ let expectedRelations = 0;
826
+ for (const image of generatedImages) {
827
+ const generatedImageId = `${missionId}-${image.id}`;
828
+ try {
829
+ const ingested = await ingestImage(root, image.path, {
830
+ missionId,
831
+ source: 'image-ux-review:gpt-image-2-generated-callout',
832
+ id: generatedImageId
833
+ });
834
+ if (!ingested.ok) {
835
+ issues.push(...(ingested.validation?.issues || [`generated_image_ingest_failed:${image.id}`]));
836
+ continue;
837
+ }
838
+ const coveredScreens = sourceScreens.filter((screen) => generatedImageCoversScreen(image, screen.id));
839
+ expectedRelations += coveredScreens.length;
840
+ for (const sourceScreen of coveredScreens) {
841
+ const sourceImageId = `${missionId}-${sourceScreen.id}-source`;
842
+ const relation = await addImageRelation(root, {
843
+ missionId,
844
+ route: '$Image-UX-Review',
845
+ type: 'generated_callout_review_of',
846
+ beforeImageId: sourceImageId,
847
+ afterImageId: generatedImageId,
848
+ sourceImageId,
849
+ generatedImageId,
850
+ anchors: anchorIds.filter((anchorId) => String(anchorId).startsWith(`${sourceImageId}-`)),
851
+ status: 'verified_partial',
852
+ verification: 'gpt-image-2-generated-callout-bound-to-source'
853
+ });
854
+ if (relation.ok)
855
+ relationIds.push(`${sourceImageId}->${generatedImageId}`);
856
+ else
857
+ issues.push(...(relation.validation?.issues || [`generated_image_relation_failed:${image.id}:${sourceScreen.id}`]));
858
+ }
859
+ }
860
+ catch (err) {
861
+ issues.push(err instanceof Error ? err.message : String(err));
862
+ }
863
+ }
864
+ return {
865
+ schema: 'sks.image-ux-generated-relation-evidence.v1',
866
+ ok: expectedRelations > 0 && relationIds.length === expectedRelations && issues.length === 0,
867
+ relations: relationIds.length,
868
+ relation_ids: relationIds,
869
+ anchor_ids: anchorIds,
870
+ issues
871
+ };
872
+ }
671
873
  async function ensureImageUxHonestModeEvidence(dir, parts = {}, opts = {}) {
672
874
  const missionId = path.basename(String(dir));
673
875
  const existing = await readExistingJson(dir, IMAGE_UX_REVIEW_HONEST_MODE_ARTIFACT);
@@ -744,6 +946,10 @@ export async function buildImageUxCalloutExtractionReport(root, extraction = {},
744
946
  schema: 'sks.image-ux-callout-extraction-report.v1',
745
947
  created_at: nowIso(),
746
948
  provider: extraction.provider || opts.provider || 'unknown',
949
+ analysis_input: 'generated_annotated_review_image',
950
+ generated_image_is_primary_evidence: true,
951
+ source_screenshot_used_for_extraction: false,
952
+ generated_image_id: opts.generatedImageId || ledger.generated_image_id || issues[0]?.generated_review_image_id || null,
747
953
  generated_image_path: generatedPath,
748
954
  generated_image_sha256: generatedSha,
749
955
  source_screenshot_sha256: sourceSha,
@@ -819,6 +1025,10 @@ function generatedReviewImageMissingBlocker() {
819
1025
  }
820
1026
  function normalizeGeneratedReviewImage(image = {}, screen = {}, opts = {}) {
821
1027
  const sourceScreenId = image.source_screen_id || screen.id || 'screen-1';
1028
+ const sourceScreenIds = [...new Set([
1029
+ ...(Array.isArray(image.source_screen_ids) ? image.source_screen_ids : []),
1030
+ sourceScreenId
1031
+ ].map((id) => String(id || '').trim()).filter(Boolean))];
822
1032
  const fileEvidence = generatedImageFileEvidence(opts.root, image);
823
1033
  const sha256 = fileEvidence.sha256 || image.sha256 || null;
824
1034
  const width = Number(image.width || 0) > 0 ? Number(image.width) : null;
@@ -832,6 +1042,7 @@ function normalizeGeneratedReviewImage(image = {}, screen = {}, opts = {}) {
832
1042
  ...image,
833
1043
  id: image.id || compactId('generated-review', `${sourceScreenId}:${image.path || nowIso()}`),
834
1044
  source_screen_id: sourceScreenId,
1045
+ source_screen_ids: sourceScreenIds,
835
1046
  path: image.path || null,
836
1047
  sha256,
837
1048
  width,
@@ -863,6 +1074,13 @@ function normalizeGeneratedReviewImage(image = {}, screen = {}, opts = {}) {
863
1074
  image_voxel_relation: image.image_voxel_relation || (image.id && sourceScreenId ? 'generated_callout_review_of' : null)
864
1075
  };
865
1076
  }
1077
+ function generatedImageCoversScreen(image = {}, screenId) {
1078
+ const id = String(screenId || '');
1079
+ if (!id)
1080
+ return false;
1081
+ return image.source_screen_id === id
1082
+ || (Array.isArray(image.source_screen_ids) && image.source_screen_ids.includes(id));
1083
+ }
866
1084
  function generatedImageEvidenceBlockers(image = {}, evidence = {}) {
867
1085
  if (image.real_generated !== true || image.mock === true || image.source === 'mock_fixture')
868
1086
  return [];