@pipeline-builder/pipeline-data 4.1.23 → 4.1.25

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 (64) hide show
  1. package/lib/api/alert-destinations.d.ts +17 -0
  2. package/lib/api/alert-destinations.js +36 -0
  3. package/lib/api/reporting/analytics-queries.d.ts +274 -0
  4. package/lib/api/reporting/analytics-queries.js +598 -0
  5. package/lib/api/reporting/daily-rollup.d.ts +41 -0
  6. package/lib/api/reporting/daily-rollup.js +173 -0
  7. package/lib/api/reporting/deploy-rollup.d.ts +78 -0
  8. package/lib/api/reporting/deploy-rollup.js +203 -0
  9. package/lib/api/reporting/dora-scoring.d.ts +14 -0
  10. package/lib/api/reporting/dora-scoring.js +25 -1
  11. package/lib/api/reporting/dora.js +30 -6
  12. package/lib/api/reporting/failure-classifier.d.ts +54 -0
  13. package/lib/api/reporting/failure-classifier.js +179 -0
  14. package/lib/api/reporting/ingest.d.ts +2 -2
  15. package/lib/api/reporting/ingest.js +129 -4
  16. package/lib/api/reporting/last-deploy.d.ts +31 -0
  17. package/lib/api/reporting/last-deploy.js +93 -0
  18. package/lib/api/reporting/needs-attention.d.ts +93 -0
  19. package/lib/api/reporting/needs-attention.js +152 -0
  20. package/lib/api/reporting/plugin-exposure.d.ts +129 -0
  21. package/lib/api/reporting/plugin-exposure.js +312 -0
  22. package/lib/api/reporting/retention-sweep.js +9 -1
  23. package/lib/api/reporting/sql-helpers.d.ts +35 -2
  24. package/lib/api/reporting/sql-helpers.js +39 -1
  25. package/lib/api/reporting/stakeholder/compose.d.ts +115 -0
  26. package/lib/api/reporting/stakeholder/compose.js +234 -0
  27. package/lib/api/reporting/stakeholder/index.d.ts +14 -0
  28. package/lib/api/reporting/stakeholder/index.js +17 -0
  29. package/lib/api/reporting/stakeholder/period.d.ts +102 -0
  30. package/lib/api/reporting/stakeholder/period.js +273 -0
  31. package/lib/api/reporting/stakeholder/report-html.d.ts +93 -0
  32. package/lib/api/reporting/stakeholder/report-html.js +208 -0
  33. package/lib/api/reporting/stakeholder/sections.d.ts +117 -0
  34. package/lib/api/reporting/stakeholder/sections.js +249 -0
  35. package/lib/api/reporting/stakeholder/store.d.ts +450 -0
  36. package/lib/api/reporting/stakeholder/store.js +981 -0
  37. package/lib/api/reporting/stakeholder/templates.d.ts +21 -0
  38. package/lib/api/reporting/stakeholder/templates.js +104 -0
  39. package/lib/api/reporting/types.d.ts +73 -0
  40. package/lib/api/reporting/types.js +1 -1
  41. package/lib/api/reporting-service.d.ts +94 -5
  42. package/lib/api/reporting-service.js +169 -10
  43. package/lib/api/soft-delete-sweep.d.ts +11 -2
  44. package/lib/api/soft-delete-sweep.js +17 -6
  45. package/lib/database/drizzle-schema.d.ts +2263 -385
  46. package/lib/database/drizzle-schema.js +18 -7
  47. package/lib/database/index.d.ts +1 -1
  48. package/lib/database/index.js +4 -2
  49. package/lib/database/schema/compliance.d.ts +0 -682
  50. package/lib/database/schema/compliance.js +6 -66
  51. package/lib/database/schema/ecosystem.d.ts +20 -157
  52. package/lib/database/schema/ecosystem.js +23 -16
  53. package/lib/database/schema/pipeline.d.ts +200 -0
  54. package/lib/database/schema/pipeline.js +100 -1
  55. package/lib/database/schema/plugin.d.ts +20 -1
  56. package/lib/database/schema/plugin.js +13 -1
  57. package/lib/database/schema/reporting-analytics.d.ts +1212 -0
  58. package/lib/database/schema/reporting-analytics.js +250 -0
  59. package/lib/database/schema/reporting-stakeholder.d.ts +1405 -0
  60. package/lib/database/schema/reporting-stakeholder.js +251 -0
  61. package/lib/index.d.ts +10 -1
  62. package/lib/index.js +15 -1
  63. package/lib/testing/exports.json +67 -4
  64. package/package.json +4 -3
