iterate 0.2.7 → 0.3.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 (218) hide show
  1. package/README.md +86 -76
  2. package/THIRD_PARTY_NOTICES.md +55 -0
  3. package/bin/iterate.js +11 -3
  4. package/dist/api-url-B6404M82.mjs +17 -0
  5. package/dist/api-url-B6404M82.mjs.map +1 -0
  6. package/dist/app-ref-BipL0feU.mjs +35 -0
  7. package/dist/app-ref-BipL0feU.mjs.map +1 -0
  8. package/dist/app-ref-C1CrgXqX.mjs +7 -0
  9. package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
  10. package/dist/app-ref-DYai_om1.mjs +7 -0
  11. package/dist/app-ref-DYai_om1.mjs.map +1 -0
  12. package/dist/cli-D0c-pDL_.mjs +1010 -0
  13. package/dist/cli-D0c-pDL_.mjs.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.mjs +4 -0
  16. package/dist/cloudflare-BTm90gQ4.mjs +951 -0
  17. package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
  18. package/dist/contract-s4FW4eES.mjs +309 -0
  19. package/dist/contract-s4FW4eES.mjs.map +1 -0
  20. package/dist/document-review/index.d.ts +5 -0
  21. package/dist/document-review/types.d.ts +107 -0
  22. package/dist/document-review.mjs +7015 -0
  23. package/dist/document-review.mjs.map +1 -0
  24. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
  25. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
  26. package/dist/idempotency-DleloJNt.mjs +28 -0
  27. package/dist/idempotency-DleloJNt.mjs.map +1 -0
  28. package/dist/index.mjs +1 -1
  29. package/dist/itx/api-url.d.ts +6 -0
  30. package/dist/itx/itx-node-client.d.ts +65 -0
  31. package/dist/itx/itx-session.d.ts +215 -0
  32. package/dist/itx/owned-rpc-session.d.ts +14 -0
  33. package/dist/itx/query-client.d.ts +10 -0
  34. package/dist/itx-api.generated.d.ts +6195 -0
  35. package/dist/itx-session-sjud8GiT.mjs +534 -0
  36. package/dist/itx-session-sjud8GiT.mjs.map +1 -0
  37. package/dist/live-state-BJNqOwFw.mjs +299 -0
  38. package/dist/live-state-BJNqOwFw.mjs.map +1 -0
  39. package/dist/next/api.d.ts +479 -0
  40. package/dist/next/api.mjs +0 -0
  41. package/dist/next/app-server.d.ts +44 -0
  42. package/dist/next/app-server.mjs +479 -0
  43. package/dist/next/app-server.mjs.map +1 -0
  44. package/dist/next/app-session.d.ts +49 -0
  45. package/dist/next/app-session.mjs +238 -0
  46. package/dist/next/app-session.mjs.map +1 -0
  47. package/dist/next/app.d.ts +29 -0
  48. package/dist/next/app.mjs +141 -0
  49. package/dist/next/app.mjs.map +1 -0
  50. package/dist/next/client/live-state.d.ts +63 -0
  51. package/dist/next/client/oauth.d.ts +12 -0
  52. package/dist/next/client/react.d.ts +109 -0
  53. package/dist/next/client/socket.d.ts +6 -0
  54. package/dist/next/client.mjs +156 -0
  55. package/dist/next/client.mjs.map +1 -0
  56. package/dist/next/expression.d.ts +146 -0
  57. package/dist/next/expression.mjs +399 -0
  58. package/dist/next/expression.mjs.map +1 -0
  59. package/dist/next/lib.d.ts +56 -0
  60. package/dist/next/lib.mjs +199 -0
  61. package/dist/next/lib.mjs.map +1 -0
  62. package/dist/next/oauth-scopes.d.ts +32 -0
  63. package/dist/next/oauth-scopes.mjs +40 -0
  64. package/dist/next/oauth-scopes.mjs.map +1 -0
  65. package/dist/next/oauth.mjs +29 -0
  66. package/dist/next/oauth.mjs.map +1 -0
  67. package/dist/next/principal.d.ts +64 -0
  68. package/dist/next/principal.mjs +98 -0
  69. package/dist/next/principal.mjs.map +1 -0
  70. package/dist/next/project-ingress.d.ts +37 -0
  71. package/dist/next/project-ingress.mjs +75 -0
  72. package/dist/next/project-ingress.mjs.map +1 -0
  73. package/dist/next/react.mjs +285 -0
  74. package/dist/next/react.mjs.map +1 -0
  75. package/dist/next/sdk/auth.d.ts +5 -0
  76. package/dist/next/sdk/index.d.ts +112 -0
  77. package/dist/next/sdk.mjs +139 -0
  78. package/dist/next/sdk.mjs.map +1 -0
  79. package/dist/next/stream/processor.d.ts +378 -0
  80. package/dist/next/stream/processor.mjs +582 -0
  81. package/dist/next/stream/processor.mjs.map +1 -0
  82. package/dist/next/stream/run.d.ts +58 -0
  83. package/dist/next/stream/run.mjs +40 -0
  84. package/dist/next/stream/run.mjs.map +1 -0
  85. package/dist/next-node.d.ts +15 -0
  86. package/dist/next-node.mjs +51 -0
  87. package/dist/next-node.mjs.map +1 -0
  88. package/dist/node.d.ts +3 -0
  89. package/dist/node.mjs +185 -0
  90. package/dist/node.mjs.map +1 -0
  91. package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
  92. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
  93. package/dist/processors/cloudflare.d.ts +3 -0
  94. package/dist/processors/durable-object-processor-durability.d.ts +79 -0
  95. package/dist/processors/event-consumption-metrics.d.ts +82 -0
  96. package/dist/processors/idempotency.d.ts +13 -0
  97. package/dist/processors/index.d.ts +12 -0
  98. package/dist/processors/processor-contracts.d.ts +342 -0
  99. package/dist/processors/processor-facet.d.ts +186 -0
  100. package/dist/processors/processor-host-capabilities.d.ts +60 -0
  101. package/dist/processors/prompt-sections.d.ts +17 -0
  102. package/dist/processors/rpc-types.d.ts +515 -0
  103. package/dist/processors/schemas.d.ts +102 -0
  104. package/dist/processors/stream-handle.d.ts +45 -0
  105. package/dist/processors/stream-processor-keepalive.d.ts +95 -0
  106. package/dist/processors/stream-processor-registry.d.ts +233 -0
  107. package/dist/processors/stream-processor-runner.d.ts +289 -0
  108. package/dist/processors/stream-processor.d.ts +339 -0
  109. package/dist/processors/stream-runtime-metrics.d.ts +107 -0
  110. package/dist/processors/testing.d.ts +302 -0
  111. package/dist/processors-BoNyeBfQ.mjs +10 -0
  112. package/dist/processors-BoNyeBfQ.mjs.map +1 -0
  113. package/dist/processors-cloudflare.mjs +3 -0
  114. package/dist/processors-testing.mjs +435 -0
  115. package/dist/processors-testing.mjs.map +1 -0
  116. package/dist/processors.mjs +52 -0
  117. package/dist/processors.mjs.map +1 -0
  118. package/dist/protocol-DnK_f2m6.mjs +251 -0
  119. package/dist/protocol-DnK_f2m6.mjs.map +1 -0
  120. package/dist/sdk/capnweb/index.d.ts +2 -0
  121. package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
  122. package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
  123. package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
  124. package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
  125. package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
  126. package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
  127. package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
  128. package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
  129. package/dist/sdk/capnweb/react.d.ts +45 -0
  130. package/dist/sdk/capnweb/react.mjs +316 -0
  131. package/dist/sdk/capnweb/react.mjs.map +1 -0
  132. package/dist/sdk/capnweb.mjs +4 -0
  133. package/dist/sdk/itx/react.d.ts +191 -0
  134. package/dist/sdk/itx/react.mjs +383 -0
  135. package/dist/sdk/itx/react.mjs.map +1 -0
  136. package/dist/sdk-DMB-IM11.mjs +933 -0
  137. package/dist/sdk-DMB-IM11.mjs.map +1 -0
  138. package/dist/sdk.d.ts +339 -0
  139. package/dist/sdk.mjs +2 -0
  140. package/dist/serve-itx.d.ts +46 -0
  141. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
  142. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
  143. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
  144. package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
  145. package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
  146. package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
  147. package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
  148. package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
  149. package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
  150. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
  151. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
  152. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
  153. package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
  154. package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
  155. package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
  156. package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
  157. package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
  158. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
  159. package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
  160. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
  161. package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
  162. package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
  163. package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
  164. package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
  165. package/dist/starter-apps/guestbook/client.d.ts +7 -0
  166. package/dist/starter-apps/guestbook/client.mjs +59 -0
  167. package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
  168. package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
  169. package/dist/starter-apps/guestbook/index.d.ts +9 -0
  170. package/dist/starter-apps/guestbook/index.mjs +31 -0
  171. package/dist/starter-apps/guestbook/index.mjs.map +1 -0
  172. package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
  173. package/dist/starter-apps/guestbook/worker.d.ts +26 -0
  174. package/dist/starter-apps/guestbook/worker.mjs +191 -0
  175. package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
  176. package/dist/starter-apps/media/configured-worker.mjs +577 -0
  177. package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
  178. package/dist/starter-apps/media/index.mjs +36 -0
  179. package/dist/starter-apps/media/index.mjs.map +1 -0
  180. package/dist/starter-apps/media/ref.mjs +20 -0
  181. package/dist/starter-apps/media/ref.mjs.map +1 -0
  182. package/dist/starter-apps/media/worker.mjs +579 -0
  183. package/dist/starter-apps/media/worker.mjs.map +1 -0
  184. package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
  185. package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
  186. package/dist/starter-apps/notes/index.mjs +23 -0
  187. package/dist/starter-apps/notes/index.mjs.map +1 -0
  188. package/dist/starter-apps/notes/ref.mjs +21 -0
  189. package/dist/starter-apps/notes/ref.mjs.map +1 -0
  190. package/dist/starter-apps/notes/worker.mjs +427 -0
  191. package/dist/starter-apps/notes/worker.mjs.map +1 -0
  192. package/dist/starter-apps/todo/client.mjs +59 -0
  193. package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
  194. package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
  195. package/dist/starter-apps/todo/index.d.ts +8 -0
  196. package/dist/starter-apps/todo/index.mjs +29 -0
  197. package/dist/starter-apps/todo/index.mjs.map +1 -0
  198. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
  199. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
  200. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  201. package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
  202. package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
  203. package/dist/worker-ref-DZxPDmb_.mjs +390 -0
  204. package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
  205. package/menubar/Iterate.entitlements +12 -0
  206. package/menubar/Iterate.swift +914 -0
  207. package/menubar/IterateIcon.swift +145 -0
  208. package/menubar/README.md +28 -0
  209. package/menubar/build-menubar-app.sh +59 -0
  210. package/package.json +235 -18
  211. package/dist/cli-DMS4kJph.mjs +0 -868
  212. package/dist/cli-DMS4kJph.mjs.map +0 -1
  213. package/dist/config-DtnR7Lv7.mjs +0 -170
  214. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  215. package/dist/index.d.mts.map +0 -1
  216. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  217. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  218. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
