@reticlehq/server 2.13.1 → 2.14.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 (449) hide show
  1. package/SKILL.md +8 -18
  2. package/bin/reticle.js +3 -0
  3. package/dist/bridge/auth-failure-reason.js +12 -1
  4. package/dist/bridge/bridge-security.d.ts +8 -0
  5. package/dist/bridge/bridge-security.js +6 -1
  6. package/dist/bridge/bridge.d.ts +3 -0
  7. package/dist/bridge/bridge.js +23 -1
  8. package/dist/bridge/bridge.test-harness.d.ts +10 -1
  9. package/dist/bridge/bridge.test-harness.js +7 -1
  10. package/dist/bridge/pairing-token.d.ts +17 -0
  11. package/dist/bridge/pairing-token.js +26 -0
  12. package/dist/bridge/token-auth.d.ts +7 -0
  13. package/dist/bridge/token-auth.js +7 -1
  14. package/dist/capsule/capsule-store.d.ts +4 -4
  15. package/dist/capsule/capsule.d.ts +1 -1
  16. package/dist/capsule/capsule.js +3 -3
  17. package/dist/capsule/causal-summary.d.ts +21 -1
  18. package/dist/capsule/causal-summary.js +8 -6
  19. package/dist/capsule/divergence.d.ts +1 -1
  20. package/dist/capsule/divergence.js +26 -10
  21. package/dist/chromium-launch-options.d.ts +2 -1
  22. package/dist/cli/adhoc-verdict.d.ts +58 -0
  23. package/dist/cli/adhoc-verdict.js +115 -0
  24. package/dist/cli/chromium-hint.d.ts +18 -4
  25. package/dist/cli/chromium-hint.js +54 -6
  26. package/dist/cli/cli-doctor.js +1 -2
  27. package/dist/cli/cli-flow-commands.d.ts +16 -2
  28. package/dist/cli/cli-flow-commands.js +28 -9
  29. package/dist/cli/cli-parse-feedback.js +1 -1
  30. package/dist/cli/cli-parse.d.ts +2 -1
  31. package/dist/cli/cli-parse.js +38 -0
  32. package/dist/cli/cli-port.d.ts +5 -13
  33. package/dist/cli/cli-port.js +5 -38
  34. package/dist/cli/cli-update-commands.js +4 -4
  35. package/dist/cli/cli-verify.d.ts +24 -7
  36. package/dist/cli/cli-verify.js +87 -13
  37. package/dist/cli/cloud-keystore.d.ts +2 -1
  38. package/dist/cli/cloud-login.d.ts +2 -1
  39. package/dist/cli/cloud-session.d.ts +2 -1
  40. package/dist/cli/cloud-session.js +1 -1
  41. package/dist/cli/daemon-start-options.d.ts +2 -1
  42. package/dist/cli/device-flow.d.ts +2 -2
  43. package/dist/cli/doctor-daemon-line.d.ts +2 -1
  44. package/dist/cli/doctor-project-line.d.ts +2 -1
  45. package/dist/cli/doctor-rows.d.ts +2 -1
  46. package/dist/cli/drive-attach.d.ts +4 -3
  47. package/dist/cli/gate-hook-message.d.ts +2 -1
  48. package/dist/cli/status-next-action.js +10 -11
  49. package/dist/cli.d.ts +1 -0
  50. package/dist/cli.js +21 -9
  51. package/dist/cloud/account-state.d.ts +20 -0
  52. package/dist/cloud/account-state.js +65 -0
  53. package/dist/cloud/cloud-sync.d.ts +46 -3
  54. package/dist/cloud/cloud-sync.js +116 -0
  55. package/dist/cloud/sync-cycle.d.ts +5 -4
  56. package/dist/cloud/sync-cycle.js +1 -1
  57. package/dist/cloud/sync-daemon.d.ts +3 -19
  58. package/dist/cloud/sync-daemon.js +2 -2
  59. package/dist/crawl/crawl-tools.js +1 -1
  60. package/dist/crawl/crawl.d.ts +5 -5
  61. package/dist/crawl/crawl.js +138 -4
  62. package/dist/daemon/daemon-resilience.d.ts +3 -2
  63. package/dist/daemon/daemon-resilience.js +43 -4
  64. package/dist/daemon/daemon-resolve.d.ts +0 -9
  65. package/dist/daemon/daemon-resolve.js +2 -2
  66. package/dist/daemon/dev-servers.d.ts +1 -1
  67. package/dist/daemon/dev-servers.js +1 -1
  68. package/dist/daemon/loopback-alias.d.ts +2 -1
  69. package/dist/daemon/startup-failure.d.ts +11 -0
  70. package/dist/daemon/startup-failure.js +46 -0
  71. package/dist/events/contradictions.d.ts +10 -16
  72. package/dist/events/contradictions.js +149 -17
  73. package/dist/events/declared.d.ts +13 -1
  74. package/dist/events/declared.js +59 -3
  75. package/dist/events/echo-mismatch.js +75 -6
  76. package/dist/events/lineage.d.ts +52 -0
  77. package/dist/events/lineage.js +122 -0
  78. package/dist/events/net-evidence.d.ts +21 -0
  79. package/dist/events/net-evidence.js +81 -0
  80. package/dist/events/predicate-console.d.ts +13 -0
  81. package/dist/events/predicate-console.js +138 -0
  82. package/dist/events/predicate-element.d.ts +13 -0
  83. package/dist/events/predicate-element.js +214 -0
  84. package/dist/events/predicate-eval.d.ts +15 -3
  85. package/dist/events/predicate-eval.js +128 -110
  86. package/dist/events/predicate-parse.js +34 -1
  87. package/dist/events/predicate-request-body.d.ts +50 -0
  88. package/dist/events/predicate-request-body.js +120 -0
  89. package/dist/events/predicate-route.d.ts +21 -1
  90. package/dist/events/predicate-route.js +149 -21
  91. package/dist/events/predicate-schema.d.ts +8 -1
  92. package/dist/events/predicate-schema.js +34 -0
  93. package/dist/events/predicate.d.ts +2 -0
  94. package/dist/events/predicate.js +52 -159
  95. package/dist/events/split-text-miss.d.ts +6 -4
  96. package/dist/events/split-text-miss.js +12 -5
  97. package/dist/flows/affected-tools.js +22 -9
  98. package/dist/flows/annotate.d.ts +2 -1
  99. package/dist/flows/annotate.js +19 -1
  100. package/dist/flows/coverage.d.ts +2 -2
  101. package/dist/flows/flake.d.ts +3 -3
  102. package/dist/flows/flow-expect-grammar.d.ts +33 -0
  103. package/dist/flows/flow-expect-grammar.js +155 -0
  104. package/dist/flows/flow-replay-run.d.ts +1 -1
  105. package/dist/flows/flow-replay-run.js +28 -15
  106. package/dist/flows/flow-replay.d.ts +10 -0
  107. package/dist/flows/flow-replay.js +57 -22
  108. package/dist/flows/flow-secret-field.d.ts +14 -0
  109. package/dist/flows/flow-secret-field.js +22 -0
  110. package/dist/flows/flow-step-runners.d.ts +1 -0
  111. package/dist/flows/flow-step-runners.js +91 -34
  112. package/dist/flows/flow-success.js +22 -0
  113. package/dist/flows/flow-tools.js +27 -8
  114. package/dist/flows/flow-upload-resolve.d.ts +23 -0
  115. package/dist/flows/flow-upload-resolve.js +46 -0
  116. package/dist/flows/flows.d.ts +15 -0
  117. package/dist/flows/flows.js +17 -14
  118. package/dist/flows/git-changed.d.ts +23 -4
  119. package/dist/flows/git-changed.js +32 -6
  120. package/dist/flows/heal-run.js +2 -2
  121. package/dist/flows/predicate-to-expect.d.ts +3 -2
  122. package/dist/flows/predicate-to-expect.js +11 -5
  123. package/dist/flows/recording-backtrack.d.ts +24 -0
  124. package/dist/flows/recording-backtrack.js +73 -0
  125. package/dist/flows/recordings.d.ts +6 -0
  126. package/dist/flows/replay-halt.d.ts +26 -0
  127. package/dist/flows/replay-halt.js +30 -0
  128. package/dist/flows/replay.d.ts +0 -8
  129. package/dist/flows/replay.js +15 -3
  130. package/dist/flows/suite-flakes.d.ts +2 -1
  131. package/dist/flows/verify-change-tools.js +16 -4
  132. package/dist/honesty/already-true.d.ts +14 -0
  133. package/dist/honesty/already-true.js +47 -4
  134. package/dist/honesty/blind-spots.d.ts +3 -1
  135. package/dist/honesty/blind-spots.js +24 -1
  136. package/dist/honesty/body-capture-remedy.d.ts +42 -0
  137. package/dist/honesty/body-capture-remedy.js +95 -0
  138. package/dist/honesty/feature-capture.d.ts +3 -2
  139. package/dist/honesty/feature-capture.js +1 -1
  140. package/dist/honesty/gap-ledger.d.ts +1 -1
  141. package/dist/honesty/gap-ledger.js +1 -1
  142. package/dist/honesty/instrumentation-gaps.d.ts +8 -1
  143. package/dist/honesty/instrumentation-gaps.js +12 -2
  144. package/dist/honesty/observability.d.ts +4 -3
  145. package/dist/honesty/tool-hit-rate.d.ts +3 -2
  146. package/dist/honesty/uncaptured-bodies.d.ts +3 -1
  147. package/dist/honesty/uncaptured-bodies.js +6 -2
  148. package/dist/honesty/unsettled.d.ts +9 -0
  149. package/dist/honesty/unsettled.js +45 -0
  150. package/dist/honesty/verified.d.ts +32 -1
  151. package/dist/honesty/verified.js +37 -10
  152. package/dist/impact/impact-store.d.ts +5 -14
  153. package/dist/impact/impact-store.js +49 -6
  154. package/dist/index.js +1 -1
  155. package/dist/input/network-detail.d.ts +26 -1
  156. package/dist/input/network-detail.js +44 -12
  157. package/dist/input/network-mock-tools.js +24 -17
  158. package/dist/input/real-input.js +1 -1
  159. package/dist/input/viewport-tools.js +25 -13
  160. package/dist/intent/inline-intent.d.ts +0 -10
  161. package/dist/intent/inline-intent.js +1 -1
  162. package/dist/intent/intent-shard-store.d.ts +3 -2
  163. package/dist/intent/intent-shard-store.js +1 -1
  164. package/dist/intent/intent-shard.d.ts +14 -14
  165. package/dist/intent/intent-shard.js +4 -4
  166. package/dist/intent/intent-store.d.ts +1 -1
  167. package/dist/intent/intent-store.js +1 -1
  168. package/dist/intent/intent-subject.d.ts +2 -1
  169. package/dist/intent/open-intents.d.ts +3 -2
  170. package/dist/license/license.d.ts +4 -4
  171. package/dist/license/license.js +1 -1
  172. package/dist/mcp/attach-memory.d.ts +2 -1
  173. package/dist/mcp/client-identity.d.ts +2 -1
  174. package/dist/mcp/mcp-outage.d.ts +1 -1
  175. package/dist/mcp/mcp-outage.js +1 -1
  176. package/dist/mcp/mcp-proxy.d.ts +3 -1
  177. package/dist/mcp/mcp-proxy.js +32 -7
  178. package/dist/mcp/mcp.js +3 -1
  179. package/dist/mcp/proxy-handshake.d.ts +2 -1
  180. package/dist/mcp/proxy-idle-exit.d.ts +45 -0
  181. package/dist/mcp/proxy-idle-exit.js +94 -0
  182. package/dist/mcp/server-instructions.d.ts +2 -1
  183. package/dist/memory/project-memory.d.ts +2 -3
  184. package/dist/memory/project-memory.js +1 -1
  185. package/dist/oracles/propose-consequences.js +5 -5
  186. package/dist/oracles/self-instrument.d.ts +2 -2
  187. package/dist/oracles/self-instrument.js +1 -1
  188. package/dist/platform.d.ts +3 -5
  189. package/dist/platform.js +3 -5
  190. package/dist/pool/browser-pool.d.ts +92 -0
  191. package/dist/pool/browser-pool.js +119 -5
  192. package/dist/pool/playwright-launcher.js +29 -2
  193. package/dist/pool/storage-seed.d.ts +23 -0
  194. package/dist/pool/storage-seed.js +198 -0
  195. package/dist/project/artifact-root.d.ts +2 -1
  196. package/dist/project/memory-fs.d.ts +2 -1
  197. package/dist/project/reticle-dir.js +1 -1
  198. package/dist/project/session-root.d.ts +8 -0
  199. package/dist/project/session-root.js +9 -2
  200. package/dist/runs/reticle-runner.d.ts +14 -1
  201. package/dist/runs/reticle-runner.js +39 -2
  202. package/dist/session/attachment-history.d.ts +2 -1
  203. package/dist/session/fake-session.d.ts +37 -0
  204. package/dist/session/fake-session.js +81 -0
  205. package/dist/session/lease-visibility.d.ts +2 -1
  206. package/dist/session/live-control-tools.js +6 -0
  207. package/dist/session/no-session-diagnosis.d.ts +17 -1
  208. package/dist/session/no-session-diagnosis.js +32 -6
  209. package/dist/session/no-session-next-action.d.ts +14 -0
  210. package/dist/session/no-session-next-action.js +18 -0
  211. package/dist/session/no-session-watch.js +46 -3
  212. package/dist/session/registered-projects.d.ts +54 -0
  213. package/dist/session/registered-projects.js +73 -0
  214. package/dist/session/review-store.d.ts +2 -1
  215. package/dist/session/session-health.d.ts +62 -1
  216. package/dist/session/session-health.js +82 -2
  217. package/dist/session/session-manager.d.ts +11 -1
  218. package/dist/session/session-manager.js +18 -3
  219. package/dist/session/session-replaced.d.ts +24 -0
  220. package/dist/session/session-replaced.js +29 -0
  221. package/dist/session/session-successor.js +20 -4
  222. package/dist/session/session.d.ts +13 -5
  223. package/dist/session/session.js +31 -23
  224. package/dist/setup/agent-configs.d.ts +3 -2
  225. package/dist/setup/agent-writer.d.ts +3 -2
  226. package/dist/setup/approval-migration.d.ts +2 -1
  227. package/dist/setup/approval-migration.js +1 -1
  228. package/dist/setup/auto-approve.d.ts +11 -19
  229. package/dist/setup/auto-approve.js +15 -12
  230. package/dist/{init → setup}/confirm.js +1 -1
  231. package/dist/setup/desktop-shape.d.ts +3 -2
  232. package/dist/setup/drive-agent.d.ts +2 -3
  233. package/dist/setup/drive-agent.js +1 -1
  234. package/dist/setup/drive-plan.d.ts +3 -2
  235. package/dist/setup/ensure-daemon.d.ts +2 -1
  236. package/dist/setup/existing-app.d.ts +12 -0
  237. package/dist/setup/existing-app.js +13 -0
  238. package/dist/setup/init-host.d.ts +11 -0
  239. package/dist/setup/init-host.js +26 -0
  240. package/dist/setup/init-runtime.d.ts +4 -10
  241. package/dist/setup/init-runtime.js +23 -16
  242. package/dist/setup/license-key.d.ts +2 -1
  243. package/dist/setup/listeners.d.ts +2 -1
  244. package/dist/setup/loopback-probe-urls.d.ts +18 -0
  245. package/dist/setup/loopback-probe-urls.js +52 -0
  246. package/dist/setup/node-effects.d.ts +3 -0
  247. package/dist/setup/node-effects.js +26 -11
  248. package/dist/setup/page-probe.d.ts +5 -0
  249. package/dist/setup/relaunch.d.ts +2 -1
  250. package/dist/setup/run-setup.d.ts +5 -1
  251. package/dist/setup/run-setup.js +141 -50
  252. package/dist/setup/setup-command.d.ts +3 -2
  253. package/dist/setup/setup-command.js +8 -0
  254. package/dist/setup/setup-options.d.ts +5 -3
  255. package/dist/setup/setup-options.js +3 -2
  256. package/dist/setup/wait-progress.d.ts +28 -0
  257. package/dist/setup/wait-progress.js +43 -0
  258. package/dist/telemetry/app-instrumented.d.ts +2 -1
  259. package/dist/telemetry/app-instrumented.js +1 -1
  260. package/dist/telemetry/automation-hint.d.ts +1 -1
  261. package/dist/telemetry/automation-hint.js +1 -1
  262. package/dist/telemetry/cli-telemetry.js +1 -1
  263. package/dist/telemetry/connect-failure.d.ts +1 -1
  264. package/dist/telemetry/connect-failure.js +1 -1
  265. package/dist/telemetry/crash-cause.d.ts +3 -2
  266. package/dist/telemetry/crash-cause.js +1 -1
  267. package/dist/telemetry/daemon-telemetry.d.ts +3 -2
  268. package/dist/telemetry/daemon-telemetry.js +1 -1
  269. package/dist/telemetry/error-class.d.ts +3 -2
  270. package/dist/telemetry/error-class.js +1 -1
  271. package/dist/telemetry/feedback-cli.js +1 -1
  272. package/dist/telemetry/feedback-context.d.ts +3 -2
  273. package/dist/telemetry/feedback-context.js +5 -4
  274. package/dist/telemetry/feedback-local.d.ts +3 -2
  275. package/dist/telemetry/feedback.d.ts +4 -3
  276. package/dist/telemetry/feedback.js +1 -1
  277. package/dist/telemetry/git-facts.d.ts +2 -2
  278. package/dist/telemetry/git-facts.js +1 -1
  279. package/dist/telemetry/identify.d.ts +1 -1
  280. package/dist/telemetry/identify.js +1 -1
  281. package/dist/telemetry/init-telemetry.d.ts +2 -14
  282. package/dist/telemetry/init-telemetry.js +4 -13
  283. package/dist/telemetry/install-source.d.ts +7 -13
  284. package/dist/telemetry/install-source.js +7 -29
  285. package/dist/telemetry/license-activation.d.ts +3 -2
  286. package/dist/telemetry/license-activation.js +1 -1
  287. package/dist/telemetry/machine-snapshot.d.ts +1 -1
  288. package/dist/telemetry/mcp-connection.js +1 -1
  289. package/dist/telemetry/project-profile.d.ts +0 -14
  290. package/dist/telemetry/project-profile.js +1 -1
  291. package/dist/telemetry/proxy-telemetry.js +1 -1
  292. package/dist/telemetry/session-metrics.d.ts +1 -1
  293. package/dist/telemetry/session-metrics.js +1 -1
  294. package/dist/telemetry/telemetry.d.ts +1 -17
  295. package/dist/telemetry/telemetry.js +4 -4
  296. package/dist/telemetry/tool-refused.d.ts +1 -1
  297. package/dist/telemetry/tool-refused.js +1 -1
  298. package/dist/tools/act-observation.d.ts +3 -2
  299. package/dist/tools/act-preflight.js +8 -3
  300. package/dist/tools/act-target.d.ts +4 -0
  301. package/dist/tools/act-target.js +14 -5
  302. package/dist/tools/act-tools.d.ts +0 -2
  303. package/dist/tools/act-tools.js +45 -15
  304. package/dist/tools/assert-source.d.ts +30 -0
  305. package/dist/tools/assert-source.js +21 -0
  306. package/dist/tools/assert-verdict.d.ts +1 -1
  307. package/dist/tools/assert-verdict.js +14 -4
  308. package/dist/tools/browser-tools.js +28 -8
  309. package/dist/tools/complete-snapshot.d.ts +3 -2
  310. package/dist/tools/dynamic-tools.js +8 -0
  311. package/dist/tools/error-recovery.d.ts +10 -0
  312. package/dist/tools/error-recovery.js +23 -0
  313. package/dist/tools/feedback-tools.js +1 -1
  314. package/dist/tools/invoke-tool.js +2 -0
  315. package/dist/tools/lease-tools.d.ts +48 -20
  316. package/dist/tools/lease-tools.js +311 -76
  317. package/dist/tools/lineage-tools.d.ts +14 -0
  318. package/dist/tools/lineage-tools.js +64 -0
  319. package/dist/tools/merge-tools.js +1 -3
  320. package/dist/tools/navigate-arrival.d.ts +45 -1
  321. package/dist/tools/navigate-arrival.js +39 -6
  322. package/dist/tools/navigate-result.d.ts +8 -1
  323. package/dist/tools/navigate-result.js +14 -4
  324. package/dist/tools/numeric-bounds.d.ts +0 -11
  325. package/dist/tools/numeric-bounds.js +4 -4
  326. package/dist/tools/observe-tools.js +22 -26
  327. package/dist/tools/observed-nothing.d.ts +9 -0
  328. package/dist/tools/observed-nothing.js +2 -1
  329. package/dist/tools/read-tools.js +12 -17
  330. package/dist/tools/real-input-attempt.d.ts +14 -6
  331. package/dist/tools/real-input-attempt.js +75 -18
  332. package/dist/tools/reconcile-tools.js +2 -1
  333. package/dist/tools/reload-result.d.ts +6 -1
  334. package/dist/tools/reload-result.js +7 -2
  335. package/dist/tools/resolve-target.d.ts +7 -1
  336. package/dist/tools/resolve-target.js +72 -9
  337. package/dist/tools/settle-in-flight.d.ts +1 -1
  338. package/dist/tools/settle-in-flight.js +9 -2
  339. package/dist/tools/snapshot-delta.d.ts +24 -1
  340. package/dist/tools/snapshot-delta.js +66 -7
  341. package/dist/tools/tool-names.d.ts +1 -0
  342. package/dist/tools/tool-names.js +1 -0
  343. package/dist/tools/tool-surface.js +20 -2
  344. package/dist/tools/tools.js +120 -11
  345. package/dist/tools/verify-next-baton.d.ts +2 -1
  346. package/dist/update/sdk-sync.d.ts +1 -1
  347. package/dist/update/sdk-sync.js +1 -1
  348. package/dist/update/updater.js +1 -1
  349. package/dist/version/sdk-fix.d.ts +2 -1
  350. package/dist/version/version-skew.d.ts +0 -8
  351. package/dist/version/version-skew.js +1 -1
  352. package/docs/README.md +8 -6
  353. package/docs/actions.mdx +1 -1
  354. package/docs/agent-cheatsheet.md +4 -2
  355. package/docs/architecture.md +1 -1
  356. package/docs/cli/mcp.mdx +3 -1
  357. package/docs/cli/watch.mdx +1 -1
  358. package/docs/cli.mdx +1 -1
  359. package/docs/desktop.mdx +1 -1
  360. package/docs/docs.json +8 -3
  361. package/docs/faq.mdx +9 -0
  362. package/docs/first-drive.md +136 -0
  363. package/docs/frameworks.mdx +29 -7
  364. package/docs/gate-plan.md +2 -2
  365. package/docs/gates.md +19 -11
  366. package/docs/getting-started.md +7 -1
  367. package/docs/http-transport.md +164 -0
  368. package/docs/index.mdx +5 -3
  369. package/docs/install-manual.mdx +1 -1
  370. package/docs/packages/tauri.mdx +1 -1
  371. package/docs/packages.mdx +15 -3
  372. package/docs/platform-integration.md +1 -1
  373. package/docs/predicates.mdx +2 -0
  374. package/docs/system-map.md +2 -2
  375. package/docs/telemetry-contract.md +4 -4
  376. package/docs/telemetry-events.mdx +1 -1
  377. package/docs/telemetry.md +3 -3
  378. package/docs/tools-navigate.mdx +14 -6
  379. package/docs/usage.md +4 -4
  380. package/package.json +7 -4
  381. package/dist/init/agent-rules.d.ts +0 -116
  382. package/dist/init/agent-rules.js +0 -279
  383. package/dist/init/app-choice.d.ts +0 -20
  384. package/dist/init/app-choice.js +0 -25
  385. package/dist/init/astro-host.d.ts +0 -21
  386. package/dist/init/astro-host.js +0 -27
  387. package/dist/init/astro-patch.d.ts +0 -33
  388. package/dist/init/astro-patch.js +0 -300
  389. package/dist/init/capabilities.d.ts +0 -40
  390. package/dist/init/capabilities.js +0 -150
  391. package/dist/init/closing-hint.d.ts +0 -36
  392. package/dist/init/closing-hint.js +0 -120
  393. package/dist/init/connect-steps.d.ts +0 -2
  394. package/dist/init/connect-steps.js +0 -50
  395. package/dist/init/cra.d.ts +0 -79
  396. package/dist/init/cra.js +0 -178
  397. package/dist/init/csp-check.d.ts +0 -38
  398. package/dist/init/csp-check.js +0 -107
  399. package/dist/init/csp-doctor.d.ts +0 -32
  400. package/dist/init/csp-doctor.js +0 -54
  401. package/dist/init/desktop-doctor.d.ts +0 -41
  402. package/dist/init/desktop-doctor.js +0 -198
  403. package/dist/init/detect.d.ts +0 -96
  404. package/dist/init/detect.js +0 -209
  405. package/dist/init/dev-script.d.ts +0 -50
  406. package/dist/init/dev-script.js +0 -66
  407. package/dist/init/format-generated.d.ts +0 -18
  408. package/dist/init/format-generated.js +0 -39
  409. package/dist/init/install-hint.d.ts +0 -9
  410. package/dist/init/install-hint.js +0 -29
  411. package/dist/init/mcp-clients.d.ts +0 -89
  412. package/dist/init/mcp-clients.js +0 -299
  413. package/dist/init/mcp-registered.d.ts +0 -12
  414. package/dist/init/mcp-registered.js +0 -14
  415. package/dist/init/mcp.d.ts +0 -89
  416. package/dist/init/mcp.js +0 -129
  417. package/dist/init/next-patch.d.ts +0 -39
  418. package/dist/init/next-patch.js +0 -196
  419. package/dist/init/node-io.d.ts +0 -7
  420. package/dist/init/node-io.js +0 -117
  421. package/dist/init/non-js-project.d.ts +0 -27
  422. package/dist/init/non-js-project.js +0 -84
  423. package/dist/init/patch-kind.d.ts +0 -20
  424. package/dist/init/patch-kind.js +0 -10
  425. package/dist/init/plan-framework.d.ts +0 -64
  426. package/dist/init/plan-framework.js +0 -565
  427. package/dist/init/plan.d.ts +0 -242
  428. package/dist/init/plan.js +0 -703
  429. package/dist/init/preflight.d.ts +0 -28
  430. package/dist/init/preflight.js +0 -38
  431. package/dist/init/project-id.d.ts +0 -12
  432. package/dist/init/project-id.js +0 -25
  433. package/dist/init/refresh-rules.d.ts +0 -39
  434. package/dist/init/refresh-rules.js +0 -51
  435. package/dist/init/run.d.ts +0 -117
  436. package/dist/init/run.js +0 -806
  437. package/dist/init/slash-command.d.ts +0 -27
  438. package/dist/init/slash-command.js +0 -132
  439. package/dist/init/snippets.d.ts +0 -134
  440. package/dist/init/snippets.js +0 -572
  441. package/dist/init/vite-config.d.ts +0 -18
  442. package/dist/init/vite-config.js +0 -114
  443. package/dist/init/windows-quote.d.ts +0 -38
  444. package/dist/init/windows-quote.js +0 -70
  445. package/dist/init/workspace-apps.d.ts +0 -30
  446. package/dist/init/workspace-apps.js +0 -178
  447. package/dist/project/remember-project.d.ts +0 -24
  448. package/dist/project/remember-project.js +0 -40
  449. /package/dist/{init → setup}/confirm.d.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { type DevServerEntry } from '@reticlehq/core';