@@ -0,0 +1,41 @@
1
+ /**
2
+ * How long after a day closes before it is rolled up (hours).
3
+ *
4
+ * Six by default, matching the report scheduler's own settle delay: both exist for
5
+ * the same reason, and a rollup that lagged the report would make a scheduled
6
+ * report read a half-built day.
7
+ */
8
+ export declare function rollupSettleHours(): number;
9
+ /** Rows per rebuild pass. Bounds the statement, not the backlog. */
10
+ export declare function rollupBatchDays(): number;
11
+ /** The UTC midnight that starts `d`'s day. */
12
+ export declare function utcDayStart(d: Date): Date;
13
+ /**
14
+ * Rebuild the rollup rows for one UTC day, for every org that had activity.
15
+ *
16
+ * ONE statement, computed entirely in Postgres. Deliberately not "select the
17
+ * events, aggregate in Node, write the rows": the whole point of the rollup is to
18
+ * avoid moving a day's events across the wire, and a job that did it to BUILD the
19
+ * rollup would just move the cost rather than remove it.
20
+ *
21
+ * Percentiles use `PERCENTILE_CONT`, which is the same function the on-demand
22
+ * queries use — so a rollup-backed number and a live number agree rather than
23
+ * differing by an interpolation rule.
24
+ */
25
+ export declare function rebuildDay(day: Date): Promise<number>;
26
+ /**
27
+ * One rollup pass: rebuild the most recent settled days.
28
+ *
29
+ * Rebuilding a WINDOW rather than only the newest day is what handles late
30
+ * events: the dead-letter redrive lands events for days already rolled up, and a
31
+ * job that only ever built "yesterday" would leave those days permanently wrong.
32
+ * Recomputing a week costs one statement per day and makes lateness self-healing
33
+ * instead of a support question.
34
+ *
35
+ * Never throws — the caller is a scheduler tick, and a failed pass must retry next
36
+ * tick rather than take the service's background work down with it.
37
+ */
38
+ export declare function runRollupPass(now?: Date): Promise<{
39
+ days: number;
40
+ rows: number;
41
+ }>;
@@ -0,0 +1,173 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Building `execution_daily_rollups` from events.
5
+ *
6
+ * Every section that says "runs", "success rate", "p95" or "build minutes" reads
7
+ * these rows, so a quarterly report over a busy org costs a few hundred rows
8
+ * instead of a few hundred thousand — and keeps working after retention has swept
9
+ * the events it was built from.
10
+ *
11
+ * TWO PROPERTIES THE JOB HAS TO HAVE, and both shape the code:
12
+ *
13
+ * - IDEMPOTENT PER DAY. A day is recomputed WHOLESALE and upserted, never
14
+ * incremented. Incrementing is how a redelivered event, a retried tick or a
15
+ * crashed half-run silently inflates a count, and an inflated success rate is
16
+ * worse than a missing one because nobody can tell.
17
+ * - REBUILDS A DAY WHEN LATE EVENTS ARRIVE. The ingest's dead-letter queue is
18
+ * redriven hours later, so yesterday's numbers legitimately change. The
19
+ * `computed_at` stamp is what makes that visible rather than mysterious.
20
+ *
21
+ * THE SETTLE DELAY lives here too: a day is only rolled up once it has been closed
22
+ * long enough for in-flight executions to finish. Rolling up a day at 00:01 counts
23
+ * every execution still running at midnight as neither a success nor a failure.
24
+ */
25
+ import { createLogger, errorMessage, envInt } from '@pipeline-builder/api-core';
26
+ import { sql } from 'drizzle-orm';
27
+ import { withTenantTx } from '../../database/tenancy.js';
28
+ const logger = createLogger('daily-rollup');
29
+ /**
30
+ * How long after a day closes before it is rolled up (hours).
31
+ *
32
+ * Six by default, matching the report scheduler's own settle delay: both exist for
33
+ * the same reason, and a rollup that lagged the report would make a scheduled
34
+ * report read a half-built day.
35
+ */
36
+ export function rollupSettleHours() {
37
+ return envInt('REPORTING_ROLLUP_SETTLE_HOURS', 6, { min: 0 });
38
+ }
39
+ /** Rows per rebuild pass. Bounds the statement, not the backlog. */
40
+ export function rollupBatchDays() {
41
+ return envInt('REPORTING_ROLLUP_BATCH_DAYS', 7, { min: 1 });
42
+ }
43
+ /** The UTC midnight that starts `d`'s day. */
44
+ export function utcDayStart(d) {
45
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
46
+ }
47
+ /**
48
+ * Rebuild the rollup rows for one UTC day, for every org that had activity.
49
+ *
50
+ * ONE statement, computed entirely in Postgres. Deliberately not "select the
51
+ * events, aggregate in Node, write the rows": the whole point of the rollup is to
52
+ * avoid moving a day's events across the wire, and a job that did it to BUILD the
53
+ * rollup would just move the cost rather than remove it.
54
+ *
55
+ * Percentiles use `PERCENTILE_CONT`, which is the same function the on-demand
56
+ * queries use — so a rollup-backed number and a live number agree rather than
57
+ * differing by an interpolation rule.
58
+ */
59
+ export async function rebuildDay(day) {
60
+ const dayStart = utcDayStart(day);
61
+ const dayEnd = new Date(dayStart.getTime() + 24 * 3600_000);
62
+ // TWO GRAINS, one scan, and no GROUPING SETS needed: a PIPELINE event carries
63
+ // no stage name, so it collapses to the `''` pipeline-level row, and a STAGE
64
+ // event lands on its own stage row. The event shape gives the hierarchy for
65
+ // free.
66
+ //
67
+ // CTEs rather than window functions, because `PERCENTILE_CONT` is an ordered-set
68
+ // aggregate and cannot be used as a window function at all — the shape that
69
+ // looks natural here does not run.
70
+ const result = await withTenantTx((tx) => tx.execute(sql `
71
+ WITH ev AS (
72
+ SELECT
73
+ org_id,
74
+ pipeline_id::text AS pipeline_id,
75
+ COALESCE(stage_name, '') AS stage_name,
76
+ status,
77
+ failure_category,
78
+ duration_ms
79
+ FROM pipeline_events
80
+ WHERE created_at >= ${dayStart} AND created_at < ${dayEnd}
81
+ AND pipeline_id IS NOT NULL
82
+ AND event_type IN ('PIPELINE', 'STAGE')
83
+ ),
84
+ cats AS (
85
+ SELECT org_id, pipeline_id, stage_name,
86
+ jsonb_object_agg(failure_category, n) AS failures_by_category
87
+ FROM (
88
+ SELECT org_id, pipeline_id, stage_name, failure_category, COUNT(*)::int AS n
89
+ FROM ev
90
+ WHERE failure_category IS NOT NULL
91
+ GROUP BY 1, 2, 3, 4
92
+ ) t
93
+ GROUP BY 1, 2, 3
94
+ ),
95
+ agg AS (
96
+ SELECT
97
+ org_id, pipeline_id, stage_name,
98
+ COUNT(*)::int AS runs,
99
+ COUNT(*) FILTER (WHERE status IN ('SUCCEEDED', 'SUCCESS'))::int AS succeeded,
100
+ COUNT(*) FILTER (WHERE status IN ('FAILED', 'FAILURE', 'ERROR', 'TIMED_OUT'))::int AS failed,
101
+ -- NULL durations are ignored by PERCENTILE_CONT, which is what we want: an
102
+ -- event with no duration has no timing to contribute, and treating it as
103
+ -- zero would drag every percentile down.
104
+ PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY duration_ms)::int AS p50_ms,
105
+ PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration_ms)::int AS p90_ms,
106
+ PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms)::int AS p95_ms,
107
+ (SUM(COALESCE(duration_ms, 0)) / 1000)::int AS build_seconds
108
+ FROM ev
109
+ GROUP BY 1, 2, 3
110
+ )
111
+ INSERT INTO execution_daily_rollups (
112
+ org_id, day, pipeline_id, stage_name,
113
+ runs, succeeded, failed, failures_by_category,
114
+ p50_ms, p90_ms, p95_ms, build_seconds, computed_at
115
+ )
116
+ SELECT
117
+ a.org_id, ${dayStart}::timestamptz, a.pipeline_id, a.stage_name,
118
+ a.runs, a.succeeded, a.failed,
119
+ COALESCE(c.failures_by_category, '{}'::jsonb),
120
+ a.p50_ms, a.p90_ms, a.p95_ms, a.build_seconds, NOW()
121
+ FROM agg a
122
+ LEFT JOIN cats c
123
+ ON c.org_id = a.org_id AND c.pipeline_id = a.pipeline_id AND c.stage_name = a.stage_name
124
+ ON CONFLICT (org_id, day, pipeline_id, stage_name) DO UPDATE SET
125
+ runs = excluded.runs,
126
+ succeeded = excluded.succeeded,
127
+ failed = excluded.failed,
128
+ failures_by_category = excluded.failures_by_category,
129
+ p50_ms = excluded.p50_ms,
130
+ p90_ms = excluded.p90_ms,
131
+ p95_ms = excluded.p95_ms,
132
+ build_seconds = excluded.build_seconds,
133
+ computed_at = excluded.computed_at
134
+ `));
135
+ const rows = result.rowCount ?? 0;
136
+ logger.debug('Rolled up a day', { day: dayStart.toISOString().slice(0, 10), rows });
137
+ return rows;
138
+ }
139
+ /**
140
+ * One rollup pass: rebuild the most recent settled days.
141
+ *
142
+ * Rebuilding a WINDOW rather than only the newest day is what handles late
143
+ * events: the dead-letter redrive lands events for days already rolled up, and a
144
+ * job that only ever built "yesterday" would leave those days permanently wrong.
145
+ * Recomputing a week costs one statement per day and makes lateness self-healing
146
+ * instead of a support question.
147
+ *
148
+ * Never throws — the caller is a scheduler tick, and a failed pass must retry next
149
+ * tick rather than take the service's background work down with it.
150
+ */
151
+ export async function runRollupPass(now = new Date()) {
152
+ const settleMs = rollupSettleHours() * 3600_000;
153
+ // The newest day that has settled. A day is only built once everything that
154
+ // started in it has had time to finish.
155
+ const newest = utcDayStart(new Date(now.getTime() - settleMs));
156
+ let rows = 0;
157
+ let days = 0;
158
+ for (let i = 0; i < rollupBatchDays(); i++) {
159
+ const day = new Date(newest.getTime() - i * 24 * 3600_000);
160
+ try {
161
+ rows += await rebuildDay(day);
162
+ days++;
163
+ }
164
+ catch (err) {
165
+ logger.warn('Rollup pass failed for a day; it will be retried next pass', {
166
+ day: day.toISOString().slice(0, 10),
167
+ error: errorMessage(err),
168
+ });
169
+ }
170
+ }
171
+ return { days, rows };
172
+ }
173
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGFpbHktcm9sbHVwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvZGFpbHktcm9sbHVwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ2hGLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDbEMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRXpELE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxjQUFjLENBQUMsQ0FBQztBQUU1Qzs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLE9BQU8sTUFBTSxDQUFDLCtCQUErQixFQUFFLENBQUMsRUFBRSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFFRCxvRUFBb0U7QUFDcEUsTUFBTSxVQUFVLGVBQWU7SUFDN0IsT0FBTyxNQUFNLENBQUMsNkJBQTZCLEVBQUUsQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDOUQsQ0FBQztBQUVELDhDQUE4QztBQUM5QyxNQUFNLFVBQVUsV0FBVyxDQUFDLENBQU87SUFDakMsT0FBTyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxjQUFjLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUNqRixDQUFDO0FBRUQ7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFVBQVUsQ0FBQyxHQUFTO0lBQ3hDLE1BQU0sUUFBUSxHQUFHLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNsQyxNQUFNLE1BQU0sR0FBRyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRSxHQUFHLFFBQVEsQ0FBQyxDQUFDO0lBRTVELDhFQUE4RTtJQUM5RSw2RUFBNkU7SUFDN0UsNEVBQTRFO0lBQzVFLFFBQVE7SUFDUixFQUFFO0lBQ0YsaUZBQWlGO0lBQ2pGLDRFQUE0RTtJQUM1RSxtQ0FBbUM7SUFDbkMsTUFBTSxNQUFNLEdBQUcsTUFBTSxZQUFZLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFBOzs7Ozs7Ozs7OzRCQVU5QixRQUFRLHFCQUFxQixNQUFNOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O2tCQXFDN0MsUUFBUTs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQnZCLENBQUMsQ0FBQyxDQUFDO0lBQ0osTUFBTSxJQUFJLEdBQUksTUFBMkMsQ0FBQyxRQUFRLElBQUksQ0FBQyxDQUFDO0lBQ3hFLE1BQU0sQ0FBQyxLQUFLLENBQUMsaUJBQWlCLEVBQUUsRUFBRSxHQUFHLEVBQUUsUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNwRixPQUFPLElBQUksQ0FBQztBQUNkLENBQUM7QUFFRDs7Ozs7Ozs7Ozs7R0FXRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsYUFBYSxDQUFDLEdBQUcsR0FBUyxJQUFJLElBQUksRUFBRTtJQUN4RCxNQUFNLFFBQVEsR0FBRyxpQkFBaUIsRUFBRSxHQUFHLFFBQVEsQ0FBQztJQUNoRCw0RUFBNEU7SUFDNUUsd0NBQXdDO0lBQ3hDLE1BQU0sTUFBTSxHQUFHLFdBQVcsQ0FBQyxJQUFJLElBQUksQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUMvRCxJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7SUFDYixJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7SUFDYixLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsZUFBZSxFQUFFLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUMzQyxNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxRQUFRLENBQUMsQ0FBQztRQUMzRCxJQUFJLENBQUM7WUFDSCxJQUFJLElBQUksTUFBTSxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDOUIsSUFBSSxFQUFFLENBQUM7UUFDVCxDQUFDO1FBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztZQUNiLE1BQU0sQ0FBQyxJQUFJLENBQUMsNERBQTRELEVBQUU7Z0JBQ3hFLEdBQUcsRUFBRSxHQUFHLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUM7Z0JBQ25DLEtBQUssRUFBRSxZQUFZLENBQUMsR0FBRyxDQUFDO2FBQ3pCLENBQUMsQ0FBQztRQUNMLENBQUM7SUFDSCxDQUFDO0lBQ0QsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUN4QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8qKlxuICogQnVpbGRpbmcgYGV4ZWN1dGlvbl9kYWlseV9yb2xsdXBzYCBmcm9tIGV2ZW50cy5cbiAqXG4gKiBFdmVyeSBzZWN0aW9uIHRoYXQgc2F5cyBcInJ1bnNcIiwgXCJzdWNjZXNzIHJhdGVcIiwgXCJwOTVcIiBvciBcImJ1aWxkIG1pbnV0ZXNcIiByZWFkc1xuICogdGhlc2Ugcm93cywgc28gYSBxdWFydGVybHkgcmVwb3J0IG92ZXIgYSBidXN5IG9yZyBjb3N0cyBhIGZldyBodW5kcmVkIHJvd3NcbiAqIGluc3RlYWQgb2YgYSBmZXcgaHVuZHJlZCB0aG91c2FuZCDigJQgYW5kIGtlZXBzIHdvcmtpbmcgYWZ0ZXIgcmV0ZW50aW9uIGhhcyBzd2VwdFxuICogdGhlIGV2ZW50cyBpdCB3YXMgYnVpbHQgZnJvbS5cbiAqXG4gKiBUV08gUFJPUEVSVElFUyBUSEUgSk9CIEhBUyBUTyBIQVZFLCBhbmQgYm90aCBzaGFwZSB0aGUgY29kZTpcbiAqXG4gKiAgLSBJREVNUE9URU5UIFBFUiBEQVkuIEEgZGF5IGlzIHJlY29tcHV0ZWQgV0hPTEVTQUxFIGFuZCB1cHNlcnRlZCwgbmV2ZXJcbiAqICAgIGluY3JlbWVudGVkLiBJbmNyZW1lbnRpbmcgaXMgaG93IGEgcmVkZWxpdmVyZWQgZXZlbnQsIGEgcmV0cmllZCB0aWNrIG9yIGFcbiAqICAgIGNyYXNoZWQgaGFsZi1ydW4gc2lsZW50bHkgaW5mbGF0ZXMgYSBjb3VudCwgYW5kIGFuIGluZmxhdGVkIHN1Y2Nlc3MgcmF0ZSBpc1xuICogICAgd29yc2UgdGhhbiBhIG1pc3Npbmcgb25lIGJlY2F1c2Ugbm9ib2R5IGNhbiB0ZWxsLlxuICogIC0gUkVCVUlMRFMgQSBEQVkgV0hFTiBMQVRFIEVWRU5UUyBBUlJJVkUuIFRoZSBpbmdlc3QncyBkZWFkLWxldHRlciBxdWV1ZSBpc1xuICogICAgcmVkcml2ZW4gaG91cnMgbGF0ZXIsIHNvIHllc3RlcmRheSdzIG51bWJlcnMgbGVnaXRpbWF0ZWx5IGNoYW5nZS4gVGhlXG4gKiAgICBgY29tcHV0ZWRfYXRgIHN0YW1wIGlzIHdoYXQgbWFrZXMgdGhhdCB2aXNpYmxlIHJhdGhlciB0aGFuIG15c3RlcmlvdXMuXG4gKlxuICogVEhFIFNFVFRMRSBERUxBWSBsaXZlcyBoZXJlIHRvbzogYSBkYXkgaXMgb25seSByb2xsZWQgdXAgb25jZSBpdCBoYXMgYmVlbiBjbG9zZWRcbiAqIGxvbmcgZW5vdWdoIGZvciBpbi1mbGlnaHQgZXhlY3V0aW9ucyB0byBmaW5pc2guIFJvbGxpbmcgdXAgYSBkYXkgYXQgMDA6MDEgY291bnRzXG4gKiBldmVyeSBleGVjdXRpb24gc3RpbGwgcnVubmluZyBhdCBtaWRuaWdodCBhcyBuZWl0aGVyIGEgc3VjY2VzcyBub3IgYSBmYWlsdXJlLlxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciwgZXJyb3JNZXNzYWdlLCBlbnZJbnQgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgeyBzcWwgfSBmcm9tICdkcml6emxlLW9ybSc7XG5pbXBvcnQgeyB3aXRoVGVuYW50VHggfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdkYWlseS1yb2xsdXAnKTtcblxuLyoqXG4gKiBIb3cgbG9uZyBhZnRlciBhIGRheSBjbG9zZXMgYmVmb3JlIGl0IGlzIHJvbGxlZCB1cCAoaG91cnMpLlxuICpcbiAqIFNpeCBieSBkZWZhdWx0LCBtYXRjaGluZyB0aGUgcmVwb3J0IHNjaGVkdWxlcidzIG93biBzZXR0bGUgZGVsYXk6IGJvdGggZXhpc3QgZm9yXG4gKiB0aGUgc2FtZSByZWFzb24sIGFuZCBhIHJvbGx1cCB0aGF0IGxhZ2dlZCB0aGUgcmVwb3J0IHdvdWxkIG1ha2UgYSBzY2hlZHVsZWRcbiAqIHJlcG9ydCByZWFkIGEgaGFsZi1idWlsdCBkYXkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByb2xsdXBTZXR0bGVIb3VycygpOiBudW1iZXIge1xuICByZXR1cm4gZW52SW50KCdSRVBPUlRJTkdfUk9MTFVQX1NFVFRMRV9IT1VSUycsIDYsIHsgbWluOiAwIH0pO1xufVxuXG4vKiogUm93cyBwZXIgcmVidWlsZCBwYXNzLiBCb3VuZHMgdGhlIHN0YXRlbWVudCwgbm90IHRoZSBiYWNrbG9nLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJvbGx1cEJhdGNoRGF5cygpOiBudW1iZXIge1xuICByZXR1cm4gZW52SW50KCdSRVBPUlRJTkdfUk9MTFVQX0JBVENIX0RBWVMnLCA3LCB7IG1pbjogMSB9KTtcbn1cblxuLyoqIFRoZSBVVEMgbWlkbmlnaHQgdGhhdCBzdGFydHMgYGRgJ3MgZGF5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHV0Y0RheVN0YXJ0KGQ6IERhdGUpOiBEYXRlIHtcbiAgcmV0dXJuIG5ldyBEYXRlKERhdGUuVVRDKGQuZ2V0VVRDRnVsbFllYXIoKSwgZC5nZXRVVENNb250aCgpLCBkLmdldFVUQ0RhdGUoKSkpO1xufVxuXG4vKipcbiAqIFJlYnVpbGQgdGhlIHJvbGx1cCByb3dzIGZvciBvbmUgVVRDIGRheSwgZm9yIGV2ZXJ5IG9yZyB0aGF0IGhhZCBhY3Rpdml0eS5cbiAqXG4gKiBPTkUgc3RhdGVtZW50LCBjb21wdXRlZCBlbnRpcmVseSBpbiBQb3N0Z3Jlcy4gRGVsaWJlcmF0ZWx5IG5vdCBcInNlbGVjdCB0aGVcbiAqIGV2ZW50cywgYWdncmVnYXRlIGluIE5vZGUsIHdyaXRlIHRoZSByb3dzXCI6IHRoZSB3aG9sZSBwb2ludCBvZiB0aGUgcm9sbHVwIGlzIHRvXG4gKiBhdm9pZCBtb3ZpbmcgYSBkYXkncyBldmVudHMgYWNyb3NzIHRoZSB3aXJlLCBhbmQgYSBqb2IgdGhhdCBkaWQgaXQgdG8gQlVJTEQgdGhlXG4gKiByb2xsdXAgd291bGQganVzdCBtb3ZlIHRoZSBjb3N0IHJhdGhlciB0aGFuIHJlbW92ZSBpdC5cbiAqXG4gKiBQZXJjZW50aWxlcyB1c2UgYFBFUkNFTlRJTEVfQ09OVGAsIHdoaWNoIGlzIHRoZSBzYW1lIGZ1bmN0aW9uIHRoZSBvbi1kZW1hbmRcbiAqIHF1ZXJpZXMgdXNlIOKAlCBzbyBhIHJvbGx1cC1iYWNrZWQgbnVtYmVyIGFuZCBhIGxpdmUgbnVtYmVyIGFncmVlIHJhdGhlciB0aGFuXG4gKiBkaWZmZXJpbmcgYnkgYW4gaW50ZXJwb2xhdGlvbiBydWxlLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVidWlsZERheShkYXk6IERhdGUpOiBQcm9taXNlPG51bWJlcj4ge1xuICBjb25zdCBkYXlTdGFydCA9IHV0Y0RheVN0YXJ0KGRheSk7XG4gIGNvbnN0IGRheUVuZCA9IG5ldyBEYXRlKGRheVN0YXJ0LmdldFRpbWUoKSArIDI0ICogMzYwMF8wMDApO1xuXG4gIC8vIFRXTyBHUkFJTlMsIG9uZSBzY2FuLCBhbmQgbm8gR1JPVVBJTkcgU0VUUyBuZWVkZWQ6IGEgUElQRUxJTkUgZXZlbnQgY2Fycmllc1xuICAvLyBubyBzdGFnZSBuYW1lLCBzbyBpdCBjb2xsYXBzZXMgdG8gdGhlIGAnJ2AgcGlwZWxpbmUtbGV2ZWwgcm93LCBhbmQgYSBTVEFHRVxuICAvLyBldmVudCBsYW5kcyBvbiBpdHMgb3duIHN0YWdlIHJvdy4gVGhlIGV2ZW50IHNoYXBlIGdpdmVzIHRoZSBoaWVyYXJjaHkgZm9yXG4gIC8vIGZyZWUuXG4gIC8vXG4gIC8vIENURXMgcmF0aGVyIHRoYW4gd2luZG93IGZ1bmN0aW9ucywgYmVjYXVzZSBgUEVSQ0VOVElMRV9DT05UYCBpcyBhbiBvcmRlcmVkLXNldFxuICAvLyBhZ2dyZWdhdGUgYW5kIGNhbm5vdCBiZSB1c2VkIGFzIGEgd2luZG93IGZ1bmN0aW9uIGF0IGFsbCDigJQgdGhlIHNoYXBlIHRoYXRcbiAgLy8gbG9va3MgbmF0dXJhbCBoZXJlIGRvZXMgbm90IHJ1bi5cbiAgY29uc3QgcmVzdWx0ID0gYXdhaXQgd2l0aFRlbmFudFR4KCh0eCkgPT4gdHguZXhlY3V0ZShzcWxgXG4gICAgV0lUSCBldiBBUyAoXG4gICAgICBTRUxFQ1RcbiAgICAgICAgb3JnX2lkLFxuICAgICAgICBwaXBlbGluZV9pZDo6dGV4dCBBUyBwaXBlbGluZV9pZCxcbiAgICAgICAgQ09BTEVTQ0Uoc3RhZ2VfbmFtZSwgJycpIEFTIHN0YWdlX25hbWUsXG4gICAgICAgIHN0YXR1cyxcbiAgICAgICAgZmFpbHVyZV9jYXRlZ29yeSxcbiAgICAgICAgZHVyYXRpb25fbXNcbiAgICAgIEZST00gcGlwZWxpbmVfZXZlbnRzXG4gICAgICBXSEVSRSBjcmVhdGVkX2F0ID49ICR7ZGF5U3RhcnR9IEFORCBjcmVhdGVkX2F0IDwgJHtkYXlFbmR9XG4gICAgICAgIEFORCBwaXBlbGluZV9pZCBJUyBOT1QgTlVMTFxuICAgICAgICBBTkQgZXZlbnRfdHlwZSBJTiAoJ1BJUEVMSU5FJywgJ1NUQUdFJylcbiAgICApLFxuICAgIGNhdHMgQVMgKFxuICAgICAgU0VMRUNUIG9yZ19pZCwgcGlwZWxpbmVfaWQsIHN0YWdlX25hbWUsXG4gICAgICAgICAgICAganNvbmJfb2JqZWN0X2FnZyhmYWlsdXJlX2NhdGVnb3J5LCBuKSBBUyBmYWlsdXJlc19ieV9jYXRlZ29yeVxuICAgICAgRlJPTSAoXG4gICAgICAgIFNFTEVDVCBvcmdfaWQsIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lLCBmYWlsdXJlX2NhdGVnb3J5LCBDT1VOVCgqKTo6aW50IEFTIG5cbiAgICAgICAgRlJPTSBldlxuICAgICAgICBXSEVSRSBmYWlsdXJlX2NhdGVnb3J5IElTIE5PVCBOVUxMXG4gICAgICAgIEdST1VQIEJZIDEsIDIsIDMsIDRcbiAgICAgICkgdFxuICAgICAgR1JPVVAgQlkgMSwgMiwgM1xuICAgICksXG4gICAgYWdnIEFTIChcbiAgICAgIFNFTEVDVFxuICAgICAgICBvcmdfaWQsIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lLFxuICAgICAgICBDT1VOVCgqKTo6aW50IEFTIHJ1bnMsXG4gICAgICAgIENPVU5UKCopIEZJTFRFUiAoV0hFUkUgc3RhdHVzIElOICgnU1VDQ0VFREVEJywgJ1NVQ0NFU1MnKSk6OmludCBBUyBzdWNjZWVkZWQsXG4gICAgICAgIENPVU5UKCopIEZJTFRFUiAoV0hFUkUgc3RhdHVzIElOICgnRkFJTEVEJywgJ0ZBSUxVUkUnLCAnRVJST1InLCAnVElNRURfT1VUJykpOjppbnQgQVMgZmFpbGVkLFxuICAgICAgICAtLSBOVUxMIGR1cmF0aW9ucyBhcmUgaWdub3JlZCBieSBQRVJDRU5USUxFX0NPTlQsIHdoaWNoIGlzIHdoYXQgd2Ugd2FudDogYW5cbiAgICAgICAgLS0gZXZlbnQgd2l0aCBubyBkdXJhdGlvbiBoYXMgbm8gdGltaW5nIHRvIGNvbnRyaWJ1dGUsIGFuZCB0cmVhdGluZyBpdCBhc1xuICAgICAgICAtLSB6ZXJvIHdvdWxkIGRyYWcgZXZlcnkgcGVyY2VudGlsZSBkb3duLlxuICAgICAgICBQRVJDRU5USUxFX0NPTlQoMC41KSBXSVRISU4gR1JPVVAgKE9SREVSIEJZIGR1cmF0aW9uX21zKTo6aW50IEFTIHA1MF9tcyxcbiAgICAgICAgUEVSQ0VOVElMRV9DT05UKDAuOSkgV0lUSElOIEdST1VQIChPUkRFUiBCWSBkdXJhdGlvbl9tcyk6OmludCBBUyBwOTBfbXMsXG4gICAgICAgIFBFUkNFTlRJTEVfQ09OVCgwLjk1KSBXSVRISU4gR1JPVVAgKE9SREVSIEJZIGR1cmF0aW9uX21zKTo6aW50IEFTIHA5NV9tcyxcbiAgICAgICAgKFNVTShDT0FMRVNDRShkdXJhdGlvbl9tcywgMCkpIC8gMTAwMCk6OmludCBBUyBidWlsZF9zZWNvbmRzXG4gICAgICBGUk9NIGV2XG4gICAgICBHUk9VUCBCWSAxLCAyLCAzXG4gICAgKVxuICAgIElOU0VSVCBJTlRPIGV4ZWN1dGlvbl9kYWlseV9yb2xsdXBzIChcbiAgICAgIG9yZ19pZCwgZGF5LCBwaXBlbGluZV9pZCwgc3RhZ2VfbmFtZSxcbiAgICAgIHJ1bnMsIHN1Y2NlZWRlZCwgZmFpbGVkLCBmYWlsdXJlc19ieV9jYXRlZ29yeSxcbiAgICAgIHA1MF9tcywgcDkwX21zLCBwOTVfbXMsIGJ1aWxkX3NlY29uZHMsIGNvbXB1dGVkX2F0XG4gICAgKVxuICAgIFNFTEVDVFxuICAgICAgYS5vcmdfaWQsICR7ZGF5U3RhcnR9Ojp0aW1lc3RhbXB0eiwgYS5waXBlbGluZV9pZCwgYS5zdGFnZV9uYW1lLFxuICAgICAgYS5ydW5zLCBhLnN1Y2NlZWRlZCwgYS5mYWlsZWQsXG4gICAgICBDT0FMRVNDRShjLmZhaWx1cmVzX2J5X2NhdGVnb3J5LCAne30nOjpqc29uYiksXG4gICAgICBhLnA1MF9tcywgYS5wOTBfbXMsIGEucDk1X21zLCBhLmJ1aWxkX3NlY29uZHMsIE5PVygpXG4gICAgRlJPTSBhZ2cgYVxuICAgIExFRlQgSk9JTiBjYXRzIGNcbiAgICAgIE9OIGMub3JnX2lkID0gYS5vcmdfaWQgQU5EIGMucGlwZWxpbmVfaWQgPSBhLnBpcGVsaW5lX2lkIEFORCBjLnN0YWdlX25hbWUgPSBhLnN0YWdlX25hbWVcbiAgICBPTiBDT05GTElDVCAob3JnX2lkLCBkYXksIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lKSBETyBVUERBVEUgU0VUXG4gICAgICBydW5zID0gZXhjbHVkZWQucnVucyxcbiAgICAgIHN1Y2NlZWRlZCA9IGV4Y2x1ZGVkLnN1Y2NlZWRlZCxcbiAgICAgIGZhaWxlZCA9IGV4Y2x1ZGVkLmZhaWxlZCxcbiAgICAgIGZhaWx1cmVzX2J5X2NhdGVnb3J5ID0gZXhjbHVkZWQuZmFpbHVyZXNfYnlfY2F0ZWdvcnksXG4gICAgICBwNTBfbXMgPSBleGNsdWRlZC5wNTBfbXMsXG4gICAgICBwOTBfbXMgPSBleGNsdWRlZC5wOTBfbXMsXG4gICAgICBwOTVfbXMgPSBleGNsdWRlZC5wOTVfbXMsXG4gICAgICBidWlsZF9zZWNvbmRzID0gZXhjbHVkZWQuYnVpbGRfc2Vjb25kcyxcbiAgICAgIGNvbXB1dGVkX2F0ID0gZXhjbHVkZWQuY29tcHV0ZWRfYXRcbiAgYCkpO1xuICBjb25zdCByb3dzID0gKHJlc3VsdCBhcyB1bmtub3duIGFzIHsgcm93Q291bnQ/OiBudW1iZXIgfSkucm93Q291bnQgPz8gMDtcbiAgbG9nZ2VyLmRlYnVnKCdSb2xsZWQgdXAgYSBkYXknLCB7IGRheTogZGF5U3RhcnQudG9JU09TdHJpbmcoKS5zbGljZSgwLCAxMCksIHJvd3MgfSk7XG4gIHJldHVybiByb3dzO1xufVxuXG4vKipcbiAqIE9uZSByb2xsdXAgcGFzczogcmVidWlsZCB0aGUgbW9zdCByZWNlbnQgc2V0dGxlZCBkYXlzLlxuICpcbiAqIFJlYnVpbGRpbmcgYSBXSU5ET1cgcmF0aGVyIHRoYW4gb25seSB0aGUgbmV3ZXN0IGRheSBpcyB3aGF0IGhhbmRsZXMgbGF0ZVxuICogZXZlbnRzOiB0aGUgZGVhZC1sZXR0ZXIgcmVkcml2ZSBsYW5kcyBldmVudHMgZm9yIGRheXMgYWxyZWFkeSByb2xsZWQgdXAsIGFuZCBhXG4gKiBqb2IgdGhhdCBvbmx5IGV2ZXIgYnVpbHQgXCJ5ZXN0ZXJkYXlcIiB3b3VsZCBsZWF2ZSB0aG9zZSBkYXlzIHBlcm1hbmVudGx5IHdyb25nLlxuICogUmVjb21wdXRpbmcgYSB3ZWVrIGNvc3RzIG9uZSBzdGF0ZW1lbnQgcGVyIGRheSBhbmQgbWFrZXMgbGF0ZW5lc3Mgc2VsZi1oZWFsaW5nXG4gKiBpbnN0ZWFkIG9mIGEgc3VwcG9ydCBxdWVzdGlvbi5cbiAqXG4gKiBOZXZlciB0aHJvd3Mg4oCUIHRoZSBjYWxsZXIgaXMgYSBzY2hlZHVsZXIgdGljaywgYW5kIGEgZmFpbGVkIHBhc3MgbXVzdCByZXRyeSBuZXh0XG4gKiB0aWNrIHJhdGhlciB0aGFuIHRha2UgdGhlIHNlcnZpY2UncyBiYWNrZ3JvdW5kIHdvcmsgZG93biB3aXRoIGl0LlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcnVuUm9sbHVwUGFzcyhub3c6IERhdGUgPSBuZXcgRGF0ZSgpKTogUHJvbWlzZTx7IGRheXM6IG51bWJlcjsgcm93czogbnVtYmVyIH0+IHtcbiAgY29uc3Qgc2V0dGxlTXMgPSByb2xsdXBTZXR0bGVIb3VycygpICogMzYwMF8wMDA7XG4gIC8vIFRoZSBuZXdlc3QgZGF5IHRoYXQgaGFzIHNldHRsZWQuIEEgZGF5IGlzIG9ubHkgYnVpbHQgb25jZSBldmVyeXRoaW5nIHRoYXRcbiAgLy8gc3RhcnRlZCBpbiBpdCBoYXMgaGFkIHRpbWUgdG8gZmluaXNoLlxuICBjb25zdCBuZXdlc3QgPSB1dGNEYXlTdGFydChuZXcgRGF0ZShub3cuZ2V0VGltZSgpIC0gc2V0dGxlTXMpKTtcbiAgbGV0IHJvd3MgPSAwO1xuICBsZXQgZGF5cyA9IDA7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgcm9sbHVwQmF0Y2hEYXlzKCk7IGkrKykge1xuICAgIGNvbnN0IGRheSA9IG5ldyBEYXRlKG5ld2VzdC5nZXRUaW1lKCkgLSBpICogMjQgKiAzNjAwXzAwMCk7XG4gICAgdHJ5IHtcbiAgICAgIHJvd3MgKz0gYXdhaXQgcmVidWlsZERheShkYXkpO1xuICAgICAgZGF5cysrO1xuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgbG9nZ2VyLndhcm4oJ1JvbGx1cCBwYXNzIGZhaWxlZCBmb3IgYSBkYXk7IGl0IHdpbGwgYmUgcmV0cmllZCBuZXh0IHBhc3MnLCB7XG4gICAgICAgIGRheTogZGF5LnRvSVNPU3RyaW5nKCkuc2xpY2UoMCwgMTApLFxuICAgICAgICBlcnJvcjogZXJyb3JNZXNzYWdlKGVyciksXG4gICAgICB9KTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHsgZGF5cywgcm93cyB9O1xufVxuIl19
@@ -0,0 +1,78 @@
1
+ import type { LeadTimeGap } from '../../database/schema/reporting-analytics.js';
2
+ /** The fields of an inserted event row this module needs. */
3
+ export interface DeployCandidate {
4
+ orgId: string;
5
+ pipelineId: string | null;
6
+ executionId: string | null;
7
+ eventType: string;
8
+ status: string;
9
+ stageName: string | null;
10
+ environment: string | null;
11
+ commitSha?: string | null;
12
+ commitTimestamp?: Date | null;
13
+ commitCount?: number | null;
14
+ completedAt?: Date | null;
15
+ startedAt?: Date | null;
16
+ createdAt?: Date | null;
17
+ }
18
+ /**
19
+ * Is this row a deploy?
20
+ *
21
+ * A STAGE event with an environment, in a terminal state. `IN_PROGRESS` rows are
22
+ * skipped deliberately: a deploy that has started has no duration, no outcome and
23
+ * no place in a deployment-frequency count, and writing it would make every
24
+ * in-flight deploy look like a successful one until its next event landed.
25
+ */
26
+ export declare function isDeployEvent(row: DeployCandidate): boolean;
27
+ /**
28
+ * Why lead time is absent for a deploy that has no commit timestamp.
29
+ *
30
+ * `no_commit_data` is the honest default: the forwarder resolves the commit range
31
+ * in-account and reports the reason it could not, so anything else here would be
32
+ * this module guessing. The reason it DID report rides on the event's `detail`.
33
+ */
34
+ export declare function leadTimeGapOf(row: DeployCandidate & {
35
+ detail?: Record<string, unknown> | null;
36
+ }): LeadTimeGap | null;
37
+ /**
38
+ * Upsert the deploy rows for a batch of freshly-inserted events.
39
+ *
40
+ * ONE ROW PER (execution, environment), so a redelivered event refreshes the row
41
+ * rather than adding a second deploy — the same at-least-once problem the event
42
+ * insert solves with its partial unique index.
43
+ *
44
+ * The conflict update deliberately does NOT touch `failed_at` / `restored_at`:
45
+ * those are set by the post-deploy outcome markers and by incident correlation,
46
+ * and a late redelivery of the original deploy event must not erase a recorded
47
+ * production failure.
48
+ *
49
+ * Never throws. An ingest batch that landed its events has done the durable part
50
+ * of its job; failing the whole batch because a derived row could not be written
51
+ * would turn a rollup bug into data loss, and the backfill can rebuild these from
52
+ * the events while they exist.
53
+ */
54
+ export declare function upsertDeployments(rows: ReadonlyArray<DeployCandidate & {
55
+ detail?: Record<string, unknown> | null;
56
+ }>): Promise<number>;
57
+ /**
58
+ * Record a post-deploy outcome against the deploy it concerns.
59
+ *
60
+ * Denormalized onto `dora_deployments` so change-failure rate and
61
+ * time-to-restore are one scan over the window instead of a join per read. The
62
+ * `deployment_outcomes` table stays the authority — this is the copy the
63
+ * aggregates use, and it is rebuildable from it.
64
+ */
65
+ export declare function applyDeployOutcome(orgId: string, executionId: string, outcome: 'failed' | 'restored', at: Date, environment?: string): Promise<void>;
66
+ /**
67
+ * Attribute an incident to the most recent successful deploy to its environment
68
+ * within the correlation window, and label how confident that link is.
69
+ *
70
+ * The confidence is the point. "This deploy broke production" and "an incident
71
+ * opened eleven hours after this deploy, and the window happens to be twelve" are
72
+ * different claims, and a manager acts differently on each — so the report shows
73
+ * which one it is rather than presenting both as a change failure.
74
+ */
75
+ export declare function correlateIncident(orgId: string, environment: string, openedAt: Date, windowHours: number): Promise<{
76
+ executionId: string;
77
+ confidence: 'high' | 'medium' | 'low';
78
+ } | null>;
@@ -0,0 +1,203 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Writing `dora_deployments` at ingest.
5
+ *
6
+ * Every DORA number used to be derived by scanning `pipeline_events` per read.
7
+ * Two things were wrong with that, and only one of them was speed:
8
+ *
9
+ * 1. LEAD TIME WAS DOUBLE-COUNTED IN WAITING. A deploy emits a STAGE event and
10
+ * an ACTION event, and both carry the same resolved commit range. Any
11
+ * aggregate over events therefore counted one deploy's lead time twice the
12
+ * moment somebody added the ACTION rows to a query. Storing it once per
13
+ * execution makes that mistake unavailable rather than merely unmade.
14
+ * 2. THE NUMBERS DID NOT SURVIVE RETENTION. The sweep purges raw events by
15
+ * tier, so a quarterly report could not be recomputed once its events were
16
+ * gone — which is exactly when someone asks about it.
17
+ *
18
+ * A deploy is a STAGE event carrying an `environment`: the same predicate the
19
+ * DORA queries have always used, now applied once at write time.
20
+ */
21
+ import { createLogger, errorMessage } from '@pipeline-builder/api-core';
22
+ import { and, eq, isNull, sql } from 'drizzle-orm';
23
+ import { schema } from '../../database/drizzle-schema.js';
24
+ import { withTenantTx } from '../../database/tenancy.js';
25
+ const logger = createLogger('deploy-rollup');
26
+ /** Statuses an event uses for "this finished, and it worked". */
27
+ const SUCCEEDED = new Set(['SUCCEEDED', 'SUCCESS']);
28
+ /** …and for "this finished, and it did not". */
29
+ const FAILED = new Set(['FAILED', 'FAILURE', 'CANCELED', 'CANCELLED', 'STOPPED', 'TIMED_OUT']);
30
+ /**
31
+ * Is this row a deploy?
32
+ *
33
+ * A STAGE event with an environment, in a terminal state. `IN_PROGRESS` rows are
34
+ * skipped deliberately: a deploy that has started has no duration, no outcome and
35
+ * no place in a deployment-frequency count, and writing it would make every
36
+ * in-flight deploy look like a successful one until its next event landed.
37
+ */
38
+ export function isDeployEvent(row) {
39
+ return row.eventType === 'STAGE'
40
+ && !!row.environment
41
+ && !!row.executionId
42
+ && (SUCCEEDED.has(row.status) || FAILED.has(row.status));
43
+ }
44
+ /**
45
+ * Why lead time is absent for a deploy that has no commit timestamp.
46
+ *
47
+ * `no_commit_data` is the honest default: the forwarder resolves the commit range
48
+ * in-account and reports the reason it could not, so anything else here would be
49
+ * this module guessing. The reason it DID report rides on the event's `detail`.
50
+ */
51
+ export function leadTimeGapOf(row) {
52
+ if (row.commitTimestamp)
53
+ return null;
54
+ const reported = row.detail?.leadTimeGap;
55
+ if (reported === 'no_token' || reported === 'rate_limited' || reported === 'unsupported_source')
56
+ return reported;
57
+ return 'no_commit_data';
58
+ }
59
+ /** When the deploy finished. Falls back through the event's own timestamps. */
60
+ function deployedAtOf(row) {
61
+ return row.completedAt ?? row.startedAt ?? row.createdAt ?? null;
62
+ }
63
+ /**
64
+ * Upsert the deploy rows for a batch of freshly-inserted events.
65
+ *
66
+ * ONE ROW PER (execution, environment), so a redelivered event refreshes the row
67
+ * rather than adding a second deploy — the same at-least-once problem the event
68
+ * insert solves with its partial unique index.
69
+ *
70
+ * The conflict update deliberately does NOT touch `failed_at` / `restored_at`:
71
+ * those are set by the post-deploy outcome markers and by incident correlation,
72
+ * and a late redelivery of the original deploy event must not erase a recorded
73
+ * production failure.
74
+ *
75
+ * Never throws. An ingest batch that landed its events has done the durable part
76
+ * of its job; failing the whole batch because a derived row could not be written
77
+ * would turn a rollup bug into data loss, and the backfill can rebuild these from
78
+ * the events while they exist.
79
+ */
80
+ export async function upsertDeployments(rows) {
81
+ const deploys = rows.filter(isDeployEvent);
82
+ if (deploys.length === 0)
83
+ return 0;
84
+ const values = deploys.flatMap((row) => {
85
+ const deployedAt = deployedAtOf(row);
86
+ if (!deployedAt)
87
+ return [];
88
+ const succeeded = SUCCEEDED.has(row.status);
89
+ const earliestCommitAt = row.commitTimestamp ?? null;
90
+ // Lead time is commit → deploy, and only when both ends are real. A negative
91
+ // gap means the two timestamps came from different clocks; it is dropped
92
+ // rather than clamped to zero, because "0 seconds" is a claim and "unknown"
93
+ // is the truth.
94
+ const leadSeconds = earliestCommitAt
95
+ ? Math.round((deployedAt.getTime() - earliestCommitAt.getTime()) / 1000)
96
+ : null;
97
+ return [{
98
+ orgId: row.orgId,
99
+ executionId: row.executionId,
100
+ environment: row.environment,
101
+ pipelineId: row.pipelineId,
102
+ deployedAt,
103
+ succeeded,
104
+ earliestCommitAt,
105
+ leadTimeSeconds: leadSeconds !== null && leadSeconds >= 0 ? leadSeconds : null,
106
+ commitCount: row.commitCount ?? null,
107
+ commitSha: row.commitSha ?? null,
108
+ leadTimeGap: leadTimeGapOf(row),
109
+ }];
110
+ });
111
+ if (values.length === 0)
112
+ return 0;
113
+ try {
114
+ const landed = await withTenantTx((tx) => tx.insert(schema.doraDeployment).values(values)
115
+ .onConflictDoUpdate({
116
+ target: [schema.doraDeployment.executionId, schema.doraDeployment.environment],
117
+ set: {
118
+ succeeded: sql `excluded.succeeded`,
119
+ deployedAt: sql `excluded.deployed_at`,
120
+ pipelineId: sql `COALESCE(excluded.pipeline_id, ${schema.doraDeployment.pipelineId})`,
121
+ // A later event may resolve a commit range the first one could not, so
122
+ // these fill in — but never blank out what was already measured.
123
+ earliestCommitAt: sql `COALESCE(excluded.earliest_commit_at, ${schema.doraDeployment.earliestCommitAt})`,
124
+ leadTimeSeconds: sql `COALESCE(excluded.lead_time_seconds, ${schema.doraDeployment.leadTimeSeconds})`,
125
+ commitCount: sql `COALESCE(excluded.commit_count, ${schema.doraDeployment.commitCount})`,
126
+ commitSha: sql `COALESCE(excluded.commit_sha, ${schema.doraDeployment.commitSha})`,
127
+ // The gap is cleared once a commit timestamp arrives: a resolved lead
128
+ // time and a reason it could not be resolved must never coexist.
129
+ leadTimeGap: sql `CASE WHEN COALESCE(excluded.earliest_commit_at, ${schema.doraDeployment.earliestCommitAt}) IS NOT NULL
130
+ THEN NULL ELSE excluded.lead_time_gap END`,
131
+ updatedAt: new Date(),
132
+ },
133
+ })
134
+ .returning({ executionId: schema.doraDeployment.executionId }));
135
+ return landed.length;
136
+ }
137
+ catch (err) {
138
+ logger.warn('Deploy rollup upsert failed; the events landed and a rebuild can recover it', {
139
+ deploys: values.length,
140
+ error: errorMessage(err),
141
+ });
142
+ return 0;
143
+ }
144
+ }
145
+ /**
146
+ * Record a post-deploy outcome against the deploy it concerns.
147
+ *
148
+ * Denormalized onto `dora_deployments` so change-failure rate and
149
+ * time-to-restore are one scan over the window instead of a join per read. The
150
+ * `deployment_outcomes` table stays the authority — this is the copy the
151
+ * aggregates use, and it is rebuildable from it.
152
+ */
153
+ export async function applyDeployOutcome(orgId, executionId, outcome, at, environment) {
154
+ const where = environment
155
+ ? and(eq(schema.doraDeployment.executionId, executionId), eq(schema.doraDeployment.environment, environment), eq(schema.doraDeployment.orgId, orgId))
156
+ : and(eq(schema.doraDeployment.executionId, executionId), eq(schema.doraDeployment.orgId, orgId));
157
+ // A `restored` marker only means something for a deploy that was marked failed;
158
+ // recording a restore with no failure would produce a negative time-to-restore.
159
+ const set = outcome === 'failed'
160
+ ? { failedAt: at, updatedAt: new Date() }
161
+ : { restoredAt: at, updatedAt: new Date() };
162
+ await withTenantTx((tx) => tx.update(schema.doraDeployment).set(set)
163
+ .where(outcome === 'restored'
164
+ ? and(where, sql `${schema.doraDeployment.failedAt} IS NOT NULL`)
165
+ : where));
166
+ }
167
+ /**
168
+ * Attribute an incident to the most recent successful deploy to its environment
169
+ * within the correlation window, and label how confident that link is.
170
+ *
171
+ * The confidence is the point. "This deploy broke production" and "an incident
172
+ * opened eleven hours after this deploy, and the window happens to be twelve" are
173
+ * different claims, and a manager acts differently on each — so the report shows
174
+ * which one it is rather than presenting both as a change failure.
175
+ */
176
+ export async function correlateIncident(orgId, environment, openedAt, windowHours) {
177
+ const windowStart = new Date(openedAt.getTime() - windowHours * 3600_000);
178
+ const rows = await withTenantTx((tx) => tx.select({
179
+ executionId: schema.doraDeployment.executionId,
180
+ deployedAt: schema.doraDeployment.deployedAt,
181
+ }).from(schema.doraDeployment)
182
+ .where(and(eq(schema.doraDeployment.orgId, orgId), eq(schema.doraDeployment.environment, environment), eq(schema.doraDeployment.succeeded, true), sql `${schema.doraDeployment.deployedAt} <= ${openedAt}`, sql `${schema.doraDeployment.deployedAt} >= ${windowStart}`))
183
+ .orderBy(sql `${schema.doraDeployment.deployedAt} DESC`)
184
+ .limit(1));
185
+ const deploy = rows[0];
186
+ if (!deploy)
187
+ return null;
188
+ // Confidence by how early in the window it fell. The thirds are a judgement,
189
+ // and a stated one: an incident within the first third of the window after a
190
+ // deploy is the case the DORA definition is actually about.
191
+ const elapsed = openedAt.getTime() - deploy.deployedAt.getTime();
192
+ const windowMs = windowHours * 3600_000;
193
+ const confidence = elapsed <= windowMs / 3 ? 'high' : elapsed <= (windowMs * 2) / 3 ? 'medium' : 'low';
194
+ await withTenantTx((tx) => tx.update(schema.doraDeployment)
195
+ .set({ failedAt: openedAt, correlationConfidence: confidence, updatedAt: new Date() })
196
+ .where(and(eq(schema.doraDeployment.executionId, deploy.executionId), eq(schema.doraDeployment.environment, environment),
197
+ // Do not overwrite a failure already attributed: the FIRST incident linked
198
+ // to a deploy is the one that made it a change failure, and a later one
199
+ // re-dating it would move time-to-restore.
200
+ isNull(schema.doraDeployment.failedAt))));
201
+ return { executionId: deploy.executionId, confidence };
202
+ }
203
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95LXJvbGx1cC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9hcGkvcmVwb3J0aW5nL2RlcGxveS1yb2xsdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0Qzs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQkc7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDbkQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBRTFELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV6RCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsZUFBZSxDQUFDLENBQUM7QUFtQjdDLGlFQUFpRTtBQUNqRSxNQUFNLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFdBQVcsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0FBQ3BELGdEQUFnRDtBQUNoRCxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsV0FBVyxDQUFDLENBQUMsQ0FBQztBQUUvRjs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLGFBQWEsQ0FBQyxHQUFvQjtJQUNoRCxPQUFPLEdBQUcsQ0FBQyxTQUFTLEtBQUssT0FBTztXQUMzQixDQUFDLENBQUMsR0FBRyxDQUFDLFdBQVc7V0FDakIsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxXQUFXO1dBQ2pCLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMs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UGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8qKlxuICogV3JpdGluZyBgZG9yYV9kZXBsb3ltZW50c2AgYXQgaW5nZXN0LlxuICpcbiAqIEV2ZXJ5IERPUkEgbnVtYmVyIHVzZWQgdG8gYmUgZGVyaXZlZCBieSBzY2FubmluZyBgcGlwZWxpbmVfZXZlbnRzYCBwZXIgcmVhZC5cbiAqIFR3byB0aGluZ3Mgd2VyZSB3cm9uZyB3aXRoIHRoYXQsIGFuZCBvbmx5IG9uZSBvZiB0aGVtIHdhcyBzcGVlZDpcbiAqXG4gKiAgMS4gTEVBRCBUSU1FIFdBUyBET1VCTEUtQ09VTlRFRCBJTiBXQUlUSU5HLiBBIGRlcGxveSBlbWl0cyBhIFNUQUdFIGV2ZW50IGFuZFxuICogICAgIGFuIEFDVElPTiBldmVudCwgYW5kIGJvdGggY2FycnkgdGhlIHNhbWUgcmVzb2x2ZWQgY29tbWl0IHJhbmdlLiBBbnlcbiAqICAgICBhZ2dyZWdhdGUgb3ZlciBldmVudHMgdGhlcmVmb3JlIGNvdW50ZWQgb25lIGRlcGxveSdzIGxlYWQgdGltZSB0d2ljZSB0aGVcbiAqICAgICBtb21lbnQgc29tZWJvZHkgYWRkZWQgdGhlIEFDVElPTiByb3dzIHRvIGEgcXVlcnkuIFN0b3JpbmcgaXQgb25jZSBwZXJcbiAqICAgICBleGVjdXRpb24gbWFrZXMgdGhhdCBtaXN0YWtlIHVuYXZhaWxhYmxlIHJhdGhlciB0aGFuIG1lcmVseSB1bm1hZGUuXG4gKiAgMi4gVEhFIE5VTUJFUlMgRElEIE5PVCBTVVJWSVZFIFJFVEVOVElPTi4gVGhlIHN3ZWVwIHB1cmdlcyByYXcgZXZlbnRzIGJ5XG4gKiAgICAgdGllciwgc28gYSBxdWFydGVybHkgcmVwb3J0IGNvdWxkIG5vdCBiZSByZWNvbXB1dGVkIG9uY2UgaXRzIGV2ZW50cyB3ZXJlXG4gKiAgICAgZ29uZSDigJQgd2hpY2ggaXMgZXhhY3RseSB3aGVuIHNvbWVvbmUgYXNrcyBhYm91dCBpdC5cbiAqXG4gKiBBIGRlcGxveSBpcyBhIFNUQUdFIGV2ZW50IGNhcnJ5aW5nIGFuIGBlbnZpcm9ubWVudGA6IHRoZSBzYW1lIHByZWRpY2F0ZSB0aGVcbiAqIERPUkEgcXVlcmllcyBoYXZlIGFsd2F5cyB1c2VkLCBub3cgYXBwbGllZCBvbmNlIGF0IHdyaXRlIHRpbWUuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyLCBlcnJvck1lc3NhZ2UgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgeyBhbmQsIGVxLCBpc051bGwsIHNxbCB9IGZyb20gJ2RyaXp6bGUtb3JtJztcbmltcG9ydCB7IHNjaGVtYSB9IGZyb20gJy4uLy4uL2RhdGFiYXNlL2RyaXp6bGUtc2NoZW1hLmpzJztcbmltcG9ydCB0eXBlIHsgTGVhZFRpbWVHYXAgfSBmcm9tICcuLi8uLi9kYXRhYmFzZS9zY2hlbWEvcmVwb3J0aW5nLWFuYWx5dGljcy5qcyc7XG5pbXBvcnQgeyB3aXRoVGVuYW50VHggfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdkZXBsb3ktcm9sbHVwJyk7XG5cbi8qKiBUaGUgZmllbGRzIG9mIGFuIGluc2VydGVkIGV2ZW50IHJvdyB0aGlzIG1vZHVsZSBuZWVkcy4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRGVwbG95Q2FuZGlkYXRlIHtcbiAgb3JnSWQ6IHN0cmluZztcbiAgcGlwZWxpbmVJZDogc3RyaW5nIHwgbnVsbDtcbiAgZXhlY3V0aW9uSWQ6IHN0cmluZyB8IG51bGw7XG4gIGV2ZW50VHlwZTogc3RyaW5nO1xuICBzdGF0dXM6IHN0cmluZztcbiAgc3RhZ2VOYW1lOiBzdHJpbmcgfCBudWxsO1xuICBlbnZpcm9ubWVudDogc3RyaW5nIHwgbnVsbDtcbiAgY29tbWl0U2hhPzogc3RyaW5nIHwgbnVsbDtcbiAgY29tbWl0VGltZXN0YW1wPzogRGF0ZSB8IG51bGw7XG4gIGNvbW1pdENvdW50PzogbnVtYmVyIHwgbnVsbDtcbiAgY29tcGxldGVkQXQ/OiBEYXRlIHwgbnVsbDtcbiAgc3RhcnRlZEF0PzogRGF0ZSB8IG51bGw7XG4gIGNyZWF0ZWRBdD86IERhdGUgfCBudWxsO1xufVxuXG4vKiogU3RhdHVzZXMgYW4gZXZlbnQgdXNlcyBmb3IgXCJ0aGlzIGZpbmlzaGVkLCBhbmQgaXQgd29ya2VkXCIuICovXG5jb25zdCBTVUNDRUVERUQgPSBuZXcgU2V0KFsnU1VDQ0VFREVEJywgJ1NVQ0NFU1MnXSk7XG4vKiog4oCmYW5kIGZvciBcInRoaXMgZmluaXNoZWQsIGFuZCBpdCBkaWQgbm90XCIuICovXG5jb25zdCBGQUlMRUQgPSBuZXcgU2V0KFsnRkFJTEVEJywgJ0ZBSUxVUkUnLCAnQ0FOQ0VMRUQnLCAnQ0FOQ0VMTEVEJywgJ1NUT1BQRUQnLCAnVElNRURfT1VUJ10pO1xuXG4vKipcbiAqIElzIHRoaXMgcm93IGEgZGVwbG95P1xuICpcbiAqIEEgU1RBR0UgZXZlbnQgd2l0aCBhbiBlbnZpcm9ubWVudCwgaW4gYSB0ZXJtaW5hbCBzdGF0ZS4gYElOX1BST0dSRVNTYCByb3dzIGFyZVxuICogc2tpcHBlZCBkZWxpYmVyYXRlbHk6IGEgZGVwbG95IHRoYXQgaGFzIHN0YXJ0ZWQgaGFzIG5vIGR1cmF0aW9uLCBubyBvdXRjb21lIGFuZFxuICogbm8gcGxhY2UgaW4gYSBkZXBsb3ltZW50LWZyZXF1ZW5jeSBjb3VudCwgYW5kIHdyaXRpbmcgaXQgd291bGQgbWFrZSBldmVyeVxuICogaW4tZmxpZ2h0IGRlcGxveSBsb29rIGxpa2UgYSBzdWNjZXNzZnVsIG9uZSB1bnRpbCBpdHMgbmV4dCBldmVudCBsYW5kZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpc0RlcGxveUV2ZW50KHJvdzogRGVwbG95Q2FuZGlkYXRlKTogYm9vbGVhbiB7XG4gIHJldHVybiByb3cuZXZlbnRUeXBlID09PSAnU1RBR0UnXG4gICAgJiYgISFyb3cuZW52aXJvbm1lbnRcbiAgICAmJiAhIXJvdy5leGVjdXRpb25JZFxuICAgICYmIChTVUNDRUVERUQuaGFzKHJvdy5zdGF0dXMpIHx8IEZBSUxFRC5oYXMocm93LnN0YXR1cykpO1xufVxuXG4vKipcbiAqIFdoeSBsZWFkIHRpbWUgaXMgYWJzZW50IGZvciBhIGRlcGxveSB0aGF0IGhhcyBubyBjb21taXQgdGltZXN0YW1wLlxuICpcbiAqIGBub19jb21taXRfZGF0YWAgaXMgdGhlIGhvbmVzdCBkZWZhdWx0OiB0aGUgZm9yd2FyZGVyIHJlc29sdmVzIHRoZSBjb21taXQgcmFuZ2VcbiAqIGluLWFjY291bnQgYW5kIHJlcG9ydHMgdGhlIHJlYXNvbiBpdCBjb3VsZCBub3QsIHNvIGFueXRoaW5nIGVsc2UgaGVyZSB3b3VsZCBiZVxuICogdGhpcyBtb2R1bGUgZ3Vlc3NpbmcuIFRoZSByZWFzb24gaXQgRElEIHJlcG9ydCByaWRlcyBvbiB0aGUgZXZlbnQncyBgZGV0YWlsYC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGxlYWRUaW1lR2FwT2Yocm93OiBEZXBsb3lDYW5kaWRhdGUgJiB7IGRldGFpbD86IFJlY29yZDxzdHJpbmcsIHVua25vd24+IHwgbnVsbCB9KTogTGVhZFRpbWVHYXAgfCBudWxsIHtcbiAgaWYgKHJvdy5jb21taXRUaW1lc3RhbXApIHJldHVybiBudWxsO1xuICBjb25zdCByZXBvcnRlZCA9IHJvdy5kZXRhaWw/LmxlYWRUaW1lR2FwO1xuICBpZiAocmVwb3J0ZWQgPT09ICdub190b2tlbicgfHwgcmVwb3J0ZWQgPT09ICdyYXRlX2xpbWl0ZWQnIHx8IHJlcG9ydGVkID09PSAndW5zdXBwb3J0ZWRfc291cmNlJykgcmV0dXJuIHJlcG9ydGVkO1xuICByZXR1cm4gJ25vX2NvbW1pdF9kYXRhJztcbn1cblxuLyoqIFdoZW4gdGhlIGRlcGxveSBmaW5pc2hlZC4gRmFsbHMgYmFjayB0aHJvdWdoIHRoZSBldmVudCdzIG93biB0aW1lc3RhbXBzLiAqL1xuZnVuY3Rpb24gZGVwbG95ZWRBdE9mKHJvdzogRGVwbG95Q2FuZGlkYXRlKTogRGF0ZSB8IG51bGwge1xuICByZXR1cm4gcm93LmNvbXBsZXRlZEF0ID8/IHJvdy5zdGFydGVkQXQgPz8gcm93LmNyZWF0ZWRBdCA/PyBudWxsO1xufVxuXG4vKipcbiAqIFVwc2VydCB0aGUgZGVwbG95IHJvd3MgZm9yIGEgYmF0Y2ggb2YgZnJlc2hseS1pbnNlcnRlZCBldmVudHMuXG4gKlxuICogT05FIFJPVyBQRVIgKGV4ZWN1dGlvbiwgZW52aXJvbm1lbnQpLCBzbyBhIHJlZGVsaXZlcmVkIGV2ZW50IHJlZnJlc2hlcyB0aGUgcm93XG4gKiByYXRoZXIgdGhhbiBhZGRpbmcgYSBzZWNvbmQgZGVwbG95IOKAlCB0aGUgc2FtZSBhdC1sZWFzdC1vbmNlIHByb2JsZW0gdGhlIGV2ZW50XG4gKiBpbnNlcnQgc29sdmVzIHdpdGggaXRzIHBhcnRpYWwgdW5pcXVlIGluZGV4LlxuICpcbiAqIFRoZSBjb25mbGljdCB1cGRhdGUgZGVsaWJlcmF0ZWx5IGRvZXMgTk9UIHRvdWNoIGBmYWlsZWRfYXRgIC8gYHJlc3RvcmVkX2F0YDpcbiAqIHRob3NlIGFyZSBzZXQgYnkgdGhlIHBvc3QtZGVwbG95IG91dGNvbWUgbWFya2VycyBhbmQgYnkgaW5jaWRlbnQgY29ycmVsYXRpb24sXG4gKiBhbmQgYSBsYXRlIHJlZGVsaXZlcnkgb2YgdGhlIG9yaWdpbmFsIGRlcGxveSBldmVudCBtdXN0IG5vdCBlcmFzZSBhIHJlY29yZGVkXG4gKiBwcm9kdWN0aW9uIGZhaWx1cmUuXG4gKlxuICogTmV2ZXIgdGhyb3dzLiBBbiBpbmdlc3QgYmF0Y2ggdGhhdCBsYW5kZWQgaXRzIGV2ZW50cyBoYXMgZG9uZSB0aGUgZHVyYWJsZSBwYXJ0XG4gKiBvZiBpdHMgam9iOyBmYWlsaW5nIHRoZSB3aG9sZSBiYXRjaCBiZWNhdXNlIGEgZGVyaXZlZCByb3cgY291bGQgbm90IGJlIHdyaXR0ZW5cbiAqIHdvdWxkIHR1cm4gYSByb2xsdXAgYnVnIGludG8gZGF0YSBsb3NzLCBhbmQgdGhlIGJhY2tmaWxsIGNhbiByZWJ1aWxkIHRoZXNlIGZyb21cbiAqIHRoZSBldmVudHMgd2hpbGUgdGhleSBleGlzdC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHVwc2VydERlcGxveW1lbnRzKFxuICByb3dzOiBSZWFkb25seUFycmF5PERlcGxveUNhbmRpZGF0ZSAmIHsgZGV0YWlsPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCBudWxsIH0+LFxuKTogUHJvbWlzZTxudW1iZXI+IHtcbiAgY29uc3QgZGVwbG95cyA9IHJvd3MuZmlsdGVyKGlzRGVwbG95RXZlbnQpO1xuICBpZiAoZGVwbG95cy5sZW5ndGggPT09IDApIHJldHVybiAwO1xuXG4gIGNvbnN0IHZhbHVlcyA9IGRlcGxveXMuZmxhdE1hcCgocm93KSA9PiB7XG4gICAgY29uc3QgZGVwbG95ZWRBdCA9IGRlcGxveWVkQXRPZihyb3cpO1xuICAgIGlmICghZGVwbG95ZWRBdCkgcmV0dXJuIFtdO1xuICAgIGNvbnN0IHN1Y2NlZWRlZCA9IFNVQ0NFRURFRC5oYXMocm93LnN0YXR1cyk7XG4gICAgY29uc3QgZWFybGllc3RDb21taXRBdCA9IHJvdy5jb21taXRUaW1lc3RhbXAgPz8gbnVsbDtcbiAgICAvLyBMZWFkIHRpbWUgaXMgY29tbWl0IOKGkiBkZXBsb3ksIGFuZCBvbmx5IHdoZW4gYm90aCBlbmRzIGFyZSByZWFsLiBBIG5lZ2F0aXZlXG4gICAgLy8gZ2FwIG1lYW5zIHRoZSB0d28gdGltZXN0YW1wcyBjYW1lIGZyb20gZGlmZmVyZW50IGNsb2NrczsgaXQgaXMgZHJvcHBlZFxuICAgIC8vIHJhdGhlciB0aGFuIGNsYW1wZWQgdG8gemVybywgYmVjYXVzZSBcIjAgc2Vjb25kc1wiIGlzIGEgY2xhaW0gYW5kIFwidW5rbm93blwiXG4gICAgLy8gaXMgdGhlIHRydXRoLlxuICAgIGNvbnN0IGxlYWRTZWNvbmRzID0gZWFybGllc3RDb21taXRBdFxuICAgICAgPyBNYXRoLnJvdW5kKChkZXBsb3llZEF0LmdldFRpbWUoKSAtIGVhcmxpZXN0Q29tbWl0QXQuZ2V0VGltZSgpKSAvIDEwMDApXG4gICAgICA6IG51bGw7XG4gICAgcmV0dXJuIFt7XG4gICAgICBvcmdJZDogcm93Lm9yZ0lkLFxuICAgICAgZXhlY3V0aW9uSWQ6IHJvdy5leGVjdXRpb25JZCBhcyBzdHJpbmcsXG4gICAgICBlbnZpcm9ubWVudDogcm93LmVudmlyb25tZW50IGFzIHN0cmluZyxcbiAgICAgIHBpcGVsaW5lSWQ6IHJvdy5waXBlbGluZUlkLFxuICAgICAgZGVwbG95ZWRBdCxcbiAgICAgIHN1Y2NlZWRlZCxcbiAgICAgIGVhcmxpZXN0Q29tbWl0QXQsXG4gICAgICBsZWFkVGltZVNlY29uZHM6IGxlYWRTZWNvbmRzICE9PSBudWxsICYmIGxlYWRTZWNvbmRzID49IDAgPyBsZWFkU2Vjb25kcyA6IG51bGwsXG4gICAgICBjb21taXRDb3VudDogcm93LmNvbW1pdENvdW50ID8/IG51bGwsXG4gICAgICBjb21taXRTaGE6IHJvdy5jb21taXRTaGEgPz8gbnVsbCxcbiAgICAgIGxlYWRUaW1lR2FwOiBsZWFkVGltZUdhcE9mKHJvdyksXG4gICAgfV07XG4gIH0pO1xuICBpZiAodmFsdWVzLmxlbmd0aCA9PT0gMCkgcmV0dXJuIDA7XG5cbiAgdHJ5IHtcbiAgICBjb25zdCBsYW5kZWQgPSBhd2FpdCB3aXRoVGVuYW50VHgoKHR4KSA9PiB0eC5pbnNlcnQoc2NoZW1hLmRvcmFEZXBsb3ltZW50KS52YWx1ZXModmFsdWVzKVxuICAgICAgLm9uQ29uZmxpY3REb1VwZGF0ZSh7XG4gICAgICAgIHRhcmdldDogW3NjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCwgc2NoZW1hLmRvcmFEZXBsb3ltZW50LmVudmlyb25tZW50XSxcbiAgICAgICAgc2V0OiB7XG4gICAgICAgICAgc3VjY2VlZGVkOiBzcWxgZXhjbHVkZWQuc3VjY2VlZGVkYCxcbiAgICAgICAgICBkZXBsb3llZEF0OiBzcWxgZXhjbHVkZWQuZGVwbG95ZWRfYXRgLFxuICAgICAgICAgIHBpcGVsaW5lSWQ6IHNxbGBDT0FMRVNDRShleGNsdWRlZC5waXBlbGluZV9pZCwgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQucGlwZWxpbmVJZH0pYCxcbiAgICAgICAgICAvLyBBIGxhdGVyIGV2ZW50IG1heSByZXNvbHZlIGEgY29tbWl0IHJhbmdlIHRoZSBmaXJzdCBvbmUgY291bGQgbm90LCBzb1xuICAgICAgICAgIC8vIHRoZXNlIGZpbGwgaW4g4oCUIGJ1dCBuZXZlciBibGFuayBvdXQgd2hhdCB3YXMgYWxyZWFkeSBtZWFzdXJlZC5cbiAgICAgICAgICBlYXJsaWVzdENvbW1pdEF0OiBzcWxgQ09BTEVTQ0UoZXhjbHVkZWQuZWFybGllc3RfY29tbWl0X2F0LCAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5lYXJsaWVzdENvbW1pdEF0fSlgLFxuICAgICAgICAgIGxlYWRUaW1lU2Vjb25kczogc3FsYENPQUxFU0NFKGV4Y2x1ZGVkLmxlYWRfdGltZV9zZWNvbmRzLCAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5sZWFkVGltZVNlY29uZHN9KWAsXG4gICAgICAgICAgY29tbWl0Q291bnQ6IHNxbGBDT0FMRVNDRShleGNsdWRlZC5jb21taXRfY291bnQsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmNvbW1pdENvdW50fSlgLFxuICAgICAgICAgIGNvbW1pdFNoYTogc3FsYENPQUxFU0NFKGV4Y2x1ZGVkLmNvbW1pdF9zaGEsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmNvbW1pdFNoYX0pYCxcbiAgICAgICAgICAvLyBUaGUgZ2FwIGlzIGNsZWFyZWQgb25jZSBhIGNvbW1pdCB0aW1lc3RhbXAgYXJyaXZlczogYSByZXNvbHZlZCBsZWFkXG4gICAgICAgICAgLy8gdGltZSBhbmQgYSByZWFzb24gaXQgY291bGQgbm90IGJlIHJlc29sdmVkIG11c3QgbmV2ZXIgY29leGlzdC5cbiAgICAgICAgICBsZWFkVGltZUdhcDogc3FsYENBU0UgV0hFTiBDT0FMRVNDRShleGNsdWRlZC5lYXJsaWVzdF9jb21taXRfYXQsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmVhcmxpZXN0Q29tbWl0QXR9KSBJUyBOT1QgTlVMTFxuICAgICAgICAgICAgVEhFTiBOVUxMIEVMU0UgZXhjbHVkZWQubGVhZF90aW1lX2dhcCBFTkRgLFxuICAgICAgICAgIHVwZGF0ZWRBdDogbmV3IERhdGUoKSxcbiAgICAgICAgfSxcbiAgICAgIH0pXG4gICAgICAucmV0dXJuaW5nKHsgZXhlY3V0aW9uSWQ6IHNjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCB9KSk7XG4gICAgcmV0dXJuIChsYW5kZWQgYXMgdW5rbm93bltdKS5sZW5ndGg7XG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGxvZ2dlci53YXJuKCdEZXBsb3kgcm9sbHVwIHVwc2VydCBmYWlsZWQ7IHRoZSBldmVudHMgbGFuZGVkIGFuZCBhIHJlYnVpbGQgY2FuIHJlY292ZXIgaXQnLCB7XG4gICAgICBkZXBsb3lzOiB2YWx1ZXMubGVuZ3RoLFxuICAgICAgZXJyb3I6IGVycm9yTWVzc2FnZShlcnIpLFxuICAgIH0pO1xuICAgIHJldHVybiAwO1xuICB9XG59XG5cbi8qKlxuICogUmVjb3JkIGEgcG9zdC1kZXBsb3kgb3V0Y29tZSBhZ2FpbnN0IHRoZSBkZXBsb3kgaXQgY29uY2VybnMuXG4gKlxuICogRGVub3JtYWxpemVkIG9udG8gYGRvcmFfZGVwbG95bWVudHNgIHNvIGNoYW5nZS1mYWlsdXJlIHJhdGUgYW5kXG4gKiB0aW1lLXRvLXJlc3RvcmUgYXJlIG9uZSBzY2FuIG92ZXIgdGhlIHdpbmRvdyBpbnN0ZWFkIG9mIGEgam9pbiBwZXIgcmVhZC4gVGhlXG4gKiBgZGVwbG95bWVudF9vdXRjb21lc2AgdGFibGUgc3RheXMgdGhlIGF1dGhvcml0eSDigJQgdGhpcyBpcyB0aGUgY29weSB0aGVcbiAqIGFnZ3JlZ2F0ZXMgdXNlLCBhbmQgaXQgaXMgcmVidWlsZGFibGUgZnJvbSBpdC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGFwcGx5RGVwbG95T3V0Y29tZShcbiAgb3JnSWQ6IHN0cmluZyxcbiAgZXhlY3V0aW9uSWQ6IHN0cmluZyxcbiAgb3V0Y29tZTogJ2ZhaWxlZCcgfCAncmVzdG9yZWQnLFxuICBhdDogRGF0ZSxcbiAgZW52aXJvbm1lbnQ/OiBzdHJpbmcsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3Qgd2hlcmUgPSBlbnZpcm9ubWVudFxuICAgID8gYW5kKFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmV4ZWN1dGlvbklkLCBleGVjdXRpb25JZCksXG4gICAgICBlcShzY2hlbWEuZG9yYURlcGxveW1lbnQuZW52aXJvbm1lbnQsIGVudmlyb25tZW50KSxcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5vcmdJZCwgb3JnSWQpLFxuICAgIClcbiAgICA6IGFuZChcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCwgZXhlY3V0aW9uSWQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50Lm9yZ0lkLCBvcmdJZCksXG4gICAgKTtcbiAgLy8gQSBgcmVzdG9yZWRgIG1hcmtlciBvbmx5IG1lYW5zIHNvbWV0aGluZyBmb3IgYSBkZXBsb3kgdGhhdCB3YXMgbWFya2VkIGZhaWxlZDtcbiAgLy8gcmVjb3JkaW5nIGEgcmVzdG9yZSB3aXRoIG5vIGZhaWx1cmUgd291bGQgcHJvZHVjZSBhIG5lZ2F0aXZlIHRpbWUtdG8tcmVzdG9yZS5cbiAgY29uc3Qgc2V0ID0gb3V0Y29tZSA9PT0gJ2ZhaWxlZCdcbiAgICA/IHsgZmFpbGVkQXQ6IGF0LCB1cGRhdGVkQXQ6IG5ldyBEYXRlKCkgfVxuICAgIDogeyByZXN0b3JlZEF0OiBhdCwgdXBkYXRlZEF0OiBuZXcgRGF0ZSgpIH07XG4gIGF3YWl0IHdpdGhUZW5hbnRUeCgodHgpID0+IHR4LnVwZGF0ZShzY2hlbWEuZG9yYURlcGxveW1lbnQpLnNldChzZXQpXG4gICAgLndoZXJlKG91dGNvbWUgPT09ICdyZXN0b3JlZCdcbiAgICAgID8gYW5kKHdoZXJlLCBzcWxgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQuZmFpbGVkQXR9IElTIE5PVCBOVUxMYClcbiAgICAgIDogd2hlcmUpKTtcbn1cblxuLyoqXG4gKiBBdHRyaWJ1dGUgYW4gaW5jaWRlbnQgdG8gdGhlIG1vc3QgcmVjZW50IHN1Y2Nlc3NmdWwgZGVwbG95IHRvIGl0cyBlbnZpcm9ubWVudFxuICogd2l0aGluIHRoZSBjb3JyZWxhdGlvbiB3aW5kb3csIGFuZCBsYWJlbCBob3cgY29uZmlkZW50IHRoYXQgbGluayBpcy5cbiAqXG4gKiBUaGUgY29uZmlkZW5jZSBpcyB0aGUgcG9pbnQuIFwiVGhpcyBkZXBsb3kgYnJva2UgcHJvZHVjdGlvblwiIGFuZCBcImFuIGluY2lkZW50XG4gKiBvcGVuZWQgZWxldmVuIGhvdXJzIGFmdGVyIHRoaXMgZGVwbG95LCBhbmQgdGhlIHdpbmRvdyBoYXBwZW5zIHRvIGJlIHR3ZWx2ZVwiIGFyZVxuICogZGlmZmVyZW50IGNsYWltcywgYW5kIGEgbWFuYWdlciBhY3RzIGRpZmZlcmVudGx5IG9uIGVhY2gg4oCUIHNvIHRoZSByZXBvcnQgc2hvd3NcbiAqIHdoaWNoIG9uZSBpdCBpcyByYXRoZXIgdGhhbiBwcmVzZW50aW5nIGJvdGggYXMgYSBjaGFuZ2UgZmFpbHVyZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvcnJlbGF0ZUluY2lkZW50KFxuICBvcmdJZDogc3RyaW5nLFxuICBlbnZpcm9ubWVudDogc3RyaW5nLFxuICBvcGVuZWRBdDogRGF0ZSxcbiAgd2luZG93SG91cnM6IG51bWJlcixcbik6IFByb21pc2U8eyBleGVjdXRpb25JZDogc3RyaW5nOyBjb25maWRlbmNlOiAnaGlnaCcgfCAnbWVkaXVtJyB8ICdsb3cnIH0gfCBudWxsPiB7XG4gIGNvbnN0IHdpbmRvd1N0YXJ0ID0gbmV3IERhdGUob3BlbmVkQXQuZ2V0VGltZSgpIC0gd2luZG93SG91cnMgKiAzNjAwXzAwMCk7XG4gIGNvbnN0IHJvd3MgPSBhd2FpdCB3aXRoVGVuYW50VHgoKHR4KSA9PiB0eC5zZWxlY3Qoe1xuICAgIGV4ZWN1dGlvbklkOiBzY2hlbWEuZG9yYURlcGxveW1lbnQuZXhlY3V0aW9uSWQsXG4gICAgZGVwbG95ZWRBdDogc2NoZW1hLmRvcmFEZXBsb3ltZW50LmRlcGxveWVkQXQsXG4gIH0pLmZyb20oc2NoZW1hLmRvcmFEZXBsb3ltZW50KVxuICAgIC53aGVyZShhbmQoXG4gICAgICBlcShzY2hlbWEuZG9yYURlcGxveW1lbnQub3JnSWQsIG9yZ0lkKSxcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5lbnZpcm9ubWVudCwgZW52aXJvbm1lbnQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LnN1Y2NlZWRlZCwgdHJ1ZSksXG4gICAgICBzcWxgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQuZGVwbG95ZWRBdH0gPD0gJHtvcGVuZWRBdH1gLFxuICAgICAgc3FsYCR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmRlcGxveWVkQXR9ID49ICR7d2luZG93U3RhcnR9YCxcbiAgICApKVxuICAgIC5vcmRlckJ5KHNxbGAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5kZXBsb3llZEF0fSBERVNDYClcbiAgICAubGltaXQoMSkpO1xuICBjb25zdCBkZXBsb3kgPSAocm93cyBhcyBBcnJheTx7IGV4ZWN1dGlvbklkOiBzdHJpbmc7IGRlcGxveWVkQXQ6IERhdGUgfT4pWzBdO1xuICBpZiAoIWRlcGxveSkgcmV0dXJuIG51bGw7XG5cbiAgLy8gQ29uZmlkZW5jZSBieSBob3cgZWFybHkgaW4gdGhlIHdpbmRvdyBpdCBmZWxsLiBUaGUgdGhpcmRzIGFyZSBhIGp1ZGdlbWVudCxcbiAgLy8gYW5kIGEgc3RhdGVkIG9uZTogYW4gaW5jaWRlbnQgd2l0aGluIHRoZSBmaXJzdCB0aGlyZCBvZiB0aGUgd2luZG93IGFmdGVyIGFcbiAgLy8gZGVwbG95IGlzIHRoZSBjYXNlIHRoZSBET1JBIGRlZmluaXRpb24gaXMgYWN0dWFsbHkgYWJvdXQuXG4gIGNvbnN0IGVsYXBzZWQgPSBvcGVuZWRBdC5nZXRUaW1lKCkgLSBkZXBsb3kuZGVwbG95ZWRBdC5nZXRUaW1lKCk7XG4gIGNvbnN0IHdpbmRvd01zID0gd2luZG93SG91cnMgKiAzNjAwXzAwMDtcbiAgY29uc3QgY29uZmlkZW5jZSA9IGVsYXBzZWQgPD0gd2luZG93TXMgLyAzID8gJ2hpZ2gnIDogZWxhcHNlZCA8PSAod2luZG93TXMgKiAyKSAvIDMgPyAnbWVkaXVtJyA6ICdsb3cnO1xuXG4gIGF3YWl0IHdpdGhUZW5hbnRUeCgodHgpID0+IHR4LnVwZGF0ZShzY2hlbWEuZG9yYURlcGxveW1lbnQpXG4gICAgLnNldCh7IGZhaWxlZEF0OiBvcGVuZWRBdCwgY29ycmVsYXRpb25Db25maWRlbmNlOiBjb25maWRlbmNlLCB1cGRhdGVkQXQ6IG5ldyBEYXRlKCkgfSlcbiAgICAud2hlcmUoYW5kKFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmV4ZWN1dGlvbklkLCBkZXBsb3kuZXhlY3V0aW9uSWQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmVudmlyb25tZW50LCBlbnZpcm9ubWVudCksXG4gICAgICAvLyBEbyBub3Qgb3ZlcndyaXRlIGEgZmFpbHVyZSBhbHJlYWR5IGF0dHJpYnV0ZWQ6IHRoZSBGSVJTVCBpbmNpZGVudCBsaW5rZWRcbiAgICAgIC8vIHRvIGEgZGVwbG95IGlzIHRoZSBvbmUgdGhhdCBtYWRlIGl0IGEgY2hhbmdlIGZhaWx1cmUsIGFuZCBhIGxhdGVyIG9uZVxuICAgICAgLy8gcmUtZGF0aW5nIGl0IHdvdWxkIG1vdmUgdGltZS10by1yZXN0b3JlLlxuICAgICAgaXNOdWxsKHNjaGVtYS5kb3JhRGVwbG95bWVudC5mYWlsZWRBdCksXG4gICAgKSkpO1xuICByZXR1cm4geyBleGVjdXRpb25JZDogZGVwbG95LmV4ZWN1dGlvbklkLCBjb25maWRlbmNlIH07XG59XG4iXX0=
@@ -64,6 +64,20 @@ export declare function round(value: number, digits: number): number;
64
64
  * fixture tests able to prove the metric math without a live Postgres).
65
65
  */
66
66
  export declare function median(values: number[]): number | null;
67
+ /**
68
+ * The `p`-th percentile of a sample (0 < p < 100), or `null` when empty.
69
+ *
70
+ * Linear interpolation between order statistics — the same definition as
71
+ * Postgres's `PERCENTILE_CONT`, so a reader comparing this to a hand-written SQL
72
+ * query gets the same number. {@link median} is p50 by this definition too; it
73
+ * stays a separate function because it is the level-scoring input and is read in
74
+ * far more places.
75
+ *
76
+ * Tails matter here: a median lead time of 2 hours with a p95 of 3 weeks is a
77
+ * team with a fast path and a stuck queue, and the median alone hides that
78
+ * entirely. That is why the plan asks for p90/p95 rather than more levels.
79
+ */
80
+ export declare function percentile(values: number[], p: number): number | null;
67
81
  /**
68
82
  * Roll a group of deploy/stage events (grouped per execution) up to one terminal
69
83
  * status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).