@@ -0,0 +1,1055 @@
1
+ import { _ as object, b as url, d as _enum, f as array, g as number, h as literal, l as defineProcessorContract, m as discriminatedUnion, o as StreamProcessor, r as StreamProcessorDurableObject, t as _usingCtx, u as ZodISODateTime, v as record, x as _isoDateTime, y as string } from "../../usingCtx-mZx5nsAW.mjs";
2
+ import { t as isIdempotencyConflict } from "../../processors-BoNyeBfQ.mjs";
3
+ //#region ../../node_modules/.pnpm/zod@4.5.4_patch_hash=bf04bb1ec1f0b9f31a7416bf98f83d3451e5011852877ca0ac19c12fcb3129b5/node_modules/zod/v4/classic/iso.js
4
+ function datetime(params) {
5
+ return _isoDateTime(ZodISODateTime, params);
6
+ }
7
+ //#endregion
8
+ //#region src/starter-apps/flake-dashboard/app-ref.ts
9
+ const flakesStreamPath = "/flakes";
10
+ /**
11
+ * The dashboard's birth certificate. Idempotency-keyed so every caller — the
12
+ * worker's lazy initialization AND the CI reporter script, whichever appends
13
+ * first — may offer it; the config values are immutable birth facts.
14
+ */
15
+ function flakeDashboardCreationEvents(config) {
16
+ return [{
17
+ type: "events.iterate.com/flakes/created",
18
+ payload: { config },
19
+ idempotencyKey: "flakes/created"
20
+ }];
21
+ }
22
+ //#endregion
23
+ //#region ../shared/src/test-support/flake-suite-summary.ts
24
+ /** A full CI suite publishes this even when its flake record list is empty. */
25
+ const FlakeSuiteSummary = object({
26
+ headSha: string().min(1),
27
+ branch: string(),
28
+ status: _enum(["complete", "incomplete"]),
29
+ startedAt: datetime(),
30
+ finishedAt: datetime(),
31
+ testCount: number().int().nonnegative(),
32
+ tests: array(object({
33
+ name: string().min(1),
34
+ outcome: _enum([
35
+ "pass",
36
+ "fail",
37
+ "skip"
38
+ ])
39
+ })).optional(),
40
+ unknownFlakeCount: number().int().nonnegative(),
41
+ failedCount: number().int().nonnegative(),
42
+ diagnostics: array(string()),
43
+ runUrl: url()
44
+ }).refine((summary) => summary.status !== "complete" || summary.testCount > 0 && !!summary.branch && summary.diagnostics.length === 0, "A complete suite needs executed tests and no missing-result diagnostics");
45
+ //#endregion
46
+ //#region src/starter-apps/flake-dashboard/contract.ts
47
+ const flakeEventTypes = {
48
+ created: "events.iterate.com/flakes/created",
49
+ runRecorded: "events.iterate.com/flakes/run-recorded",
50
+ transitionProposed: "events.iterate.com/flakes/transition-proposed",
51
+ dashboardRenderSettled: "events.iterate.com/flakes/dashboard-render-settled"
52
+ };
53
+ const StreamOffset = number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
54
+ /**
55
+ * One `createFlake` test outcome. Mirrors `FlakeRecord` in
56
+ * `@iterate-com/shared/test-support/flake-test` — the wrapper writes these
57
+ * lines to `FLAKE_RECORD_DIR` and CI ships them here verbatim.
58
+ */
59
+ const FlakeOutcome = _enum([
60
+ "pass",
61
+ "flake-fail",
62
+ "unexpected-error",
63
+ "pinned-fail",
64
+ "unexpected-pass",
65
+ "retried-pass"
66
+ ]);
67
+ /**
68
+ * Which wrapper (or reporter) produced a record: createFlake, createFailing,
69
+ * or the telemetry reporters' retried-pass records for tests nobody has
70
+ * classified yet. Old events predate the field — they were all createFlake.
71
+ */
72
+ const FlakeKind = _enum([
73
+ "flake",
74
+ "failing",
75
+ "unknown"
76
+ ]);
77
+ const FlakeRecord = object({
78
+ name: string().min(1).max(1e3),
79
+ kind: FlakeKind.default("flake"),
80
+ outcome: FlakeOutcome,
81
+ pattern: string().min(1).max(2e3).optional(),
82
+ durationMs: number().nonnegative(),
83
+ at: string().min(1),
84
+ error: string().max(4e3).optional()
85
+ });
86
+ const FlakeDashboardConfig = object({
87
+ repository: object({
88
+ owner: string().min(1),
89
+ repo: string().min(1)
90
+ }),
91
+ issueTitle: string().min(1).max(200),
92
+ defaultBranch: string().min(1)
93
+ });
94
+ const FlakeRunRecorded = object({
95
+ runId: string().min(1).max(200),
96
+ suite: string().min(1).max(200),
97
+ branch: string().min(1).max(500),
98
+ commit: string().min(1).max(100),
99
+ records: array(FlakeRecord).max(1e4),
100
+ summary: FlakeSuiteSummary.optional()
101
+ });
102
+ const MainSuiteRun = object({
103
+ runId: string(),
104
+ commit: string(),
105
+ summary: FlakeSuiteSummary
106
+ });
107
+ const FlakeTransition = _enum([
108
+ "unwrap",
109
+ "switch-to-failing",
110
+ "unwrap-failing"
111
+ ]);
112
+ const FlakeTransitionProposed = object({
113
+ testName: string().min(1),
114
+ transition: FlakeTransition,
115
+ evidence: object({
116
+ consecutiveRuns: number().int().positive(),
117
+ firstAt: string().min(1),
118
+ lastAt: string().min(1)
119
+ })
120
+ });
121
+ const FlakeDashboardRenderResult = discriminatedUnion("status", [object({
122
+ status: literal("succeeded"),
123
+ issueNumber: number().int().positive(),
124
+ issueUrl: string().url()
125
+ }), object({
126
+ status: literal("failed"),
127
+ error: string().min(1).max(8e3)
128
+ })]);
129
+ /**
130
+ * A run of consecutive same-outcome recorded runs on the default branch.
131
+ * `unexpected-error` outcomes and outcome changes reset it. The streak is the
132
+ * whole input to transition proposals, so it lives in reduced state rather
133
+ * than being recomputed from history.
134
+ */
135
+ const DefaultBranchStreak = object({
136
+ outcome: FlakeOutcome,
137
+ runs: number().int().positive(),
138
+ firstAt: string().min(1),
139
+ lastAt: string().min(1)
140
+ });
141
+ const TrackedTest = object({
142
+ /** Latest record's kind wins: adopting an unknown flake into createFlake migrates its row. */
143
+ kind: FlakeKind.default("flake"),
144
+ /** Empty for kind "unknown" — those rows show error samples instead. */
145
+ pattern: string().default(""),
146
+ suites: array(string()).default([]),
147
+ /**
148
+ * Per suite, the offset of the newest run-recorded event (any branch) whose
149
+ * records included this test. Compared against the suite's
150
+ * `recentRunOffsets` window at render time: a test absent from all of its
151
+ * suite's last few ingested runs is retired from the table — hidden, not
152
+ * deleted, so a transient absence self-heals on the next record.
153
+ */
154
+ lastSeenOffset: record(string(), StreamOffset).default({}),
155
+ /**
156
+ * The latest (up to) 10 outcomes on any branch, ordered by test time — the
157
+ * render's emoji streak bar, each entry carrying the commit that produced
158
+ * it so the square can link straight to that commit's checks. All branches,
159
+ * like the counts, for debugging PR failures too. The numeric
160
+ * defaultBranchStreak below stays main-only and carries the
161
+ * transition-threshold counts past what 10 entries can show.
162
+ */
163
+ recent: array(object({
164
+ outcome: FlakeOutcome,
165
+ commit: string().min(1).max(100),
166
+ at: string()
167
+ })).max(10).default([]),
168
+ /** Keyed by outcome value ("pass", "pinned-fail", …). */
169
+ counts: record(string(), number().int().nonnegative()).default({}),
170
+ /**
171
+ * The last few error samples worth showing (retried-pass and
172
+ * unexpected-error records) — for unknown flakes these are the copy-paste
173
+ * material for the createFlake pattern.
174
+ */
175
+ recentErrors: array(object({
176
+ error: string().max(4e3),
177
+ commit: string(),
178
+ at: string()
179
+ })).max(3).default([]),
180
+ lastFlakeAt: string().nullable().default(null),
181
+ firstRecordedAt: string(),
182
+ lastRecordedAt: string(),
183
+ defaultBranchStreak: DefaultBranchStreak.nullable().default(null),
184
+ /** Older retry evidence cannot undo a wrapper observed on main in this suite. */
185
+ lastMainWrapperAt: record(string(), string()).default({}),
186
+ /**
187
+ * One entry per proposal already made, keyed `${transition}:${streak.firstAt}`
188
+ * — a streak proposes at most once however long it grows.
189
+ */
190
+ proposed: array(string()).default([])
191
+ });
192
+ const FlakeDashboardState = object({
193
+ birthCertificate: object({ config: FlakeDashboardConfig }).nullable().default(null),
194
+ tests: record(string(), TrackedTest).default({}),
195
+ /**
196
+ * Main-only unknowns, isolated by suite. Healthy rows are hidden, retaining
197
+ * their streak so late failures can correct an apparent recovery.
198
+ */
199
+ unknownFlakes: record(string(), record(string(), object({
200
+ record: FlakeRecord,
201
+ passStreak: number().int().nonnegative(),
202
+ /** Newest run that actually contained this test, including skips. */
203
+ lastRunAt: string(),
204
+ recent: array(object({
205
+ outcome: FlakeOutcome,
206
+ commit: string(),
207
+ at: string()
208
+ })).max(10)
209
+ }))).default({}),
210
+ mainRuns: record(string(), object({
211
+ latest: MainSuiteRun,
212
+ complete: MainSuiteRun.nullable(),
213
+ inventory: object({
214
+ startedAt: datetime(),
215
+ names: array(string())
216
+ }).nullable().default(null)
217
+ })).default({}),
218
+ /**
219
+ * The newest three ingested results across all branches retire absent
220
+ * tracked tests. This preserves visibility during partial PR runs. Unknown
221
+ * flakes retire after 20 main passes, wrapper adoption, or proven absence
222
+ * from their suite's full main inventory.
223
+ */
224
+ suites: record(string(), object({ recentRunOffsets: array(StreamOffset).max(3).default([]) })).default({}),
225
+ /**
226
+ * Offset of the newest reduced DATA event (created / run-recorded /
227
+ * transition-proposed). Render settlements deliberately do not bump it —
228
+ * that is what stops a settled render from demanding another render.
229
+ */
230
+ lastDataOffset: StreamOffset.default(0),
231
+ render: object({
232
+ throughOffset: StreamOffset,
233
+ issueNumber: number().int().positive().nullable(),
234
+ issueUrl: string().nullable()
235
+ }).nullable().default(null)
236
+ });
237
+ /**
238
+ * Suggest unwrapping createFlake after 20 consecutive main passes, regardless
239
+ * of elapsed time. Unknown flakes retire automatically at the same count.
240
+ * Propose createFailing after 25 matched failures over >=2 days.
241
+ * Tunable constants. Sentinel tests are excluded from proposals entirely —
242
+ * they are designed to flake.
243
+ */
244
+ const flakeTransitionThresholds = {
245
+ unwrap: {
246
+ runs: 20,
247
+ minSpanMs: 0
248
+ },
249
+ "switch-to-failing": {
250
+ runs: 25,
251
+ minSpanMs: 2880 * 60 * 1e3
252
+ },
253
+ "unwrap-failing": {
254
+ runs: 10,
255
+ minSpanMs: 2880 * 60 * 1e3
256
+ }
257
+ };
258
+ /**
259
+ * The exact shape of a delivered event this app ingests: a platform-verified
260
+ * GitHub webhook for a COMPLETED check_run on a connection stream. This
261
+ * repo's test suites run on Depot CI, whose jobs are GitHub check runs (NOT
262
+ * GitHub Actions workflow runs — `workflow_run` never fires for them). Zod
263
+ * strips the rest of GitHub's (huge) payload; only these fields are read.
264
+ */
265
+ const CheckRunWebhookEvent = object({
266
+ type: literal("events.iterate.com/github/webhook-received"),
267
+ path: string().regex(/^\/integrations\/github\/[^/]+$/),
268
+ payload: object({
269
+ delivery: object({ name: literal("check_run") }),
270
+ body: object({
271
+ action: literal("completed"),
272
+ check_run: object({
273
+ conclusion: _enum(["success", "failure"]),
274
+ head_sha: string().min(1),
275
+ check_suite: object({ head_branch: string().nullish() }).optional()
276
+ }),
277
+ repository: object({
278
+ name: string().min(1),
279
+ owner: object({ login: string().min(1) }),
280
+ default_branch: string().nullish()
281
+ })
282
+ })
283
+ })
284
+ });
285
+ const FlakeDashboardProcessorContract = defineProcessorContract({
286
+ slug: "flake-dashboard",
287
+ version: "0.8.0",
288
+ description: "Folds createFlake test outcomes reported by CI into per-test flake stats, renders the GitHub 'Flake dashboard' issue, and proposes data-provable lifecycle transitions.",
289
+ stateSchema: FlakeDashboardState,
290
+ events: {
291
+ [flakeEventTypes.created]: {
292
+ description: "The flake dashboard exists: target repository, issue title, and which branch counts for transitions.",
293
+ payloadSchema: object({ config: FlakeDashboardConfig }),
294
+ examples: [{
295
+ description: "The dogfood dashboard for this repo.",
296
+ payload: { config: {
297
+ repository: {
298
+ owner: "iterate",
299
+ repo: "iterate"
300
+ },
301
+ issueTitle: "Flake dashboard",
302
+ defaultBranch: "main"
303
+ } }
304
+ }]
305
+ },
306
+ [flakeEventTypes.runRecorded]: {
307
+ description: "One CI run+suite's createFlake outcomes, appended by the CI reporter script (never by this processor). Idempotency-keyed on run+suite so CI retries cannot double-count.",
308
+ payloadSchema: FlakeRunRecorded
309
+ },
310
+ [flakeEventTypes.transitionProposed]: {
311
+ description: "A test's default-branch streak crossed a lifecycle threshold; downstream automation (an agent) should open the unwrap / switch-to-failing PR. The file-edit guard (a streak is only trustworthy if the test file did not change during it) is applied by that agent, which can read git history — the fold cannot.",
312
+ payloadSchema: FlakeTransitionProposed
313
+ },
314
+ [flakeEventTypes.dashboardRenderSettled]: {
315
+ description: "Terminal fact for one dashboard render attempt: the GitHub issue reached (or the error), and the data offset it covered.",
316
+ payloadSchema: object({
317
+ throughOffset: StreamOffset,
318
+ result: FlakeDashboardRenderResult
319
+ })
320
+ }
321
+ },
322
+ consumes: [...Object.values(flakeEventTypes)],
323
+ emits: [flakeEventTypes.transitionProposed, flakeEventTypes.dashboardRenderSettled]
324
+ });
325
+ //#endregion
326
+ //#region src/starter-apps/flake-dashboard/worker.ts
327
+ const ARTIFACT_PREFIX = "flake-records-";
328
+ const MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
329
+ /**
330
+ * The whole app is this worker: everything is processEvent and reduce.
331
+ * `FlakeDashboardApp.processEvent` ingests workflow_run webhooks (CI's
332
+ * credential-free reporting signal) into run-recorded events and wakes the
333
+ * processor for /flakes events; `FlakeDashboardProcessor.reduce` folds them;
334
+ * the processor's own processEvent drives the issue render and transition
335
+ * proposals. No HTTP is served.
336
+ */
337
+ var FlakeDashboardApp = class extends StreamProcessorDurableObject {
338
+ streamPath = flakesStreamPath;
339
+ recovery = true;
340
+ createProcessor(deps) {
341
+ return new FlakeDashboardProcessor({
342
+ ...deps,
343
+ renderDashboard: async (state) => {
344
+ try {
345
+ var _usingCtx$1 = _usingCtx();
346
+ return await renderFlakeDashboardIssue(_usingCtx$1.u(await this.env.ITX.get()), state);
347
+ } catch (_) {
348
+ _usingCtx$1.e = _;
349
+ } finally {
350
+ _usingCtx$1.d();
351
+ }
352
+ }
353
+ });
354
+ }
355
+ /**
356
+ * Every flake-relevant event lands here — the same override point the
357
+ * platform's own delivery uses (widened public so the config worker can
358
+ * forward events over RPC). A /flakes event is a wake hint: catch-up
359
+ * re-reads committed events from the stream and owns validation, ordering,
360
+ * checkpointing, and dedupe.
361
+ */
362
+ async processEvent(event, options) {
363
+ const webhook = CheckRunWebhookEvent.safeParse(event);
364
+ if (webhook.success) {
365
+ if (!options?.depot) return;
366
+ try {
367
+ await this.#ingestCheckRunFlakeArtifacts(webhook.data, options.depot);
368
+ } catch (error) {
369
+ console.error(`[flake-ingest] ingestion failed for sha ${webhook.data.payload.body.check_run.head_sha} (records dropped):`, error);
370
+ }
371
+ return;
372
+ }
373
+ if (event.path !== "/flakes") return;
374
+ await (await this.registry()).catchUp("flake-dashboard");
375
+ }
376
+ /**
377
+ * Artifact-pull ingestion for flake telemetry. This repo's suites run on
378
+ * Depot CI, so a completed check_run is the signal: resolve the Depot run
379
+ * by commit sha, list its `flake-records-<suite>` artifacts, fetch each
380
+ * via a short-lived signed URL, and append one run-recorded event per
381
+ * artifact to /flakes. CI holds no iterate credential; the Depot read
382
+ * token is a platform-held project secret. Any check_run completing on the
383
+ * same Depot run re-scans it — repeated appends dedupe on the
384
+ * run+attempt+suite idempotency key, so redeliveries and sibling checks
385
+ * are harmless.
386
+ */
387
+ async #ingestCheckRunFlakeArtifacts(webhook, depot) {
388
+ try {
389
+ var _usingCtx3 = _usingCtx();
390
+ const { check_run: checkRun, repository } = webhook.payload.body;
391
+ const params = {
392
+ owner: repository.owner.login,
393
+ repo: repository.name
394
+ };
395
+ const itx = _usingCtx3.u(await this.env.ITX.get());
396
+ const token = await itx.secrets.get(depot.secretPath).reveal();
397
+ const depotRpc = async (method, body) => {
398
+ const response = await fetch(`https://api.depot.dev/depot.ci.v1.CIService/${method}`, {
399
+ method: "POST",
400
+ headers: {
401
+ "content-type": "application/json",
402
+ authorization: `Bearer ${token}`,
403
+ "x-depot-org": depot.orgId
404
+ },
405
+ body: JSON.stringify(body)
406
+ });
407
+ if (!response.ok) throw new Error(`depot ${method} failed: HTTP ${response.status} ${await response.text()}`);
408
+ return await response.json();
409
+ };
410
+ const runs = await depotRpc("ListRuns", {
411
+ repo: `${params.owner}/${params.repo}`,
412
+ sha: checkRun.head_sha,
413
+ pageSize: 10
414
+ });
415
+ for (const run of runs.runs || []) {
416
+ const flakeArtifacts = ((await depotRpc("ListArtifacts", {
417
+ runId: run.runId,
418
+ pageSize: 100
419
+ })).artifacts || []).filter((artifact) => artifact.name.startsWith(ARTIFACT_PREFIX));
420
+ if (flakeArtifacts.length === 0) continue;
421
+ const stream = itx.streams.get(flakesStreamPath);
422
+ try {
423
+ await stream.append(...flakeDashboardCreationEvents({
424
+ repository: params,
425
+ issueTitle: "Flake dashboard",
426
+ defaultBranch: repository.default_branch || "main"
427
+ }));
428
+ } catch (error) {
429
+ if (!isIdempotencyConflict(error)) throw error;
430
+ }
431
+ for (const artifact of flakeArtifacts) {
432
+ if (Number(artifact.sizeBytes || 0) > MAX_ARTIFACT_BYTES) {
433
+ console.warn(`[flake-ingest] skipping oversized artifact ${artifact.name} (${artifact.sizeBytes} bytes)`);
434
+ continue;
435
+ }
436
+ const suite = artifact.name.slice(14);
437
+ const download = await depotRpc("GetArtifactDownloadURL", { artifactId: artifact.artifactId });
438
+ if (!download.url) {
439
+ console.warn(`[flake-ingest] no download url for artifact ${artifact.name}`);
440
+ continue;
441
+ }
442
+ const zipResponse = await fetch(download.url);
443
+ if (!zipResponse.ok) throw new Error(`artifact download failed: HTTP ${zipResponse.status}`);
444
+ const files = await unzip(new Uint8Array(await zipResponse.arrayBuffer()));
445
+ const malformedRecords = [];
446
+ const records = Object.entries(files).filter(([name]) => name.endsWith(".jsonl")).flatMap(([name, bytes]) => new TextDecoder().decode(bytes).split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
447
+ let json;
448
+ try {
449
+ json = JSON.parse(line);
450
+ } catch {
451
+ malformedRecords.push(`Malformed record in ${name}`);
452
+ return [];
453
+ }
454
+ const parsed = FlakeRecord.safeParse(json);
455
+ if (!parsed.success) {
456
+ malformedRecords.push(`Malformed record in ${name}`);
457
+ return [];
458
+ }
459
+ return [parsed.data];
460
+ }));
461
+ const summaryBytes = Object.entries(files).find(([name]) => name === "suite-summary.json" || name.endsWith("/suite-summary.json"))?.[1];
462
+ let summary;
463
+ if (summaryBytes) try {
464
+ summary = FlakeSuiteSummary.parse(JSON.parse(new TextDecoder().decode(summaryBytes)));
465
+ } catch {
466
+ console.error(`[flake-ingest] invalid suite-summary.json in ${artifact.name}; skipping artifact`);
467
+ continue;
468
+ }
469
+ if (summary && summary.unknownFlakeCount !== records.filter((record) => record.kind === "unknown").length) malformedRecords.push("Unknown flake records do not match the full runner result");
470
+ if (summary?.tests && summary.tests.length !== summary.testCount) malformedRecords.push("Per-test results do not match the full runner test count");
471
+ if (summary && malformedRecords.length > 0) {
472
+ summary.status = "incomplete";
473
+ summary.diagnostics.push(...new Set(malformedRecords));
474
+ }
475
+ if (records.length === 0 && !summary) continue;
476
+ const runId = `${run.runId}-${artifact.attempt || 1}`;
477
+ try {
478
+ await stream.append({
479
+ type: "events.iterate.com/flakes/run-recorded",
480
+ idempotencyKey: `flakes/run:${runId}:${suite}`,
481
+ payload: {
482
+ runId,
483
+ suite,
484
+ branch: summary ? summary.branch : checkRun.check_suite?.head_branch || "unknown",
485
+ commit: summary ? summary.headSha : checkRun.head_sha,
486
+ records,
487
+ summary
488
+ }
489
+ });
490
+ } catch (error) {
491
+ if (!isIdempotencyConflict(error)) throw error;
492
+ }
493
+ console.log(`[flake-ingest] ingested ${records.length} records from ${artifact.name} (run ${runId})`);
494
+ }
495
+ }
496
+ } catch (_) {
497
+ _usingCtx3.e = _;
498
+ } finally {
499
+ _usingCtx3.d();
500
+ }
501
+ }
502
+ /** Internal RPC surface used by smoke checks and debugging. */
503
+ async getState() {
504
+ await (await this.registry()).catchUp("flake-dashboard");
505
+ return (await this.snapshot()).state;
506
+ }
507
+ };
508
+ /**
509
+ * One instance runs on the project's `/flakes` stream. CI appends
510
+ * `run-recorded` events; this processor folds them into per-test stats, owns
511
+ * the GitHub issue render as its durable obligation, and proposes lifecycle
512
+ * transitions when a default-branch streak crosses a threshold.
513
+ */
514
+ var FlakeDashboardProcessor = class extends StreamProcessor {
515
+ contract = FlakeDashboardProcessorContract;
516
+ /**
517
+ * Runtime-only render attempt guard. The render-settled stream event is the
518
+ * durable truth: after an eviction this is false and the next caught-up
519
+ * pass re-drives any still-unrendered data.
520
+ */
521
+ #liveRender = false;
522
+ reduce({ event, state }) {
523
+ switch (event.type) {
524
+ case flakeEventTypes.created:
525
+ if (state.birthCertificate !== null) return state;
526
+ return {
527
+ ...state,
528
+ birthCertificate: event.payload,
529
+ lastDataOffset: event.offset
530
+ };
531
+ case flakeEventTypes.runRecorded: {
532
+ const config = state.birthCertificate?.config;
533
+ if (config === void 0) return state;
534
+ const onDefaultBranch = event.payload.branch === config.defaultBranch;
535
+ const { summary, suite, runId, commit, records } = event.payload;
536
+ const tests = { ...state.tests };
537
+ for (const record of event.payload.records) {
538
+ const existing = tests[record.name];
539
+ const runAt = summary?.startedAt || record.at;
540
+ const wrapperAt = existing?.lastMainWrapperAt[suite];
541
+ const unknownAt = state.unknownFlakes[suite]?.[record.name]?.lastRunAt;
542
+ const staleKind = existing && onDefaultBranch && (record.kind === "unknown" ? wrapperAt && runAt < wrapperAt : unknownAt && runAt < unknownAt);
543
+ const kind = staleKind ? existing.kind : record.kind;
544
+ const counts = { ...existing?.counts };
545
+ counts[record.outcome] = (counts[record.outcome] || 0) + 1;
546
+ const streak = existing?.defaultBranchStreak || null;
547
+ const nextStreak = !onDefaultBranch || staleKind ? streak : record.outcome === "unexpected-error" ? null : streak && streak.outcome === record.outcome ? {
548
+ ...streak,
549
+ runs: streak.runs + 1,
550
+ lastAt: record.at
551
+ } : {
552
+ outcome: record.outcome,
553
+ runs: 1,
554
+ firstAt: record.at,
555
+ lastAt: record.at
556
+ };
557
+ const flakeStruck = record.outcome === "flake-fail" || record.outcome === "retried-pass";
558
+ const errorSample = record.error !== void 0 && (record.outcome === "retried-pass" || record.outcome === "unexpected-error") ? {
559
+ error: record.error,
560
+ commit: event.payload.commit,
561
+ at: record.at
562
+ } : null;
563
+ tests[record.name] = {
564
+ kind,
565
+ pattern: staleKind ? existing.pattern : record.pattern || existing?.pattern || "",
566
+ suites: existing?.suites.includes(event.payload.suite) ? existing.suites : [...existing?.suites || [], event.payload.suite],
567
+ lastSeenOffset: {
568
+ ...existing?.lastSeenOffset,
569
+ [event.payload.suite]: event.offset
570
+ },
571
+ recent: [...existing?.recent || [], {
572
+ outcome: record.outcome,
573
+ commit: event.payload.commit,
574
+ at: record.at
575
+ }].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-10),
576
+ counts,
577
+ recentErrors: errorSample ? [...existing?.recentErrors || [], errorSample].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-3) : existing?.recentErrors || [],
578
+ lastFlakeAt: flakeStruck && (!existing?.lastFlakeAt || Date.parse(record.at) > Date.parse(existing.lastFlakeAt)) ? record.at : existing?.lastFlakeAt || null,
579
+ firstRecordedAt: existing && existing.kind === kind ? existing.firstRecordedAt : record.at,
580
+ lastRecordedAt: existing && Date.parse(existing.lastRecordedAt) > Date.parse(record.at) ? existing.lastRecordedAt : record.at,
581
+ defaultBranchStreak: nextStreak,
582
+ lastMainWrapperAt: onDefaultBranch && record.kind !== "unknown" ? {
583
+ ...existing?.lastMainWrapperAt,
584
+ [suite]: wrapperAt && wrapperAt > runAt ? wrapperAt : runAt
585
+ } : existing?.lastMainWrapperAt || {},
586
+ proposed: existing?.proposed || []
587
+ };
588
+ }
589
+ const suites = {
590
+ ...state.suites,
591
+ [event.payload.suite]: { recentRunOffsets: [...state.suites[event.payload.suite]?.recentRunOffsets || [], ...(event.payload.summary ? event.payload.summary.status === "complete" : event.payload.records.length > 0) ? [event.offset] : []].slice(-3) }
592
+ };
593
+ const mainRuns = { ...state.mainRuns };
594
+ const previousInventory = mainRuns[suite]?.inventory || null;
595
+ const inventory = onDefaultBranch && summary?.status === "complete" && summary.tests && (!previousInventory || summary.startedAt >= previousInventory.startedAt) ? {
596
+ startedAt: summary.startedAt,
597
+ names: [...new Set(summary.tests.map((test) => test.name))]
598
+ } : previousInventory;
599
+ const unknownFlakes = { ...state.unknownFlakes };
600
+ if (onDefaultBranch) {
601
+ const unknowns = { ...unknownFlakes[suite] };
602
+ const recordedNames = new Set(records.map((record) => record.name));
603
+ const results = /* @__PURE__ */ new Map();
604
+ for (const test of summary?.tests || []) {
605
+ const previous = results.get(test.name);
606
+ results.set(test.name, previous === "fail" || test.outcome === "fail" ? "fail" : previous === "skip" || test.outcome === "skip" ? "skip" : "pass");
607
+ }
608
+ if (summary) for (const [name, test] of Object.entries(unknowns)) {
609
+ if (recordedNames.has(name)) continue;
610
+ const wrapperAt = tests[name]?.lastMainWrapperAt[suite];
611
+ if (wrapperAt && summary.startedAt < wrapperAt) continue;
612
+ const outcome = results.get(name);
613
+ if (!outcome) continue;
614
+ const pass = outcome === "pass" && summary.status === "complete";
615
+ const fail = outcome === "fail";
616
+ if (!fail && summary.startedAt <= test.lastRunAt) continue;
617
+ const passStreak = fail ? 0 : test.passStreak + Number(pass);
618
+ unknowns[name] = {
619
+ ...test,
620
+ passStreak,
621
+ lastRunAt: summary.startedAt > test.lastRunAt ? summary.startedAt : test.lastRunAt,
622
+ recent: pass || fail ? test.recent.concat({
623
+ outcome: pass ? "pass" : "unexpected-error",
624
+ commit,
625
+ at: summary.startedAt
626
+ }).sort((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-10) : test.recent
627
+ };
628
+ }
629
+ for (const record of records) {
630
+ const previous = unknowns[record.name];
631
+ const lastRunAt = summary?.startedAt || record.at;
632
+ const wrapperAt = tests[record.name]?.lastMainWrapperAt[suite];
633
+ if (record.kind === "unknown" && wrapperAt && lastRunAt < wrapperAt) continue;
634
+ if (record.kind !== "unknown") {
635
+ if (previous && lastRunAt <= previous.lastRunAt) continue;
636
+ delete unknowns[record.name];
637
+ continue;
638
+ }
639
+ unknowns[record.name] = {
640
+ record: previous && Date.parse(previous.record.at) > Date.parse(record.at) ? previous.record : record,
641
+ passStreak: 0,
642
+ lastRunAt: previous && previous.lastRunAt > lastRunAt ? previous.lastRunAt : lastRunAt,
643
+ recent: [...previous?.recent || [], {
644
+ outcome: record.outcome,
645
+ commit,
646
+ at: record.at
647
+ }].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-10)
648
+ };
649
+ }
650
+ if (inventory) {
651
+ const names = new Set(inventory.names);
652
+ for (const [name, test] of Object.entries(unknowns)) if (test.lastRunAt <= inventory.startedAt && !names.has(name)) delete unknowns[name];
653
+ }
654
+ unknownFlakes[suite] = unknowns;
655
+ }
656
+ if (onDefaultBranch && summary) {
657
+ const previous = mainRuns[suite];
658
+ const incoming = {
659
+ runId,
660
+ commit,
661
+ summary: {
662
+ ...summary,
663
+ tests: void 0
664
+ }
665
+ };
666
+ mainRuns[suite] = {
667
+ inventory,
668
+ latest: !previous || summary.startedAt >= previous.latest.summary.startedAt ? incoming : previous.latest,
669
+ complete: summary.status === "complete" && (!previous?.complete || summary.startedAt >= previous.complete.summary.startedAt) ? incoming : previous?.complete || null
670
+ };
671
+ }
672
+ return {
673
+ ...state,
674
+ tests,
675
+ suites,
676
+ mainRuns,
677
+ unknownFlakes,
678
+ lastDataOffset: event.offset
679
+ };
680
+ }
681
+ case flakeEventTypes.transitionProposed: {
682
+ const test = state.tests[event.payload.testName];
683
+ if (test === void 0) return {
684
+ ...state,
685
+ lastDataOffset: event.offset
686
+ };
687
+ const marker = `${event.payload.transition}:${event.payload.evidence.firstAt}`;
688
+ return {
689
+ ...state,
690
+ tests: {
691
+ ...state.tests,
692
+ [event.payload.testName]: {
693
+ ...test,
694
+ proposed: test.proposed.includes(marker) ? test.proposed : [...test.proposed, marker]
695
+ }
696
+ },
697
+ lastDataOffset: event.offset
698
+ };
699
+ }
700
+ case flakeEventTypes.dashboardRenderSettled:
701
+ if (event.payload.result.status !== "succeeded") return state;
702
+ return {
703
+ ...state,
704
+ render: {
705
+ throughOffset: event.payload.throughOffset,
706
+ issueNumber: event.payload.result.issueNumber,
707
+ issueUrl: event.payload.result.issueUrl
708
+ }
709
+ };
710
+ }
711
+ }
712
+ processEvent(args) {
713
+ const { append, blockProcessorWhile, delivery, event, runInBackground, state } = args;
714
+ if (event?.type === flakeEventTypes.runRecorded) for (const [testName, test] of Object.entries(state.tests)) {
715
+ if (isSentinel(testName)) continue;
716
+ const streak = test.defaultBranchStreak;
717
+ if (streak === null) continue;
718
+ const transition = streak.outcome === "pass" ? "unwrap" : streak.outcome === "flake-fail" ? "switch-to-failing" : streak.outcome === "unexpected-pass" ? "unwrap-failing" : null;
719
+ if (transition === null) continue;
720
+ const threshold = flakeTransitionThresholds[transition];
721
+ const spanMs = Date.parse(streak.lastAt) - Date.parse(streak.firstAt);
722
+ if (streak.runs < threshold.runs || spanMs < threshold.minSpanMs) continue;
723
+ const marker = `${transition}:${streak.firstAt}`;
724
+ if (test.proposed.includes(marker)) continue;
725
+ blockProcessorWhile(() => this.#appendTolerantOfSettledRace(append, {
726
+ type: flakeEventTypes.transitionProposed,
727
+ idempotencyKey: this.idempotencyKey(`transition:${testName}:${marker}`),
728
+ payload: {
729
+ testName,
730
+ transition,
731
+ evidence: {
732
+ consecutiveRuns: streak.runs,
733
+ firstAt: streak.firstAt,
734
+ lastAt: streak.lastAt
735
+ }
736
+ }
737
+ }));
738
+ }
739
+ if (!delivery.caughtUp) return;
740
+ if (state.birthCertificate === null) return;
741
+ if (state.lastDataOffset <= (state.render?.throughOffset || 0)) return;
742
+ if (this.#liveRender) return;
743
+ const throughOffset = state.lastDataOffset;
744
+ this.#liveRender = true;
745
+ runInBackground(async () => {
746
+ let result;
747
+ try {
748
+ result = await this.deps.renderDashboard(state);
749
+ } catch (error) {
750
+ result = {
751
+ status: "failed",
752
+ error: (error instanceof Error ? error.message : String(error)).slice(0, 8e3) || "Unknown render failure."
753
+ };
754
+ }
755
+ try {
756
+ await this.#appendTolerantOfSettledRace(append, {
757
+ type: flakeEventTypes.dashboardRenderSettled,
758
+ idempotencyKey: this.idempotencyKey(`dashboard-render:${throughOffset}:${result.status}`),
759
+ payload: {
760
+ throughOffset,
761
+ result
762
+ }
763
+ });
764
+ } finally {
765
+ this.#liveRender = false;
766
+ }
767
+ });
768
+ }
769
+ /**
770
+ * Growing streaks and overlapping render attempts can race on one
771
+ * processor-owned key with fresher bodies. The first committed fact wins;
772
+ * a same-key/different-body rejection means the work is already settled.
773
+ */
774
+ async #appendTolerantOfSettledRace(append, event) {
775
+ try {
776
+ await append(event);
777
+ } catch (error) {
778
+ if (!isIdempotencyConflict(error)) throw error;
779
+ }
780
+ }
781
+ };
782
+ const DASHBOARD_MARKER = "<!-- iterate-flake-dashboard -->";
783
+ /**
784
+ * Upsert the GitHub "Flake dashboard" issue from folded state. Mechanical by
785
+ * design (the AI-linter publication stance): all decisions were made when the
786
+ * events reduced. The processor is the single writer, so a plain body
787
+ * overwrite is race-free; the marker plus remembered issue number make the
788
+ * upsert idempotent across retried renders.
789
+ */
790
+ async function renderFlakeDashboardIssue(itx, state) {
791
+ const config = state.birthCertificate?.config;
792
+ if (config === void 0) throw new Error("Flake dashboard has no birth certificate.");
793
+ const repos = await itx.repos.list();
794
+ const link = (await Promise.all(repos.map(async ({ path }) => (await itx.repos.get(path).processor.snapshot()).state.github))).find((candidate) => candidate !== null && candidate.owner === config.repository.owner && candidate.repo === config.repository.repo);
795
+ if (link === null || link === void 0) throw new Error(`No linked GitHub connection for ${config.repository.owner}/${config.repository.repo}; link a repo to that repository before the dashboard can render.`);
796
+ const octokit = itx.integrations.github.get(link.connection).octokit;
797
+ const params = {
798
+ owner: config.repository.owner,
799
+ repo: config.repository.repo
800
+ };
801
+ const body = renderBody(state);
802
+ const rememberedIssueNumber = state.render?.issueNumber || null;
803
+ if (rememberedIssueNumber !== null) return {
804
+ status: "succeeded",
805
+ issueNumber: rememberedIssueNumber,
806
+ issueUrl: (await octokit.rest.issues.update({
807
+ ...params,
808
+ issue_number: rememberedIssueNumber,
809
+ body,
810
+ title: config.issueTitle
811
+ })).data.html_url
812
+ };
813
+ const existing = (await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
814
+ ...params,
815
+ state: "open",
816
+ per_page: 100
817
+ })).find((issue) => issue.body?.includes(DASHBOARD_MARKER) === true);
818
+ if (existing !== void 0) {
819
+ await octokit.rest.issues.update({
820
+ ...params,
821
+ issue_number: existing.number,
822
+ body,
823
+ title: config.issueTitle
824
+ });
825
+ return {
826
+ status: "succeeded",
827
+ issueNumber: existing.number,
828
+ issueUrl: existing.html_url
829
+ };
830
+ }
831
+ const created = await octokit.rest.issues.create({
832
+ ...params,
833
+ title: config.issueTitle,
834
+ body
835
+ });
836
+ return {
837
+ status: "succeeded",
838
+ issueNumber: created.data.number,
839
+ issueUrl: created.data.html_url
840
+ };
841
+ }
842
+ const OUTCOME_EMOJI = {
843
+ pass: "🟩",
844
+ "unexpected-pass": "🟩",
845
+ "flake-fail": "🟥",
846
+ "pinned-fail": "🟥",
847
+ "retried-pass": "🟥",
848
+ "unexpected-error": "❌"
849
+ };
850
+ /**
851
+ * The deliberate canary flakes are identified by naming convention — every
852
+ * suite's sentinel test contains this phrase ("flake sentinel",
853
+ * "flake sentinel (specs)", …). A convention beats a record flag: it needs no
854
+ * wire-format field and holds for all historical records.
855
+ */
856
+ function isSentinel(testName) {
857
+ return testName.includes("flake sentinel");
858
+ }
859
+ const MONTHS = [
860
+ "Jan",
861
+ "Feb",
862
+ "Mar",
863
+ "Apr",
864
+ "May",
865
+ "Jun",
866
+ "Jul",
867
+ "Aug",
868
+ "Sep",
869
+ "Oct",
870
+ "Nov",
871
+ "Dec"
872
+ ];
873
+ /** "Sep 4, 7:16am" (UTC) — ISO timestamps read like log spam in the table. */
874
+ function shortDate(iso) {
875
+ const date = new Date(iso);
876
+ if (Number.isNaN(date.getTime())) return iso;
877
+ const hours = date.getUTCHours();
878
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
879
+ return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}, ${hours % 12 || 12}:${minutes}${hours >= 12 ? "pm" : "am"}`;
880
+ }
881
+ /** Exported for tests: the table is a pure projection of folded state. */
882
+ function renderBody(state) {
883
+ const tests = Object.entries(state.tests).sort(([a], [b]) => a.localeCompare(b));
884
+ const lastRecordedAt = [...tests.map(([, test]) => test.lastRecordedAt), ...Object.values(state.mainRuns).map((result) => result.latest.summary.finishedAt)].sort().at(-1);
885
+ const tracked = tests.filter(([, test]) => test.kind !== "unknown");
886
+ const visible = tracked.filter(([, test]) => Object.entries(test.lastSeenOffset).some(([suite, seen]) => {
887
+ const windowStart = state.suites[suite]?.recentRunOffsets[0];
888
+ return windowStart === void 0 || seen >= windowStart;
889
+ }));
890
+ const retiredCount = tracked.length - visible.length;
891
+ const config = state.birthCertificate?.config;
892
+ const count = (test, outcome) => test.counts[outcome] || 0;
893
+ const row = ([name, test]) => {
894
+ const runs = Object.values(test.counts).reduce((total, n) => total + n, 0);
895
+ const info = [
896
+ `pattern: \`${`/${test.pattern}/`.replaceAll(/\s+/gu, " ").replaceAll("`", "'").replaceAll("|", "\\|")}\``,
897
+ `suites: ${test.suites.join(", ")}`,
898
+ ...test.proposed.length === 0 ? [] : [`proposed: ${test.proposed.map((p) => p.split(":")[0]).join(", ")}`]
899
+ ].join("<br>");
900
+ const gated = count(test, "pass") + count(test, "flake-fail");
901
+ const stats = (test.kind === "failing" ? [
902
+ `runs: ${runs}`,
903
+ `pin held: ${count(test, "pinned-fail")}`,
904
+ `unexpected passes: ${count(test, "unexpected-pass")}`,
905
+ `pinned since: ${shortDate(test.firstRecordedAt)}`
906
+ ] : [
907
+ `runs: ${runs}`,
908
+ `flake rate: ${gated === 0 ? "—" : `${Math.round(count(test, "flake-fail") / gated * 100)}%`}`,
909
+ `last flake: ${test.lastFlakeAt ? shortDate(test.lastFlakeAt) : "never"}`
910
+ ]).join("<br>");
911
+ const streak = [test.recent.map((entry) => {
912
+ const emoji = OUTCOME_EMOJI[entry.outcome];
913
+ if (config === void 0) return emoji;
914
+ const { owner, repo } = config.repository;
915
+ return `[${emoji}](https://github.com/${owner}/${repo}/commit/${entry.commit})`;
916
+ }).join("") || "—", ...test.defaultBranchStreak === null || config === void 0 ? [] : [`${test.defaultBranchStreak.runs}× ${test.defaultBranchStreak.outcome} (${config.defaultBranch})`]].join("<br>");
917
+ return [
918
+ `\`${name}\``,
919
+ info,
920
+ stats,
921
+ streak
922
+ ].join(" | ");
923
+ };
924
+ const sections = [
925
+ {
926
+ title: "Flakes",
927
+ legend: `_createFlake-wrapped: 🟩 passed · 🟥 the known flake struck · ❌ failed differently than expected. Suggest removing the wrapper after ${flakeTransitionThresholds.unwrap.runs} consecutive main passes, with no minimum elapsed time._`,
928
+ tests: visible.filter(([name, test]) => test.kind === "flake" && !isSentinel(name))
929
+ },
930
+ {
931
+ title: "Failures",
932
+ legend: "_createFailing pins: 🟥 the pinned bug is present · 🟩 passed unexpectedly — the bug may be fixed · ❌ failed differently than expected._",
933
+ tests: visible.filter(([, test]) => test.kind === "failing")
934
+ },
935
+ {
936
+ title: "Sentinels",
937
+ legend: "_Deliberate canary flakes proving the recording pipeline works: 🟩 passed · 🟥 the known flake struck · ❌ failed differently than expected._",
938
+ tests: visible.filter(([name, test]) => test.kind === "flake" && isSentinel(name))
939
+ }
940
+ ].filter((section) => section.tests.length > 0);
941
+ return [
942
+ DASHBOARD_MARKER,
943
+ "Test health, folded from CI-reported runs: [`createFlake`](https://github.com/iterate/iterate/blob/main/packages/shared/src/test-support/flake-test.ts) wraps, [`createFailing`](https://github.com/iterate/iterate/blob/main/packages/shared/src/test-support/failing-test.ts) pins, and unclassified flaky tests caught by CI retries. Maintained automatically — edits to this body will be overwritten. Squares show the last 10 outcomes, oldest→newest, and link to their commits. Wrapped tests show all branches; unknown flakes show main only. Lifecycle streak counts use main only.",
944
+ ...sections.filter((section) => section.title !== "Sentinels").flatMap(renderSection),
945
+ ...renderUnknownFlakes(state),
946
+ ...sections.filter((section) => section.title === "Sentinels").flatMap(renderSection),
947
+ "",
948
+ `_Last recorded outcome: ${lastRecordedAt || "none"}._`,
949
+ ...retiredCount === 0 ? [] : [`_${retiredCount} retired ${retiredCount === 1 ? "test" : "tests"} hidden (absent from the last 3 runs of their suite)._`]
950
+ ].join("\n");
951
+ function renderSection(section) {
952
+ const collapsed = section.title === "Failures" || section.title === "Sentinels";
953
+ const suiteCounts = /* @__PURE__ */ new Map();
954
+ for (const [, test] of section.tests) for (const suite of test.suites) suiteCounts.set(suite, (suiteCounts.get(suite) || 0) + 1);
955
+ const summary = [`${section.tests.length} ${section.tests.length === 1 ? "test" : "tests"}`, ...[...suiteCounts].sort(([a], [b]) => a.localeCompare(b)).map(([suite, count]) => `${suite}: ${count}`)].join(" · ");
956
+ return [
957
+ "",
958
+ `## ${section.title}`,
959
+ "",
960
+ ...collapsed ? [
961
+ "<details>",
962
+ `<summary>${summary}</summary>`,
963
+ ""
964
+ ] : [],
965
+ section.legend,
966
+ "",
967
+ "test | info | stats | streak",
968
+ "--- | --- | --- | ---",
969
+ ...section.tests.map(row),
970
+ ...collapsed ? ["", "</details>"] : []
971
+ ];
972
+ }
973
+ }
974
+ function renderUnknownFlakes(state) {
975
+ const suites = [...new Set([...Object.keys(state.suites), ...Object.keys(state.mainRuns)])].sort();
976
+ const escape = (text) => text.replaceAll(/\s+/gu, " ").replaceAll(/([\\`*_[\]|<>])/gu, "\\$1");
977
+ const rows = suites.flatMap((suite) => Object.entries(state.unknownFlakes[suite] || {}).filter(([, test]) => test.passStreak < flakeTransitionThresholds.unwrap.runs).sort(([a], [b]) => a.localeCompare(b)).map(([, test]) => {
978
+ const record = test.record;
979
+ const config = state.birthCertificate.config;
980
+ const squares = test.recent.map(({ outcome, commit }) => `[${OUTCOME_EMOJI[outcome]}](https://github.com/${config.repository.owner}/${config.repository.repo}/commit/${commit})`).join("");
981
+ return [
982
+ escape(record.name),
983
+ `\`${(record.error || "No error sample recorded").replaceAll(/\s+/gu, " ").replaceAll("`", "'").replaceAll("|", "\\|").slice(0, 300)}\``,
984
+ escape(suite),
985
+ `${squares}<br>${test.passStreak}/${flakeTransitionThresholds.unwrap.runs} consecutive passes`
986
+ ].join(" | ");
987
+ }));
988
+ const completeCount = suites.filter((suite) => state.mainRuns[suite]?.complete).length;
989
+ return [
990
+ "",
991
+ "## Unknown flakes",
992
+ "",
993
+ `_Any main retry adds a test here. Remove it after ${flakeTransitionThresholds.unwrap.runs} consecutive main passes, wrapper adoption, or absence from a complete main run's full test list. Failures reset the streak. Skips and incomplete results cannot advance it or prove deletion. 🟩 passed · 🟥 needed a retry · ❌ failed. PR results do not affect this table._`,
994
+ "",
995
+ ...suites.map((suite) => {
996
+ const result = state.mainRuns[suite];
997
+ if (!result) return `- **${escape(suite)}:** awaiting a complete main result.`;
998
+ const complete = result.complete;
999
+ const incomplete = result.latest.summary.status === "incomplete" ? ` Latest attempt [${result.latest.commit.slice(0, 7)}](${result.latest.summary.runUrl}) incomplete: ${escape(result.latest.summary.diagnostics.join("; "))}.` : "";
1000
+ if (!complete) return `- **${escape(suite)}:** awaiting a complete main result.${incomplete}`;
1001
+ return `- **${escape(suite)}:** [${complete.commit.slice(0, 7)}](${complete.summary.runUrl}) · ${shortDate(complete.summary.finishedAt)} UTC · ${complete.summary.testCount} tests · ${complete.summary.failedCount} failed.${incomplete}`;
1002
+ }),
1003
+ "",
1004
+ ...rows.length > 0 ? [
1005
+ "test | first failure | suite | streak (main)",
1006
+ "--- | --- | --- | ---",
1007
+ ...rows
1008
+ ] : [completeCount === 0 ? "_Awaiting the first complete main results._" : "_No active unknown flakes. Passing streaks are evidence of stability, not proof that the root cause is fixed._"]
1009
+ ];
1010
+ }
1011
+ /**
1012
+ * Minimal zip reader on the runtime's own DecompressionStream — deliberately
1013
+ * not a dependency. The format surface is narrow by construction: one
1014
+ * producer (GitHub's artifact service), a 5MB size cap upstream, and reading
1015
+ * via the central directory (sizes come from there, so streaming-writer data
1016
+ * descriptors don't matter). No zip64 — impossible under the size cap — and
1017
+ * anything unexpected throws, which ingestion treats as a logged drop.
1018
+ */
1019
+ async function unzip(bytes) {
1020
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1021
+ let eocd = -1;
1022
+ for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 65535); i--) if (view.getUint32(i, true) === 101010256) {
1023
+ eocd = i;
1024
+ break;
1025
+ }
1026
+ if (eocd < 0) throw new Error("not a zip: no end-of-central-directory record");
1027
+ const entryCount = view.getUint16(eocd + 10, true);
1028
+ const files = {};
1029
+ let offset = view.getUint32(eocd + 16, true);
1030
+ for (let i = 0; i < entryCount; i++) {
1031
+ if (view.getUint32(offset, true) !== 33639248) throw new Error("corrupt zip: bad central directory entry signature");
1032
+ const method = view.getUint16(offset + 10, true);
1033
+ const compressedSize = view.getUint32(offset + 20, true);
1034
+ const nameLength = view.getUint16(offset + 28, true);
1035
+ const extraLength = view.getUint16(offset + 30, true);
1036
+ const commentLength = view.getUint16(offset + 32, true);
1037
+ const localHeaderOffset = view.getUint32(offset + 42, true);
1038
+ const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength));
1039
+ const localNameLength = view.getUint16(localHeaderOffset + 26, true);
1040
+ const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
1041
+ const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
1042
+ const data = bytes.slice(dataStart, dataStart + compressedSize);
1043
+ if (method === 0) files[name] = data;
1044
+ else if (method === 8) {
1045
+ const inflated = new Response(data).body.pipeThrough(new DecompressionStream("deflate-raw"));
1046
+ files[name] = new Uint8Array(await new Response(inflated).arrayBuffer());
1047
+ } else throw new Error(`unsupported zip compression method ${method} for ${name}`);
1048
+ offset += 46 + nameLength + extraLength + commentLength;
1049
+ }
1050
+ return files;
1051
+ }
1052
+ //#endregion
1053
+ export { FlakeDashboardApp };
1054
+
1055
+ //# sourceMappingURL=configured-worker.mjs.map