1
+ import { type DevServerEntry } from '@reticlehq/core/artifacts';
2
2
  /** Injected so the reading is unit-testable without a real home or real processes. */
3
3
  export interface DevServerIo {
4
4
  readdir: (dir: string) => string[];
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { readdirSync, readFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
- import { DevServerEntrySchema, devServerRegistryPort, liveDevServers, } from '@reticlehq/core';
21
+ import { DevServerEntrySchema, devServerRegistryPort, liveDevServers, } from '@reticlehq/core/artifacts';
22
22
  /** process.kill(pid, 0) throws iff the process is gone — the same probe the daemon registry uses. */
23
23
  function isAliveNow(pid) {
24
24
  try {
@@ -18,7 +18,7 @@
18
18
  */
19
19
  /** IPv6 loopback. The other half of what `localhost` can mean. */
20
20
  export declare const IPV6_LOOPBACK = "::1";
21
- export interface LoopbackAlias {
21
+ interface LoopbackAlias {
22
22
  /** False when the alias could not be bound — the daemon still serves IPv4 as before. */
23
23
  opened: boolean;
24
24
  close: () => Promise<void>;
@@ -30,3 +30,4 @@ export interface LoopbackAlias {
30
30
  * trust holds — the alias does not widen who may connect, only which address they may name.
31
31
  */
32
32
  export declare function openLoopbackAlias(port: number): Promise<LoopbackAlias>;
33
+ export {};
@@ -0,0 +1,11 @@
1
+ /** The structured log event emitted when the daemon child cannot start. */
2
+ export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
3
+ /**
4
+ * Return the most recent startup cause written by this launch attempt.
5
+ *
6
+ * Daemon logs are append-only across launches, so an event name alone is not enough: surfacing an
7
+ * older failure would confidently diagnose the wrong attempt. The parent supplies the timestamp it
8
+ * captured immediately before spawning and this reader ignores every earlier record. Malformed or
9
+ * unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
10
+ */
11
+ export declare function readDaemonStartupCause(path: string, sinceMs: number): string | undefined;
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** The structured log event emitted when the daemon child cannot start. */
3
+ export const DAEMON_START_FAILED_EVENT = 'reticle_daemon_start_failed';
4
+ /**
5
+ * Return the most recent startup cause written by this launch attempt.
6
+ *
7
+ * Daemon logs are append-only across launches, so an event name alone is not enough: surfacing an
8
+ * older failure would confidently diagnose the wrong attempt. The parent supplies the timestamp it
9
+ * captured immediately before spawning and this reader ignores every earlier record. Malformed or
10
+ * unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
11
+ */
12
+ export function readDaemonStartupCause(path, sinceMs) {
13
+ let contents;
14
+ try {
15
+ contents = readFileSync(path, 'utf8');
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ const lines = contents.split('\n');
21
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
22
+ const line = lines[index]?.trim();
23
+ if (undefined === line || 0 === line.length)
24
+ continue;
25
+ let record;
26
+ try {
27
+ record = JSON.parse(line);
28
+ }
29
+ catch {
30
+ continue;
31
+ }
32
+ if (typeof record !== 'object' || null === record)
33
+ continue;
34
+ const candidate = record;
35
+ if (candidate.event !== DAEMON_START_FAILED_EVENT)
36
+ continue;
37
+ if (typeof candidate.t !== 'string' || typeof candidate.error !== 'string')
38
+ continue;
39
+ const recordedAt = Date.parse(candidate.t);
40
+ const error = candidate.error.trim();
41
+ if (!Number.isFinite(recordedAt) || recordedAt < sinceMs || 0 === error.length)
42
+ continue;
43
+ return error;
44
+ }
45
+ return undefined;
46
+ }
@@ -156,21 +156,15 @@ export interface ContradictionOptions {
156
156
  * one: a caller that cannot say which page is under test gets the behaviour it had before this.
157
157
  */
158
158
  appOrigin?: string | undefined;
159
+ /**
160
+ * The traffic the assertion actually named — `urlContains` from each net clause, `''` for a clause
161
+ * that named the whole channel.
162
+ *
163
+ * Used only by `duplicate-request`, to tell "the write you asked about fired twice" from "some
164
+ * other endpoint is busy". Undefined disables the split and every duplicate is reported as it was
165
+ * before, which is what a caller with no predicate (a bare `observe`) should get: with nothing
166
+ * declared, nothing is unrelated.
167
+ */
168
+ namedNetUrls?: readonly string[] | undefined;
159
169
  }
160
- /**
161
- * Cross-channel contradictions in this window, with the edit-epoch caveat attached when it applies.
162
- *
163
- * Cross-epoch evidence is LABELLED rather than excluded, which is the opposite of what the document
164
- * scoping does, and the difference is the point. A navigation is total — it throws away the page,
165
- * the refs, the in-flight requests and the state — so nothing recorded before it is still about the
166
- * world, and dropping it is the only honest option. A hot update is not: most modules, most of the
167
- * DOM, the whole network log and every console line survive one, so most of what was observed a
168
- * second before an edit is still true a second after. Excluding it would empty windows that hold
169
- * real findings, and an emptied window reads as "nothing happened" — which is the more expensive of
170
- * the two wrong answers and the one this whole family of checks exists to prevent.
171
- *
172
- * So the findings stand and the caveat is said out loud, and only when it is unambiguous: EVERY
173
- * observation in the window predates the edit. One post-edit observation and the agent is already
174
- * looking at the code it wrote, so the label would be noise.
175
- */
176
170
  export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
@@ -21,6 +21,32 @@ function netCall(e) {
21
21
  : true === e.data['ok'] || (e.data['ok'] === undefined && (status ?? 0) < 400),
22
22
  };
23
23
  }
24
+ /**
25
+ * The failed calls that a LATER successful call to the same endpoint replaced.
26
+ *
27
+ * Identity, not index: each `netCall` is a fresh object held by `settled`, and `settled` is built
28
+ * from events in sequence order, so "later" is simply "further along the array". Compared on
29
+ * `matchUrl` so a cache-buster or a changing query token cannot make a retry look like a different
30
+ * call — the same normalisation every other url comparison here uses.
31
+ */
32
+ function recoveredByRetry(settled) {
33
+ const out = new Set();
34
+ for (let i = 0; i < settled.length; i += 1) {
35
+ const call = settled[i];
36
+ if (call === undefined || false !== call.ok)
37
+ continue;
38
+ for (let j = i + 1; j < settled.length; j += 1) {
39
+ const later = settled[j];
40
+ if (later === undefined)
41
+ continue;
42
+ if (true === later.ok && later.method === call.method && later.matchUrl === call.matchUrl) {
43
+ out.add(call);
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ return out;
49
+ }
24
50
  /**
25
51
  * Did the user-visible application state move forward? DOM, store and route only — deliberately NOT
26
52
  * network, animation or signal. The question every rule below asks is "did the app act as if it
@@ -221,6 +247,42 @@ function splitForeignTraffic(events, appOrigin) {
221
247
  * observation in the window predates the edit. One post-edit observation and the agent is already
222
248
  * looking at the code it wrote, so the label would be noise.
223
249
  */
250
+ /**
251
+ * Below this, repeated writes are a BURST, whatever their spacing.
252
+ *
253
+ * A double submit is two clicks, or one click and a re-render: milliseconds apart. Nothing a human
254
+ * or a StrictMode remount does lands on a quarter-second grid, so this is the floor under which
255
+ * regularity means nothing.
256
+ */
257
+ const POLL_MIN_INTERVAL_MS = 250;
258
+ /**
259
+ * How far a gap may sit from the median and still count as the same cadence.
260
+ *
261
+ * Loose on purpose: a real poll drifts under load, and a `setInterval` competing with a busy main
262
+ * thread is not metronomic. Tight enough that a burst followed by a late retry — the shape a double
263
+ * submit plus a user's second attempt makes — is not read as a rhythm.
264
+ */
265
+ const POLL_JITTER_RATIO = 0.4;
266
+ /**
267
+ * Is this the same write on a steady interval, rather than the same write twice?
268
+ *
269
+ * THREE samples minimum, and that is the load-bearing part: two writes give one gap, and a single
270
+ * gap cannot distinguish a cadence from a coincidence. Two writes stay a duplicate however far
271
+ * apart they are, which is the classic double submit and every case this rule was written for.
272
+ */
273
+ function isSteadyCadence(times) {
274
+ if (times.length < 3)
275
+ return false;
276
+ const ordered = [...times].sort((a, b) => a - b);
277
+ const gaps = [];
278
+ for (let i = 1; i < ordered.length; i += 1)
279
+ gaps.push((ordered[i] ?? 0) - (ordered[i - 1] ?? 0));
280
+ const sorted = [...gaps].sort((a, b) => a - b);
281
+ const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
282
+ if (median < POLL_MIN_INTERVAL_MS)
283
+ return false;
284
+ return gaps.every((gap) => Math.abs(gap - median) <= median * POLL_JITTER_RATIO);
285
+ }
224
286
  export function findContradictions(allEvents, options = {}) {
225
287
  const found = findWindowContradictions(allEvents, options);
226
288
  const predates = allEvents.length > 0 &&
@@ -303,7 +365,20 @@ function findWindowContradictions(allEvents, options) {
303
365
  },
304
366
  ];
305
367
  }
306
- const failed = settled.filter((c) => false === c.ok);
368
+ // A failure the app RECOVERED from is not evidence against anything.
369
+ //
370
+ // Reported from the field on an app whose api-client documents the pattern every token-refreshing
371
+ // app has: a 401 re-hydrates the session and retries once. One extraction produced a 401 and then
372
+ // a 200 to the same endpoint, with exactly one extraction in state, and Reticle called it two
373
+ // separate contradictions. Both were false, both cost calls to disprove, and a rule that
374
+ // manufactures reds costs more trust than a missed bug does.
375
+ //
376
+ // Structural, not a timing heuristic: what makes it a retry is that a LATER call to the same
377
+ // method and url SUCCEEDED. The write landed, so the UI was entitled to move and a success signal
378
+ // was entitled to fire. Filtered at the definition rather than at the two reading sites, because
379
+ // both rules make the same claim about the same evidence and a fix in one is a fix in half.
380
+ const recovered = recoveredByRetry(settled);
381
+ const failed = settled.filter((c) => false === c.ok && !recovered.has(c));
307
382
  // The failures NOBODY declared — see ContradictionOptions.expectedFailures. Only the heuristic
308
383
  // "the UI moved while a request failed" rule reads this; the sharp rules still read `failed`.
309
384
  const unexpected = failed.filter((c) => !matchesDeclaredFailure(c, options.expectedFailures ?? []));
@@ -352,12 +427,33 @@ function findWindowContradictions(allEvents, options) {
352
427
  e.type === EventType.DOM_TEXT);
353
428
  const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
354
429
  if (routed && !hashAnchorOnly && !rendered && !fetched && true !== options.renderProved) {
355
- found.push({
356
- kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
357
- claim: 'the app navigated to a new route',
358
- counter: 'nothing was rendered for it — no content added or removed, and no request made',
359
- detail: 'the URL moved but the destination produced no content: a route with no view, a view that returned null, or data the page never asked for. A control that navigates always looks alive, so this is invisible to a dead-control check. Confirm by reading the page — a view revealed from DOM that already existed emits this same window',
360
- });
430
+ // A console error in the SAME window turns "nothing rendered" from an absence into a positive
431
+ // claim: the destination did not merely fail to produce content, it crashed while trying to.
432
+ // Reported once as `unknown` when this held — a React hooks error and an empty destination were
433
+ // both in hand, and the honest, definitive answer was available and not given (#897).
434
+ const consoleErrors = events
435
+ .filter((e) => e.type === EventType.CONSOLE_ERROR)
436
+ .map((e) => asString(e.data['message']))
437
+ .filter((m) => undefined !== m && m.length > 0);
438
+ if (consoleErrors.length > 0) {
439
+ found.push({
440
+ kind: ContradictionKind.ROUTE_RENDERED_NOTHING_CRASHED,
441
+ claim: 'the app navigated to a new route',
442
+ counter: `nothing was rendered for it, and the console shows why: ${consoleErrors[0] ?? ''}`,
443
+ detail: `the URL moved but the destination produced no content, and the same window logged ` +
444
+ `${String(consoleErrors.length)} console error(s) — the first: "${consoleErrors[0] ?? ''}". ` +
445
+ 'The destination crashed rather than merely rendering nothing; read the error for the ' +
446
+ 'component and line at fault.',
447
+ });
448
+ }
449
+ else {
450
+ found.push({
451
+ kind: ContradictionKind.ROUTE_RENDERED_NOTHING,
452
+ claim: 'the app navigated to a new route',
453
+ counter: 'nothing was rendered for it — no content added or removed, and no request made',
454
+ detail: 'the URL moved but the destination produced no content: a route with no view, a view that returned null, or data the page never asked for. A control that navigates always looks alive, so this is invisible to a dead-control check. Confirm by reading the page — a view revealed from DOM that already existed emits this same window',
455
+ });
456
+ }
361
457
  }
362
458
  // ── The app claimed success while its own request failed ────────────────────────────────────
363
459
  // A signal is the sharper claim: the app did not merely LOOK right, it explicitly asserted
@@ -524,7 +620,7 @@ function findWindowContradictions(allEvents, options) {
524
620
  * distinguishes them.
525
621
  */
526
622
  const navigatedAt = events.find((e) => EventType.ROUTE_CHANGE === e.type && e.t >= actionSince)?.t;
527
- const writeCounts = new Map();
623
+ const writeTimes = new Map();
528
624
  for (const event of events) {
529
625
  if (event.type !== EventType.NET_REQUEST || event.t < actionSince)
530
626
  continue;
@@ -536,18 +632,54 @@ function findWindowContradictions(allEvents, options) {
536
632
  const label = `${call.method} ${call.url}`;
537
633
  const body = asString(event.data['requestBody']);
538
634
  const key = body === undefined || 0 === body.length ? label : `${label} ${body}`;
539
- const entry = writeCounts.get(key) ?? { label, count: 0 };
540
- entry.count += 1;
541
- writeCounts.set(key, entry);
635
+ const entry = writeTimes.get(key) ?? { label, times: [], landed: 0 };
636
+ entry.times.push(event.t);
637
+ // Counted separately from the occurrences, because the claim is about what APPLIED.
638
+ if (false !== call.ok)
639
+ entry.landed += 1;
640
+ writeTimes.set(key, entry);
542
641
  }
543
- for (const [, { label, count }] of writeCounts) {
544
- if (count < 2)
642
+ /**
643
+ * Did the assertion name this endpoint?
644
+ *
645
+ * `undefined` means the caller declared nothing (a bare `observe`), and with nothing declared
646
+ * nothing is unrelated — every duplicate keeps the behaviour it had. An empty-string entry is a
647
+ * net clause with no `urlContains`, which named the whole channel and therefore matches
648
+ * everything.
649
+ */
650
+ const named = options.namedNetUrls;
651
+ const wasNamed = (label) => named === undefined || named.some((u) => label.includes(u));
652
+ for (const [, { label, times, landed }] of writeTimes) {
653
+ if (times.length < 2)
654
+ continue;
655
+ // A DOUBLE SUBMIT is a write that landed twice. Two attempts of which one failed is a RETRY,
656
+ // and the field case is the commonest retry there is: a 401 that refreshed a token and went
657
+ // again, one row created, reported here as `duplicate-request ×2`. Two attempts that both
658
+ // failed are not a double submit either — nothing applied even once, so the claim "one user
659
+ // action was performed" is not contradicted by them. A call with no verdict at all still
660
+ // counts, because absence of a status is not evidence the write was rejected.
661
+ if (landed < 2)
662
+ continue;
663
+ // A steady cadence is a POLL, and a poll is not a double submit. An app that polls could not
664
+ // produce a verdict at all: a camera scan loop POSTing until it acquires a lock had every
665
+ // assertion that had already seen its consequence come back `unknown` behind writes that
666
+ // were the app working correctly (#673).
667
+ if (isSteadyCadence(times))
545
668
  continue;
669
+ // A burst the assertion never mentioned is still worth telling the caller about -- a retry
670
+ // loop or a bursty beacon is a real finding -- but it is not evidence about the consequence
671
+ // they declared, so it is reported and decides nothing. The named case keeps its downgrade:
672
+ // "the write you asked about fired twice" is exactly what this rule is for.
673
+ const related = wasNamed(label);
546
674
  found.push({
547
- kind: ContradictionKind.DUPLICATE_REQUEST,
548
- claim: 'one user action was performed',
549
- counter: `the same write fired ${String(count)} times`,
550
- detail: `${label} ×${String(count)}`,
675
+ kind: related
676
+ ? ContradictionKind.DUPLICATE_REQUEST
677
+ : ContradictionKind.DUPLICATE_REQUEST_UNRELATED,
678
+ claim: related
679
+ ? 'one user action was performed'
680
+ : 'the assertion did not name this endpoint',
681
+ counter: `the same write fired ${String(times.length)} times`,
682
+ detail: `${label} ×${String(times.length)}`,
551
683
  });
552
684
  }
553
685
  }
@@ -24,11 +24,22 @@ export interface DeclaredNetFailure {
24
24
  urlContains?: string;
25
25
  status?: number;
26
26
  }
27
- export interface DeclaredExpectations {
27
+ interface DeclaredExpectations {
28
28
  /** Requests the caller declared would FAIL, so failing is the expected outcome, not a disagreement. */
29
29
  netFailures: readonly DeclaredNetFailure[];
30
30
  /** The caller required something to be ON SCREEN — an element or text, present rather than absent. */
31
31
  rendersContent: boolean;
32
+ /**
33
+ * The `urlContains` of every net clause the predicate carries, failure or not.
34
+ *
35
+ * Distinct from `netFailures`, which is about EXPECTED failures. This answers a different
36
+ * question: which traffic did the caller actually mention? A rule that reports on writes the
37
+ * assertion never named is reporting on something else, and must not decide this verdict (#673).
38
+ *
39
+ * A net clause with no `urlContains` names ALL traffic — it asked about the whole channel — which
40
+ * is why an empty-string entry is meaningful and is preserved rather than skipped.
41
+ */
42
+ netUrls: readonly string[];
32
43
  }
33
44
  export declare function declaredExpectations(predicate: Predicate | undefined): DeclaredExpectations;
34
45
  /**
@@ -60,3 +71,4 @@ export declare function matchesDeclaredFailure(call: {
60
71
  matchUrl?: string;
61
72
  status: number | undefined;
62
73
  }, declared: readonly DeclaredNetFailure[]): boolean;
74
+ export {};
@@ -17,11 +17,57 @@
17
17
  * opposite of a consequence — honouring either would suppress a real finding on the strength of
18
18
  * something that never happened. Pure: a predicate in, a description out.
19
19
  */
20
- import { PredicateKind } from '@reticlehq/core';
20
+ import { PredicateKind, QueryBy } from '@reticlehq/core';
21
21
  /** Below this, a status is a success or a redirect: not a declared failure. */
22
22
  const FAILURE_STATUS_MIN = 400;
23
+ /** Statuses that mean the caller was refused — the proof of an auth/authz denial, not a fault. */
24
+ const AUTH_DENIAL_STATUSES = [401, 403];
25
+ /** ARIA roles that ARE the error UI — waiting for an alert is how the first report asserted a 401. */
26
+ const DENIAL_ROLES = new Set(['alert', 'alertdialog']);
27
+ /**
28
+ * Phrases that name a denial rather than a success. Conservative and multi-word where a short
29
+ * token would match a greeting ("invalid date" is not an auth failure).
30
+ */
31
+ const DENIAL_TEXT_MARKERS = [
32
+ 'access denied',
33
+ 'unauthorized',
34
+ 'forbidden',
35
+ 'invalid key',
36
+ 'invalid credentials',
37
+ 'incorrect email',
38
+ 'incorrect password',
39
+ 'not authorized',
40
+ 'permission denied',
41
+ 'unauthenticated',
42
+ 'not allowed',
43
+ ];
44
+ function isDenialPhrase(text) {
45
+ const lower = text.toLowerCase();
46
+ return DENIAL_TEXT_MARKERS.some((marker) => lower.includes(marker));
47
+ }
48
+ function isDenialRole(role) {
49
+ return DENIAL_ROLES.has(role.toLowerCase());
50
+ }
51
+ function isDenialQuery(query) {
52
+ if (undefined !== query.role && isDenialRole(query.role))
53
+ return true;
54
+ if (QueryBy.ROLE === query.by && undefined !== query.value && isDenialRole(query.value)) {
55
+ return true;
56
+ }
57
+ for (const field of [query.name, query.text, query.value, query.label]) {
58
+ if ('string' === typeof field && isDenialPhrase(field))
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ function pushAuthDenialStatuses(into) {
64
+ for (const status of AUTH_DENIAL_STATUSES) {
65
+ into.push({ status });
66
+ }
67
+ }
23
68
  export function declaredExpectations(predicate) {
24
69
  const netFailures = [];
70
+ const netUrls = [];
25
71
  let rendersContent = false;
26
72
  const walk = (p) => {
27
73
  switch (p.kind) {
@@ -30,6 +76,7 @@ export function declaredExpectations(predicate) {
30
76
  walk(child);
31
77
  return;
32
78
  case PredicateKind.NET: {
79
+ netUrls.push(p.urlContains ?? '');
33
80
  const declaredFailure = false === p.ok || (p.status !== undefined && p.status >= FAILURE_STATUS_MIN);
34
81
  if (!declaredFailure)
35
82
  return;
@@ -41,9 +88,18 @@ export function declaredExpectations(predicate) {
41
88
  return;
42
89
  }
43
90
  case PredicateKind.ELEMENT:
91
+ if (true !== p.absent) {
92
+ rendersContent = true;
93
+ if (isDenialQuery(p.query))
94
+ pushAuthDenialStatuses(netFailures);
95
+ }
96
+ return;
44
97
  case PredicateKind.TEXT:
45
- if (true !== p.absent)
98
+ if (true !== p.absent) {
46
99
  rendersContent = true;
100
+ if (isDenialPhrase(p.contains))
101
+ pushAuthDenialStatuses(netFailures);
102
+ }
47
103
  return;
48
104
  default:
49
105
  return;
@@ -51,7 +107,7 @@ export function declaredExpectations(predicate) {
51
107
  };
52
108
  if (predicate !== undefined)
53
109
  walk(predicate);
54
- return { netFailures, rendersContent };
110
+ return { netFailures, rendersContent, netUrls };
55
111
  }
56
112
  /**
57
113
  * Did the caller name a consequence that does not depend on the response body?
@@ -23,6 +23,9 @@ import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core'
23
23
  * as a half-applied write. The gate was on the RESPONSE being a success and never on the REQUEST
24
24
  * being a write, so every successful call carrying a body read as a save — and the same key name
25
25
  * legitimately means different things on the two sides of a request/response pair;
26
+ * - identity keys (`id`, `*_id`) and create sentinels (`0`, `""`, `null`) are not compared. A
27
+ * create that sends `sub_category_id: 0` and gets back `19314` assigned the row; two id spaces
28
+ * that share a field name were never expected to match. Both used to fire on every healthy POST;
26
29
  * - only keys the request actually SENT are considered;
27
30
  * - only scalars, since deep structural diffing is where the false positives live;
28
31
  * - values are compared NORMALISED (trimmed, case-folded, numbers as numbers), so `FR` vs `fr` and
@@ -71,6 +74,23 @@ function normalize(value) {
71
74
  return String(value);
72
75
  return undefined;
73
76
  }
77
+ /**
78
+ * Values that mean "server, you decide", not a field the caller asked to persist. A create that
79
+ * sends `0` for the new row's id, or an empty string the backend fills in, is the usual REST shape
80
+ * — not a dropped write.
81
+ */
82
+ function isSentinelValue(value) {
83
+ return 0 === value || '' === value || null === value;
84
+ }
85
+ /**
86
+ * Keys that name an identity, not a persisted attribute. The server assigns these (create returns a
87
+ * new id; a public id and an internal row id share a field name). Comparing them as echoes is how
88
+ * this kind fired on every healthy POST-create and poisoned later asserts in the same window.
89
+ */
90
+ function isIdentityKey(key) {
91
+ const lower = key.toLowerCase();
92
+ return 'id' === lower || lower.endsWith('_id');
93
+ }
74
94
  /**
75
95
  * Every scalar value the response carries for each key, at any depth.
76
96
  *
@@ -78,20 +98,22 @@ function normalize(value) {
78
98
  * `{result:{...}}` — and demanding the same path as the request would miss nearly every real API.
79
99
  * Collecting ALL values for a key is what makes the "echoes both old and new" case safe.
80
100
  */
81
- function scalarsByKey(body, into = new Map(), depth = 0) {
101
+ function scalarsByKey(body, into = new Map(), depth = 0, omitSentinels = false) {
82
102
  if (depth > 6 || into.size > MAX_ECHO_KEYS)
83
103
  return into;
84
104
  if (Array.isArray(body)) {
85
105
  for (const item of body)
86
- scalarsByKey(item, into, depth + 1);
106
+ scalarsByKey(item, into, depth + 1, omitSentinels);
87
107
  return into;
88
108
  }
89
109
  if (!isRecord(body))
90
110
  return into;
91
111
  for (const [key, value] of Object.entries(body)) {
112
+ if (omitSentinels && isSentinelValue(value))
113
+ continue;
92
114
  const scalar = normalize(value);
93
115
  if (scalar === undefined) {
94
- scalarsByKey(value, into, depth + 1);
116
+ scalarsByKey(value, into, depth + 1, omitSentinels);
95
117
  continue;
96
118
  }
97
119
  const seen = into.get(key) ?? new Set();
@@ -117,6 +139,46 @@ const EchoDiscriminator = {
117
139
  * A response carrying this many times more scalar keys than the request, overlapping on fewer than
118
140
  * `MIN_SHARED_KEYS`, is a snapshot that happened to reuse a name — not an echo of the write.
119
141
  */
142
+ /**
143
+ * The two halves of an ADDRESSED write: a key that NAMES the field to set, and a generic container
144
+ * holding the value to set it to — `{"slot":"_dim","value":"3D"}`, `{"field":"locale","value":"fr"}`.
145
+ *
146
+ * A container key cannot be compared across documents by name. `value` in the request means "the
147
+ * value of the field named by `slot`"; `value` in the response means "the value of whichever field
148
+ * this node belongs to", and a document holding many addressed fields holds many `value` nodes. The
149
+ * flat by-key comparison below therefore measures the request's value against an unrelated sibling
150
+ * field's value, which is not evidence about the target field at all.
151
+ *
152
+ * Measured in the field, and it produced a FALSE RED on a passing assertion: the request asked
153
+ * `{"slot":"_dim","value":"3D"}`; the server applied it (`brief.dim: "3D"`, and the interview
154
+ * advanced from `{kind:"question"}` to `{kind:"geometry", payload:{dim:"3D"}}`); and this kind
155
+ * reported "asked 3d, got cfd", where `cfd` is `brief.slots._family.value` — a different slot,
156
+ * set earlier in the same interview. The next call on the same endpoint DID mirror into
157
+ * `slots.<slot>.value` and passed clean, so the kind misfires exactly when a server persists an
158
+ * addressed field somewhere other than a mirror named after it. That is a normal API shape.
159
+ *
160
+ * Cost of getting this wrong: a manufactured red is worse than a missed one here. Reticle's whole
161
+ * proposition is that an agent must not talk itself past a red; a heuristic that invents them
162
+ * teaches, inside a single session, that a `contradicted` verdict may be noise worth arguing with.
163
+ *
164
+ * The addressing key itself is still compared. If the server echoes a DIFFERENT `slot` than the one
165
+ * requested, that is a genuine dropped write and this file should say so.
166
+ */
167
+ const ADDRESSING_KEYS = new Set(['slot', 'field', 'key', 'path', 'property']);
168
+ const CONTAINER_KEYS = new Set(['value', 'val', 'newvalue']);
169
+ /** True when the request says "set the field named X" rather than carrying the fields themselves. */
170
+ function isAddressedWrite(asked) {
171
+ let addressed = false;
172
+ let container = false;
173
+ for (const key of asked.keys()) {
174
+ const lower = key.toLowerCase();
175
+ if (ADDRESSING_KEYS.has(lower))
176
+ addressed = true;
177
+ if (CONTAINER_KEYS.has(lower))
178
+ container = true;
179
+ }
180
+ return addressed && container;
181
+ }
120
182
  const SNAPSHOT_KEY_RATIO = 3;
121
183
  const MIN_SHARED_KEYS = 2;
122
184
  /**
@@ -194,16 +256,23 @@ export function findEchoMismatches(events, actionSince) {
194
256
  if (request === undefined || response === undefined)
195
257
  continue;
196
258
  const echoed = scalarsByKey(response);
197
- const asked = scalarsByKey(request);
259
+ const asked = scalarsByKey(request, new Map(), 0, true);
198
260
  if (!responseRestatesRequest(asked, echoed))
199
261
  continue;
262
+ // See ADDRESSING_KEYS: on an addressed write the container key names nothing on its own, so
263
+ // comparing it by name across the two documents compares two different fields.
264
+ const addressed = isAddressedWrite(asked);
200
265
  const dropped = [];
201
266
  for (const [key, wanted] of asked) {
267
+ if (addressed && CONTAINER_KEYS.has(key.toLowerCase()))
268
+ continue;
202
269
  // More than one requested value for a key (a before/after pair, a list of items) makes "what
203
270
  // was asked for" ambiguous, and a guess here is exactly how this kind would earn a reputation
204
271
  // for crying wolf.
205
272
  if (wanted.size !== 1)
206
273
  continue;
274
+ if (isIdentityKey(key))
275
+ continue;
207
276
  const values = echoed.get(key);
208
277
  // Not echoed at all = no evidence either way. Only a key the server chose to report back can
209
278
  // contradict the request, and silence is not a contradiction.
@@ -222,8 +291,8 @@ export function findEchoMismatches(events, actionSince) {
222
291
  found.push({
223
292
  kind: ContradictionKind.WRITE_FIELD_IGNORED,
224
293
  claim: 'the write returned success and the page treated it as saved',
225
- counter: `its own echo shows ${String(dropped.length)} field(s) NOT applied — ${dropped.join('; ')}`,
226
- detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set, so the write half-applied; the UI has no way to know and will show the value the user typed${attribution}`,
294
+ counter: `its own echo shows ${String(dropped.length)} field(s) that differ from the request — ${dropped.join('; ')}`,
295
+ detail: `${method} ${url} — the server answered OK and returned a different value than it was asked to set${attribution}`,
227
296
  });
228
297
  }
229
298
  return found;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Where a runtime value came from: state path ← the signal that preceded it ← the request behind it.
3
+ *
4
+ * The prior art this issue names (`firatorhan/sentinel`'s `get_lineage`) builds the same chain by
5
+ * picking "the most recent action whose payload contains this value", then "an API call whose type
6
+ * matches". That is CORRELATION, and for a devtool a human reads it is the right trade — the reader
7
+ * discounts it on sight.
8
+ *
9
+ * Reticle's output is read by an agent that acts on it, so the same chain presented as fact is the
10
+ * false-green shape this project exists to refuse. The join here is deliberately no cleverer than
11
+ * theirs; what is different is that every link carries how much it is worth:
12
+ *
13
+ * - a state path that changed is OBSERVED — we have the event;
14
+ * - "this signal caused it" is INFERRED, and the wording says so;
15
+ * - several candidates are NAMED rather than narrowed to the most recent, because choosing is the
16
+ * one thing a correlation cannot justify;
17
+ * - no candidate says the value was not seen to pass through one, and reaches for nothing.
18
+ *
19
+ * Read-only. Produces no verdict, is not reachable from an assert path, and changes nothing.
20
+ */
21
+ import { type ReticleEvent } from '@reticlehq/core';
22
+ /** What the caller is asking about. `value` narrows a path that changed more than once. */
23
+ export interface LineageQuery {
24
+ path: string;
25
+ value?: string;
26
+ }
27
+ /** One line of the chain, with the confidence that produced it. */
28
+ export interface LineageLink {
29
+ /** Which stream this came from. */
30
+ kind: 'state' | 'signal' | 'net';
31
+ /** The rendered line, already carrying its own hedge. */
32
+ text: string;
33
+ /** True only when this is an event we hold. A join is never observed. */
34
+ observed: boolean;
35
+ /** Every candidate, when more than one could explain the link. Present only when ambiguous. */
36
+ candidates?: string[];
37
+ }
38
+ export interface Lineage {
39
+ /** False when the path was never seen to change — a different answer from an empty chain. */
40
+ found: boolean;
41
+ chain: LineageLink[];
42
+ /** Why the chain stops where it does, when it stops early. */
43
+ note?: string;
44
+ }
45
+ /**
46
+ * The chain, most recent change first.
47
+ *
48
+ * Stops at the first link with nothing to point at, and says why. Walking past a gap would mean
49
+ * joining across a hole in the evidence, which is exactly the step that turns a correlation into a
50
+ * claim.
51
+ */
52
+ export declare function traceLineage(events: readonly ReticleEvent[], query: LineageQuery): Lineage;