@pipeline-builder/pipeline-data 4.1.24 → 4.1.26

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 +2244 -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 +1373 -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,93 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * The last commit this pipeline deployed to an environment — the exclusive lower
5
+ * bound the events Lambda needs to ask an SCM "what shipped since?".
6
+ *
7
+ * The Lambda keeps that bound in a warm-container map (`lastShaByPipeline` in
8
+ * `scm.ts`). A cold start empties it, so the next deploy resolved as a SINGLE
9
+ * commit: lead time came out as the age of the newest commit alone and
10
+ * `commitCount` as 1, both far too small, and nothing in the data marked the
11
+ * number as degraded. Lambda containers recycle constantly, so this was the
12
+ * common case, not an edge one.
13
+ *
14
+ * Reading it back from `commit_sha` — already persisted for every event that
15
+ * carried one — makes the bound survive a cold start without any new state.
16
+ */
17
+ import { createLogger } from '@pipeline-builder/api-core';
18
+ import { eq, sql } from 'drizzle-orm';
19
+ import { schema } from '../../database/drizzle-schema.js';
20
+ import { withTenantTx } from '../../database/tenancy.js';
21
+ import { drizzleRows } from '../crud-service.js';
22
+ const logger = createLogger('reporting-last-deploy');
23
+ const NONE = { commitSha: null, deployedAt: null };
24
+ /**
25
+ * The commit sha of the most recent SUCCEEDED deploy-stage execution for this
26
+ * pipeline — narrowed to one `environment` when given.
27
+ *
28
+ * Two steps, because the two facts live on different rows: `environment` is
29
+ * stamped on the deploy STAGE event, while `commit_sha` rides the source
30
+ * ACTION/PIPELINE event of the same execution. They are joined by `execution_id`,
31
+ * exactly as the DORA lead-time query does it.
32
+ *
33
+ * `environment` is OPTIONAL because of who asks. The events Lambda resolves a
34
+ * commit while handling the SOURCE event, and a source stage carries no
35
+ * environment — the execution has not reached a deploy stage yet, so at that
36
+ * moment the environment is genuinely unknown. Omitting it answers "what did this
37
+ * pipeline last ship anywhere", which is the right lower bound for an SCM range;
38
+ * passing it answers per-environment, for callers that know.
39
+ *
40
+ * TENANCY. `caller` is enforced the same way as {@link ingestEvents}: the org
41
+ * comes from the pipeline REGISTRY, never from the caller, and a pipeline outside
42
+ * the caller's allow-list returns nothing. Without this the endpoint would hand
43
+ * any `reporting:ingest` holder another tenant's commit history — the same hole
44
+ * the ingest tenancy check closes on the write side.
45
+ */
46
+ export async function getLastDeployedCommit(pipelineId, environment, caller) {
47
+ const unrestricted = caller === undefined || caller.crossTenant === true;
48
+ const allowed = new Set(caller?.allowedOrgIds ?? []);
49
+ return withTenantTx(async (tx) => {
50
+ const [registry] = await tx
51
+ .select({ orgId: schema.pipelineRegistry.orgId })
52
+ .from(schema.pipelineRegistry)
53
+ .where(eq(schema.pipelineRegistry.pipelineId, pipelineId));
54
+ // Unregistered pipeline: indistinguishable from "no deploys yet" to the
55
+ // caller, deliberately — an unregistered id must not confirm or deny existence.
56
+ if (!registry)
57
+ return NONE;
58
+ if (!unrestricted && !allowed.has(registry.orgId)) {
59
+ logger.warn('Refused last-deploy-commit lookup outside the caller org scope', {
60
+ pipelineId, foreignOrgId: registry.orgId, allowedOrgIds: [...allowed],
61
+ });
62
+ return NONE;
63
+ }
64
+ const rows = drizzleRows((await tx.execute(sql `
65
+ WITH last_deploy AS (
66
+ SELECT e.execution_id AS execution_id, e.completed_at AS completed_at
67
+ FROM ${schema.pipelineEvent} e
68
+ WHERE e.pipeline_id = ${pipelineId}
69
+ AND e.org_id = ${registry.orgId}
70
+ ${environment ? sql `AND e.environment = ${environment}` : sql `AND e.environment IS NOT NULL`}
71
+ AND e.event_type = 'STAGE'
72
+ AND e.status = 'SUCCEEDED'
73
+ AND e.completed_at IS NOT NULL
74
+ ORDER BY e.completed_at DESC
75
+ LIMIT 1
76
+ )
77
+ SELECT c.commit_sha AS commit_sha, ld.completed_at::text AS completed_at
78
+ FROM last_deploy ld
79
+ JOIN ${schema.pipelineEvent} c
80
+ ON c.execution_id = ld.execution_id
81
+ AND c.pipeline_id = ${pipelineId}
82
+ AND c.org_id = ${registry.orgId}
83
+ WHERE c.commit_sha IS NOT NULL
84
+ ORDER BY c.created_at DESC
85
+ LIMIT 1
86
+ `)).rows);
87
+ const row = rows[0];
88
+ if (!row)
89
+ return NONE;
90
+ return { commitSha: row.commit_sha ?? null, deployedAt: row.completed_at ?? null };
91
+ });
92
+ }
93
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGFzdC1kZXBsb3kuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBpL3JlcG9ydGluZy9sYXN0LWRlcGxveS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7Ozs7O0dBYUc7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDMUQsT0FBTyxFQUFFLEVBQUUsRUFBRSxHQUFHLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFdEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBQzFELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUN6RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFakQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLHVCQUF1QixDQUFDLENBQUM7QUFVckQsTUFBTSxJQUFJLEdBQXVCLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLENBQUM7QUFFdkU7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUscUJBQXFCLENBQ3pDLFVBQWtCLEVBQ2xCLFdBQW9CLEVBQ3BCLE1BQXFCO0lBRXJCLE1BQU0sWUFBWSxHQUFHLE1BQU0sS0FBSyxTQUFTLElBQUksTUFBTSxDQUFDLFdBQVcsS0FBSyxJQUFJLENBQUM7SUFDekUsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxFQUFFLGFBQWEsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUVyRCxPQUFPLFlBQVksQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEVBQUU7UUFDL0IsTUFBTSxDQUFDLFFBQVEsQ0FBQyxHQUFHLE1BQU0sRUFBRTthQUN4QixNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLGdCQUFnQixDQUFDLEtBQUssRUFBRSxDQUFDO2FBQ2hELElBQUksQ0FBQyxNQUFNLENBQUMsZ0JBQWdCLENBQUM7YUFDN0IsS0FBSyxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsZ0JBQWdCLENBQUMsVUFBVSxFQUFFLFVBQVUsQ0FBQyxDQUFDLENBQUM7UUFDN0Qsd0VBQXdFO1FBQ3hFLGdGQUFnRjtRQUNoRixJQUFJLENBQUMsUUFBUTtZQUFFLE9BQU8sSUFBSSxDQUFDO1FBRTNCLElBQUksQ0FBQyxZQUFZLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1lBQ2xELE1BQU0sQ0FBQyxJQUFJLENBQUMsZ0VBQWdFLEVBQUU7Z0JBQzVFLFVBQVUsRUFBRSxZQUFZLEVBQUUsUUFBUSxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsQ0FBQyxHQUFHLE9BQU8sQ0FBQzthQUN0RSxDQUFDLENBQUM7WUFDSCxPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCxNQUFNLElBQUksR0FBRyxXQUFXLENBQTZELENBQUMsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQTs7O2VBRy9GLE1BQU0sQ0FBQyxhQUFhO2dDQUNILFVBQVU7MkJBQ2YsUUFBUSxDQUFDLEtBQUs7WUFDN0IsV0FBVyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUEsdUJBQXVCLFdBQVcsRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUEsK0JBQStCOzs7Ozs7Ozs7YUFTekYsTUFBTSxDQUFDLGFBQWE7OzZCQUVKLFVBQVU7d0JBQ2YsUUFBUSxDQUFDLEtBQUs7Ozs7S0FJakMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFVixNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDcEIsSUFBSSxDQUFDLEdBQUc7WUFBRSxPQUFPLElBQUksQ0FBQztRQUN0QixPQUFPLEVBQUUsU0FBUyxFQUFFLEdBQUcsQ0FBQyxVQUFVLElBQUksSUFBSSxFQUFFLFVBQVUsRUFBRSxHQUFHLENBQUMsWUFBWSxJQUFJLElBQUksRUFBRSxDQUFDO0lBQ3JGLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFRoZSBsYXN0IGNvbW1pdCB0aGlzIHBpcGVsaW5lIGRlcGxveWVkIHRvIGFuIGVudmlyb25tZW50IOKAlCB0aGUgZXhjbHVzaXZlIGxvd2VyXG4gKiBib3VuZCB0aGUgZXZlbnRzIExhbWJkYSBuZWVkcyB0byBhc2sgYW4gU0NNIFwid2hhdCBzaGlwcGVkIHNpbmNlP1wiLlxuICpcbiAqIFRoZSBMYW1iZGEga2VlcHMgdGhhdCBib3VuZCBpbiBhIHdhcm0tY29udGFpbmVyIG1hcCAoYGxhc3RTaGFCeVBpcGVsaW5lYCBpblxuICogYHNjbS50c2ApLiBBIGNvbGQgc3RhcnQgZW1wdGllcyBpdCwgc28gdGhlIG5leHQgZGVwbG95IHJlc29sdmVkIGFzIGEgU0lOR0xFXG4gKiBjb21taXQ6IGxlYWQgdGltZSBjYW1lIG91dCBhcyB0aGUgYWdlIG9mIHRoZSBuZXdlc3QgY29tbWl0IGFsb25lIGFuZFxuICogYGNvbW1pdENvdW50YCBhcyAxLCBib3RoIGZhciB0b28gc21hbGwsIGFuZCBub3RoaW5nIGluIHRoZSBkYXRhIG1hcmtlZCB0aGVcbiAqIG51bWJlciBhcyBkZWdyYWRlZC4gTGFtYmRhIGNvbnRhaW5lcnMgcmVjeWNsZSBjb25zdGFudGx5LCBzbyB0aGlzIHdhcyB0aGVcbiAqIGNvbW1vbiBjYXNlLCBub3QgYW4gZWRnZSBvbmUuXG4gKlxuICogUmVhZGluZyBpdCBiYWNrIGZyb20gYGNvbW1pdF9zaGFgIOKAlCBhbHJlYWR5IHBlcnNpc3RlZCBmb3IgZXZlcnkgZXZlbnQgdGhhdFxuICogY2FycmllZCBvbmUg4oCUIG1ha2VzIHRoZSBib3VuZCBzdXJ2aXZlIGEgY29sZCBzdGFydCB3aXRob3V0IGFueSBuZXcgc3RhdGUuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHsgZXEsIHNxbCB9IGZyb20gJ2RyaXp6bGUtb3JtJztcbmltcG9ydCB0eXBlIHsgSW5nZXN0Q2FsbGVyIH0gZnJvbSAnLi90eXBlcy5qcyc7XG5pbXBvcnQgeyBzY2hlbWEgfSBmcm9tICcuLi8uLi9kYXRhYmFzZS9kcml6emxlLXNjaGVtYS5qcyc7XG5pbXBvcnQgeyB3aXRoVGVuYW50VHggfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcbmltcG9ydCB7IGRyaXp6bGVSb3dzIH0gZnJvbSAnLi4vY3J1ZC1zZXJ2aWNlLmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdyZXBvcnRpbmctbGFzdC1kZXBsb3knKTtcblxuLyoqIFRoZSBib3VuZCwgb3IgbnVsbHMgd2hlbiB0aGlzIHBpcGVsaW5lIGhhcyBubyByZWNvcmRlZCBkZXBsb3kgY2FycnlpbmcgYSBjb21taXQuICovXG5leHBvcnQgaW50ZXJmYWNlIExhc3REZXBsb3llZENvbW1pdCB7XG4gIC8qKiBDb21taXQgc2hhIG9mIHRoZSBtb3N0IHJlY2VudCBzdWNjZXNzZnVsIGRlcGxveSB0byB0aGUgZW52aXJvbm1lbnQuICovXG4gIGNvbW1pdFNoYTogc3RyaW5nIHwgbnVsbDtcbiAgLyoqIFdoZW4gdGhhdCBkZXBsb3kgY29tcGxldGVkLCBJU08gdGV4dCDigJQgZm9yIHRoZSBjYWxsZXIncyBvd24gbG9nZ2luZy4gKi9cbiAgZGVwbG95ZWRBdDogc3RyaW5nIHwgbnVsbDtcbn1cblxuY29uc3QgTk9ORTogTGFzdERlcGxveWVkQ29tbWl0ID0geyBjb21taXRTaGE6IG51bGwsIGRlcGxveWVkQXQ6IG51bGwgfTtcblxuLyoqXG4gKiBUaGUgY29tbWl0IHNoYSBvZiB0aGUgbW9zdCByZWNlbnQgU1VDQ0VFREVEIGRlcGxveS1zdGFnZSBleGVjdXRpb24gZm9yIHRoaXNcbiAqIHBpcGVsaW5lIOKAlCBuYXJyb3dlZCB0byBvbmUgYGVudmlyb25tZW50YCB3aGVuIGdpdmVuLlxuICpcbiAqIFR3byBzdGVwcywgYmVjYXVzZSB0aGUgdHdvIGZhY3RzIGxpdmUgb24gZGlmZmVyZW50IHJvd3M6IGBlbnZpcm9ubWVudGAgaXNcbiAqIHN0YW1wZWQgb24gdGhlIGRlcGxveSBTVEFHRSBldmVudCwgd2hpbGUgYGNvbW1pdF9zaGFgIHJpZGVzIHRoZSBzb3VyY2VcbiAqIEFDVElPTi9QSVBFTElORSBldmVudCBvZiB0aGUgc2FtZSBleGVjdXRpb24uIFRoZXkgYXJlIGpvaW5lZCBieSBgZXhlY3V0aW9uX2lkYCxcbiAqIGV4YWN0bHkgYXMgdGhlIERPUkEgbGVhZC10aW1lIHF1ZXJ5IGRvZXMgaXQuXG4gKlxuICogYGVudmlyb25tZW50YCBpcyBPUFRJT05BTCBiZWNhdXNlIG9mIHdobyBhc2tzLiBUaGUgZXZlbnRzIExhbWJkYSByZXNvbHZlcyBhXG4gKiBjb21taXQgd2hpbGUgaGFuZGxpbmcgdGhlIFNPVVJDRSBldmVudCwgYW5kIGEgc291cmNlIHN0YWdlIGNhcnJpZXMgbm9cbiAqIGVudmlyb25tZW50IOKAlCB0aGUgZXhlY3V0aW9uIGhhcyBub3QgcmVhY2hlZCBhIGRlcGxveSBzdGFnZSB5ZXQsIHNvIGF0IHRoYXRcbiAqIG1vbWVudCB0aGUgZW52aXJvbm1lbnQgaXMgZ2VudWluZWx5IHVua25vd24uIE9taXR0aW5nIGl0IGFuc3dlcnMgXCJ3aGF0IGRpZCB0aGlzXG4gKiBwaXBlbGluZSBsYXN0IHNoaXAgYW55d2hlcmVcIiwgd2hpY2ggaXMgdGhlIHJpZ2h0IGxvd2VyIGJvdW5kIGZvciBhbiBTQ00gcmFuZ2U7XG4gKiBwYXNzaW5nIGl0IGFuc3dlcnMgcGVyLWVudmlyb25tZW50LCBmb3IgY2FsbGVycyB0aGF0IGtub3cuXG4gKlxuICogVEVOQU5DWS4gYGNhbGxlcmAgaXMgZW5mb3JjZWQgdGhlIHNhbWUgd2F5IGFzIHtAbGluayBpbmdlc3RFdmVudHN9OiB0aGUgb3JnXG4gKiBjb21lcyBmcm9tIHRoZSBwaXBlbGluZSBSRUdJU1RSWSwgbmV2ZXIgZnJvbSB0aGUgY2FsbGVyLCBhbmQgYSBwaXBlbGluZSBvdXRzaWRlXG4gKiB0aGUgY2FsbGVyJ3MgYWxsb3ctbGlzdCByZXR1cm5zIG5vdGhpbmcuIFdpdGhvdXQgdGhpcyB0aGUgZW5kcG9pbnQgd291bGQgaGFuZFxuICogYW55IGByZXBvcnRpbmc6aW5nZXN0YCBob2xkZXIgYW5vdGhlciB0ZW5hbnQncyBjb21taXQgaGlzdG9yeSDigJQgdGhlIHNhbWUgaG9sZVxuICogdGhlIGluZ2VzdCB0ZW5hbmN5IGNoZWNrIGNsb3NlcyBvbiB0aGUgd3JpdGUgc2lkZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGdldExhc3REZXBsb3llZENvbW1pdChcbiAgcGlwZWxpbmVJZDogc3RyaW5nLFxuICBlbnZpcm9ubWVudD86IHN0cmluZyxcbiAgY2FsbGVyPzogSW5nZXN0Q2FsbGVyLFxuKTogUHJvbWlzZTxMYXN0RGVwbG95ZWRDb21taXQ+IHtcbiAgY29uc3QgdW5yZXN0cmljdGVkID0gY2FsbGVyID09PSB1bmRlZmluZWQgfHwgY2FsbGVyLmNyb3NzVGVuYW50ID09PSB0cnVlO1xuICBjb25zdCBhbGxvd2VkID0gbmV3IFNldChjYWxsZXI/LmFsbG93ZWRPcmdJZHMgPz8gW10pO1xuXG4gIHJldHVybiB3aXRoVGVuYW50VHgoYXN5bmMgKHR4KSA9PiB7XG4gICAgY29uc3QgW3JlZ2lzdHJ5XSA9IGF3YWl0IHR4XG4gICAgICAuc2VsZWN0KHsgb3JnSWQ6IHNjaGVtYS5waXBlbGluZVJlZ2lzdHJ5Lm9yZ0lkIH0pXG4gICAgICAuZnJvbShzY2hlbWEucGlwZWxpbmVSZWdpc3RyeSlcbiAgICAgIC53aGVyZShlcShzY2hlbWEucGlwZWxpbmVSZWdpc3RyeS5waXBlbGluZUlkLCBwaXBlbGluZUlkKSk7XG4gICAgLy8gVW5yZWdpc3RlcmVkIHBpcGVsaW5lOiBpbmRpc3Rpbmd1aXNoYWJsZSBmcm9tIFwibm8gZGVwbG95cyB5ZXRcIiB0byB0aGVcbiAgICAvLyBjYWxsZXIsIGRlbGliZXJhdGVseSDigJQgYW4gdW5yZWdpc3RlcmVkIGlkIG11c3Qgbm90IGNvbmZpcm0gb3IgZGVueSBleGlzdGVuY2UuXG4gICAgaWYgKCFyZWdpc3RyeSkgcmV0dXJuIE5PTkU7XG5cbiAgICBpZiAoIXVucmVzdHJpY3RlZCAmJiAhYWxsb3dlZC5oYXMocmVnaXN0cnkub3JnSWQpKSB7XG4gICAgICBsb2dnZXIud2FybignUmVmdXNlZCBsYXN0LWRlcGxveS1jb21taXQgbG9va3VwIG91dHNpZGUgdGhlIGNhbGxlciBvcmcgc2NvcGUnLCB7XG4gICAgICAgIHBpcGVsaW5lSWQsIGZvcmVpZ25PcmdJZDogcmVnaXN0cnkub3JnSWQsIGFsbG93ZWRPcmdJZHM6IFsuLi5hbGxvd2VkXSxcbiAgICAgIH0pO1xuICAgICAgcmV0dXJuIE5PTkU7XG4gICAgfVxuXG4gICAgY29uc3Qgcm93cyA9IGRyaXp6bGVSb3dzPHsgY29tbWl0X3NoYTogc3RyaW5nIHwgbnVsbDsgY29tcGxldGVkX2F0OiBzdHJpbmcgfCBudWxsIH0+KChhd2FpdCB0eC5leGVjdXRlKHNxbGBcbiAgICAgIFdJVEggbGFzdF9kZXBsb3kgQVMgKFxuICAgICAgICBTRUxFQ1QgZS5leGVjdXRpb25faWQgQVMgZXhlY3V0aW9uX2lkLCBlLmNvbXBsZXRlZF9hdCBBUyBjb21wbGV0ZWRfYXRcbiAgICAgICAgRlJPTSAke3NjaGVtYS5waXBlbGluZUV2ZW50fSBlXG4gICAgICAgIFdIRVJFIGUucGlwZWxpbmVfaWQgPSAke3BpcGVsaW5lSWR9XG4gICAgICAgICAgQU5EIGUub3JnX2lkID0gJHtyZWdpc3RyeS5vcmdJZH1cbiAgICAgICAgICAke2Vudmlyb25tZW50ID8gc3FsYEFORCBlLmVudmlyb25tZW50ID0gJHtlbnZpcm9ubWVudH1gIDogc3FsYEFORCBlLmVudmlyb25tZW50IElTIE5PVCBOVUxMYH1cbiAgICAgICAgICBBTkQgZS5ldmVudF90eXBlID0gJ1NUQUdFJ1xuICAgICAgICAgIEFORCBlLnN0YXR1cyA9ICdTVUNDRUVERUQnXG4gICAgICAgICAgQU5EIGUuY29tcGxldGVkX2F0IElTIE5PVCBOVUxMXG4gICAgICAgIE9SREVSIEJZIGUuY29tcGxldGVkX2F0IERFU0NcbiAgICAgICAgTElNSVQgMVxuICAgICAgKVxuICAgICAgU0VMRUNUIGMuY29tbWl0X3NoYSBBUyBjb21taXRfc2hhLCBsZC5jb21wbGV0ZWRfYXQ6OnRleHQgQVMgY29tcGxldGVkX2F0XG4gICAgICBGUk9NIGxhc3RfZGVwbG95IGxkXG4gICAgICBKT0lOICR7c2NoZW1hLnBpcGVsaW5lRXZlbnR9IGNcbiAgICAgICAgT04gYy5leGVjdXRpb25faWQgPSBsZC5leGVjdXRpb25faWRcbiAgICAgICBBTkQgYy5waXBlbGluZV9pZCA9ICR7cGlwZWxpbmVJZH1cbiAgICAgICBBTkQgYy5vcmdfaWQgPSAke3JlZ2lzdHJ5Lm9yZ0lkfVxuICAgICAgV0hFUkUgYy5jb21taXRfc2hhIElTIE5PVCBOVUxMXG4gICAgICBPUkRFUiBCWSBjLmNyZWF0ZWRfYXQgREVTQ1xuICAgICAgTElNSVQgMVxuICAgIGApKS5yb3dzKTtcblxuICAgIGNvbnN0IHJvdyA9IHJvd3NbMF07XG4gICAgaWYgKCFyb3cpIHJldHVybiBOT05FO1xuICAgIHJldHVybiB7IGNvbW1pdFNoYTogcm93LmNvbW1pdF9zaGEgPz8gbnVsbCwgZGVwbG95ZWRBdDogcm93LmNvbXBsZXRlZF9hdCA/PyBudWxsIH07XG4gIH0pO1xufVxuIl19
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Which pipelines need somebody to look at them, and WHY.
3
+ *
4
+ * The rest of the report describes; this decides. That is a meaningful difference,
5
+ * and it is why every finding carries its EVIDENCE — "p95 up 31%, 4m12s → 5m31s",
6
+ * not "performance degraded". A flag without the number behind it is an opinion the
7
+ * reader has to take on trust, and the first time one is wrong the whole section
8
+ * stops being read.
9
+ *
10
+ * THE THRESHOLDS ARE ORG-CONFIGURABLE, with defaults stated below. They have to be:
11
+ * 90% success is a crisis for a deploy pipeline and unremarkable for a flaky
12
+ * integration suite somebody is already rewriting, and a platform-imposed number
13
+ * would make the section wrong for one of them by construction.
14
+ *
15
+ * WHAT IS DELIBERATELY NOT A RULE: anything per-person, anything about volume
16
+ * ("this team shipped less than that team"), and anything comparing one team to
17
+ * another. Those are the flags that turn a delivery report into a performance
18
+ * review, which is the failure mode this whole feature is trying to avoid.
19
+ */
20
+ import type { PipelineBreakdownRow } from './analytics-queries.js';
21
+ /** The thresholds a rule trips at. Every one is overridable per org. */
22
+ export interface NeedsAttentionThresholds {
23
+ /** Success rate below this, as a percentage. */
24
+ successRatePct: number;
25
+ /** p95 duration up by more than this, as a percentage of the previous period. */
26
+ p95IncreasePct: number;
27
+ /** This many consecutive failures. */
28
+ consecutiveFailures: number;
29
+ /** No successful run in this many days. */
30
+ staleDays: number;
31
+ /** A failure within this many hours of a pipeline configuration change. */
32
+ failureAfterChangeHours: number;
33
+ }
34
+ /**
35
+ * The defaults.
36
+ *
37
+ * Each is the point at which a reasonable lead would want to know, not the point at
38
+ * which something is definitely wrong — the section's job is to raise attention,
39
+ * and a threshold set where the problem is already undeniable raises it too late.
40
+ */
41
+ export declare const DEFAULT_THRESHOLDS: Readonly<NeedsAttentionThresholds>;
42
+ /** Why a pipeline was flagged. The id is stable; the evidence is human. */
43
+ export type AttentionRuleId = 'low_success_rate' | 'slower_p95' | 'consecutive_failures' | 'no_recent_success' | 'failed_after_config_change' | 'vulnerable_plugin' | 'no_deploy_tracking';
44
+ /** One finding about one pipeline. */
45
+ export interface AttentionFinding {
46
+ rule: AttentionRuleId;
47
+ /** The sentence a lead reads, with the number in it. */
48
+ evidence: string;
49
+ /** `high` means "this is broken now"; `medium` means "this is drifting". */
50
+ severity: 'high' | 'medium';
51
+ }
52
+ /** A flagged pipeline and everything that tripped. */
53
+ export interface AttentionItem {
54
+ pipelineId: string;
55
+ pipelineName: string | null;
56
+ findings: AttentionFinding[];
57
+ /** The worst severity among the findings — what the list sorts on. */
58
+ severity: 'high' | 'medium';
59
+ }
60
+ /** What the evaluator needs beyond the current period's breakdown. */
61
+ export interface AttentionInput {
62
+ current: readonly PipelineBreakdownRow[];
63
+ /** The previous period, for the p95 comparison. Absent ⇒ that rule is skipped. */
64
+ previous?: readonly PipelineBreakdownRow[];
65
+ /** Per pipeline: consecutive failures ending the period, and last success. */
66
+ streaks?: ReadonlyMap<string, {
67
+ consecutiveFailures: number;
68
+ lastSuccessAt: Date | null;
69
+ }>;
70
+ /** Per pipeline: the most recent config change, from the audit trail. */
71
+ configChanges?: ReadonlyMap<string, Date>;
72
+ /** Pipelines with an open Critical plugin exposure. */
73
+ vulnerablePipelines?: ReadonlySet<string>;
74
+ /** Pipelines that ran but produced no deploy rows — DORA cannot see them. */
75
+ untrackedPipelines?: ReadonlySet<string>;
76
+ thresholds?: Partial<NeedsAttentionThresholds>;
77
+ now?: Date;
78
+ }
79
+ /**
80
+ * Evaluate every rule against the period.
81
+ *
82
+ * Returns only the pipelines that tripped something, worst first, so the section is
83
+ * a short list of things to do rather than a table of everything.
84
+ */
85
+ export declare function evaluateNeedsAttention(input: AttentionInput): AttentionItem[];
86
+ /**
87
+ * Read an org's thresholds out of stored settings, falling back per field.
88
+ *
89
+ * Per FIELD, not per object: an org that set only `successRatePct` keeps the
90
+ * defaults for everything else, rather than silently disabling four rules by
91
+ * configuring one.
92
+ */
93
+ export declare function thresholdsFrom(stored: Partial<NeedsAttentionThresholds> | null | undefined): NeedsAttentionThresholds;
@@ -0,0 +1,152 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * The defaults.
5
+ *
6
+ * Each is the point at which a reasonable lead would want to know, not the point at
7
+ * which something is definitely wrong — the section's job is to raise attention,
8
+ * and a threshold set where the problem is already undeniable raises it too late.
9
+ */
10
+ export const DEFAULT_THRESHOLDS = Object.freeze({
11
+ successRatePct: 90,
12
+ p95IncreasePct: 25,
13
+ consecutiveFailures: 3,
14
+ staleDays: 7,
15
+ failureAfterChangeHours: 24,
16
+ });
17
+ /** `272000` → `4m32s`, for evidence a person reads. */
18
+ function duration(ms) {
19
+ const total = Math.round(ms / 1000);
20
+ const m = Math.floor(total / 60);
21
+ const s = total % 60;
22
+ return m > 0 ? `${m}m${String(s).padStart(2, '0')}s` : `${s}s`;
23
+ }
24
+ /**
25
+ * Evaluate every rule against the period.
26
+ *
27
+ * Returns only the pipelines that tripped something, worst first, so the section is
28
+ * a short list of things to do rather than a table of everything.
29
+ */
30
+ export function evaluateNeedsAttention(input) {
31
+ const t = { ...DEFAULT_THRESHOLDS, ...input.thresholds };
32
+ const now = input.now ?? new Date();
33
+ const priorByPipeline = new Map((input.previous ?? []).map((p) => [p.pipelineId, p]));
34
+ const items = [];
35
+ for (const row of input.current) {
36
+ const findings = [];
37
+ // 1. Success rate. Skipped entirely for a pipeline with no runs: a pipeline
38
+ // nobody ran has no success rate, and flagging it as 0% would put every
39
+ // dormant pipeline at the top of the list forever.
40
+ if (row.runs > 0 && row.successPct !== null && row.successPct < t.successRatePct) {
41
+ findings.push({
42
+ rule: 'low_success_rate',
43
+ severity: row.successPct < t.successRatePct / 2 ? 'high' : 'medium',
44
+ evidence: `${row.successPct}% of ${row.runs} runs succeeded (below ${t.successRatePct}%)`,
45
+ });
46
+ }
47
+ // 2. Getting slower. Needs both periods AND a real baseline — a jump from 2s
48
+ // to 3s is 50% and means nothing, so a tiny baseline is not evidence.
49
+ const prior = priorByPipeline.get(row.pipelineId);
50
+ if (row.p95Ms !== null && prior?.p95Ms && prior.p95Ms > 10_000) {
51
+ const deltaPct = ((row.p95Ms - prior.p95Ms) / prior.p95Ms) * 100;
52
+ if (deltaPct > t.p95IncreasePct) {
53
+ findings.push({
54
+ rule: 'slower_p95',
55
+ severity: deltaPct > t.p95IncreasePct * 2 ? 'high' : 'medium',
56
+ evidence: `p95 up ${deltaPct.toFixed(0)}%, ${duration(prior.p95Ms)} → ${duration(row.p95Ms)}`,
57
+ });
58
+ }
59
+ }
60
+ const streak = input.streaks?.get(row.pipelineId);
61
+ // 3. Consecutive failures. The clearest "this is broken now" signal there is:
62
+ // a 70% success rate over a month and three failures in a row are very
63
+ // different situations, and only the second needs somebody today.
64
+ if (streak && streak.consecutiveFailures >= t.consecutiveFailures) {
65
+ findings.push({
66
+ rule: 'consecutive_failures',
67
+ severity: 'high',
68
+ evidence: `${streak.consecutiveFailures} consecutive failures`,
69
+ });
70
+ }
71
+ // 4. Nothing has worked lately. Distinct from the rate: a pipeline that
72
+ // succeeded 90% of the time and then stopped succeeding entirely looks fine
73
+ // by rate and is completely broken.
74
+ if (streak && row.runs > 0) {
75
+ const days = streak.lastSuccessAt
76
+ ? Math.floor((now.getTime() - streak.lastSuccessAt.getTime()) / 86_400_000)
77
+ : null;
78
+ if (days === null) {
79
+ findings.push({
80
+ rule: 'no_recent_success',
81
+ severity: 'high',
82
+ evidence: `no successful run in this period (${row.runs} attempts)`,
83
+ });
84
+ }
85
+ else if (days >= t.staleDays) {
86
+ findings.push({
87
+ rule: 'no_recent_success',
88
+ severity: days >= t.staleDays * 2 ? 'high' : 'medium',
89
+ evidence: `last succeeded ${days} days ago`,
90
+ });
91
+ }
92
+ }
93
+ // 5. Broke shortly after somebody changed it. The most useful correlation in
94
+ // the whole section, because it names a likely cause rather than a symptom —
95
+ // and it is the one a human would work out by hand from two screens.
96
+ const changedAt = input.configChanges?.get(row.pipelineId);
97
+ if (changedAt && row.lastFailureAt) {
98
+ const gapHours = (new Date(row.lastFailureAt).getTime() - changedAt.getTime()) / 3600_000;
99
+ if (gapHours >= 0 && gapHours <= t.failureAfterChangeHours) {
100
+ findings.push({
101
+ rule: 'failed_after_config_change',
102
+ severity: 'high',
103
+ evidence: `failed ${gapHours < 1 ? 'less than an hour' : `${Math.round(gapHours)}h`} after a configuration change`
104
+ + (row.lastFailureStage ? ` (${row.lastFailureStage})` : ''),
105
+ });
106
+ }
107
+ }
108
+ // 6. Running a plugin version with open Critical findings.
109
+ if (input.vulnerablePipelines?.has(row.pipelineId)) {
110
+ findings.push({
111
+ rule: 'vulnerable_plugin',
112
+ severity: 'high',
113
+ evidence: 'runs a plugin version with open Critical vulnerabilities',
114
+ });
115
+ }
116
+ // 7. Invisible to DORA. Not a failure — a MEASUREMENT gap, and flagged
117
+ // because a pipeline nobody can see deploying is the reason a deployment-
118
+ // frequency number is wrong, and nothing else in the report would say so.
119
+ if (input.untrackedPipelines?.has(row.pipelineId) && row.runs > 0) {
120
+ findings.push({
121
+ rule: 'no_deploy_tracking',
122
+ severity: 'medium',
123
+ evidence: 'ran, but produced no deploy data — re-synth to include deploy tags',
124
+ });
125
+ }
126
+ if (findings.length > 0) {
127
+ items.push({
128
+ pipelineId: row.pipelineId,
129
+ pipelineName: row.pipelineName,
130
+ findings,
131
+ severity: findings.some((f) => f.severity === 'high') ? 'high' : 'medium',
132
+ });
133
+ }
134
+ }
135
+ // High first, then the pipelines with the most wrong with them.
136
+ return items.sort((a, b) => {
137
+ if (a.severity !== b.severity)
138
+ return a.severity === 'high' ? -1 : 1;
139
+ return b.findings.length - a.findings.length;
140
+ });
141
+ }
142
+ /**
143
+ * Read an org's thresholds out of stored settings, falling back per field.
144
+ *
145
+ * Per FIELD, not per object: an org that set only `successRatePct` keeps the
146
+ * defaults for everything else, rather than silently disabling four rules by
147
+ * configuring one.
148
+ */
149
+ export function thresholdsFrom(stored) {
150
+ return { ...DEFAULT_THRESHOLDS, ...(stored ?? {}) };
151
+ }
152
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmVlZHMtYXR0ZW50aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvbmVlZHMtYXR0ZW50aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFzQ3RDOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUF1QyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2xGLGNBQWMsRUFBRSxFQUFFO0lBQ2xCLGNBQWMsRUFBRSxFQUFFO0lBQ2xCLG1CQUFtQixFQUFFLENBQUM7SUFDdEIsU0FBUyxFQUFFLENBQUM7SUFDWix1QkFBdUIsRUFBRSxFQUFFO0NBQzVCLENBQUMsQ0FBQztBQStDSCx1REFBdUQ7QUFDdkQsU0FBUyxRQUFRLENBQUMsRUFBVTtJQUMxQixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztJQUNwQyxNQUFNLENBQUMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssR0FBRyxFQUFFLENBQUMsQ0FBQztJQUNqQyxNQUFNLENBQUMsR0FBRyxLQUFLLEdBQUcsRUFBRSxDQUFDO0lBQ3JCLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQztBQUNqRSxDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsc0JBQXNCLENBQUMsS0FBcUI7SUFDMUQsTUFBTSxDQUFDLEdBQUcsRUFBRSxHQUFHLGtCQUFrQixFQUFFLEdBQUcsS0FBSyxDQUFDLFVBQVUsRUFBRSxDQUFDO0lBQ3pELE1BQU0sR0FBRyxHQUFHLEtBQUssQ0FBQyxHQUFHLElBQUksSUFBSSxJQUFJLEVBQUUsQ0FBQztJQUNwQyxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxRQUFRLElBQUksRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3RGLE1BQU0sS0FBSyxHQUFvQixFQUFFLENBQUM7SUFFbEMsS0FBSyxNQUFNLEdBQUcsSUFBSSxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDaEMsTUFBTSxRQUFRLEdBQXVCLEVBQUUsQ0FBQztRQUV4Qyw0RUFBNEU7UUFDNUUsd0VBQXdFO1FBQ3hFLG1EQUFtRDtRQUNuRCxJQUFJLEdBQUcsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxVQUFVLEtBQUssSUFBSSxJQUFJLEdBQUcsQ0FBQyxVQUFVLEdBQUcsQ0FBQyxDQUFDLGNBQWMsRUFBRSxDQUFDO1lBQ2pGLFFBQVEsQ0FBQyxJQUFJLENBQUM7Z0JBQ1osSUFBSSxFQUFFLGtCQUFrQjtnQkFDeEIsUUFBUSxFQUFFLEdBQUcsQ0FBQyxVQUFVLEdBQUcsQ0FBQyxDQUFDLGNBQWMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUTtnQkFDbkUsUUFBUSxFQUFFLEdBQUcsR0FBRyxDQUFDLFVBQVUsUUFBUSxHQUFHLENBQUMsSUFBSSwwQkFBMEIsQ0FBQyxDQUFDLGNBQWMsSUFBSTthQUMxRixDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsNkVBQTZFO1FBQzdFLHNFQUFzRTtRQUN0RSxNQUFNLEtBQUssR0FBRyxlQUFlLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUNsRCxJQUFJLEdBQUcsQ0FBQyxLQUFLLEtBQUssSUFBSSxJQUFJLEtBQUssRUFBRSxLQUFLLElBQUksS0FBSyxDQUFDLEtBQUssR0FBRyxNQUFNLEVBQUUsQ0FBQztZQUMvRCxNQUFNLFFBQVEsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLEtBQUssR0FBRyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLEdBQUcsQ0FBQztZQUNqRSxJQUFJLFFBQVEsR0FBRyxDQUFDLENBQUMsY0FBYyxFQUFFLENBQUM7Z0JBQ2hDLFFBQVEsQ0FBQyxJQUFJLENBQUM7b0JBQ1osSUFBSSxFQUFFLFlBQVk7b0JBQ2xCLFFBQVEsRUFBRSxRQUFRLEdBQUcsQ0FBQyxDQUFDLGNBQWMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUTtvQkFDN0QsUUFBUSxFQUFFLFVBQVUsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxRQUFRLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxNQUFNLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLEVBQUU7aUJBQzlGLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO1FBRUQsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLE9BQU8sRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBRWxELDhFQUE4RTtRQUM5RSx1RUFBdUU7UUFDdkUsa0VBQWtFO1FBQ2xFLElBQUksTUFBTSxJQUFJLE1BQU0sQ0FBQyxtQkFBbUIsSUFBSSxDQUFDLENBQUMsbUJBQW1CLEVBQUUsQ0FBQztZQUNsRSxRQUFRLENBQUMsSUFBSSxDQUFDO2dCQUNaLElBQUksRUFBRSxzQkFBc0I7Z0JBQzVCLFFBQVEsRUFBRSxNQUFNO2dCQUNoQixRQUFRLEVBQUUsR0FBRyxNQUFNLENBQUMsbUJBQW1CLHVCQUF1QjthQUMvRCxDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsd0VBQXdFO1FBQ3hFLDRFQUE0RTtRQUM1RSxvQ0FBb0M7UUFDcEMsSUFBSSxNQUFNLElBQUksR0FBRyxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUMzQixNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsYUFBYTtnQkFDL0IsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsTUFBTSxDQUFDLGFBQWEsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFHLFVBQVUsQ0FBQztnQkFDM0UsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNULElBQUksSUFBSSxLQUFLLElBQUksRUFBRSxDQUFDO2dCQUNsQixRQUFRLENBQUMsSUFBSSxDQUFDO29CQUNaLElBQUksRUFBRSxtQkFBbUI7b0JBQ3pCLFFBQVEsRUFBRSxNQUFNO29CQUNoQixRQUFRLEVBQUUscUNBQXFDLEdBQUcsQ0FBQyxJQUFJLFlBQVk7aUJBQ3BFLENBQUMsQ0FBQztZQUNMLENBQUM7aUJBQU0sSUFBSSxJQUFJLElBQUksQ0FBQyxDQUFDLFNBQVMsRUFBRSxDQUFDO2dCQUMvQixRQUFRLENBQUMsSUFBSSxDQUFDO29CQUNaLElBQUksRUFBRSxtQkFBbUI7b0JBQ3pCLFFBQVEsRUFBRSxJQUFJLElBQUksQ0FBQyxDQUFDLFNBQVMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUTtvQkFDckQsUUFBUSxFQUFFLGtCQUFrQixJQUFJLFdBQVc7aUJBQzVDLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO1FBRUQsNkVBQTZFO1FBQzdFLDZFQUE2RTtRQUM3RSxxRUFBcUU7UUFDckUsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLGFBQWEsRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQzNELElBQUksU0FBUyxJQUFJLEdBQUcsQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUNuQyxNQUFNLFFBQVEsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsQ0FBQyxPQUFPLEVBQUUsR0FBRyxTQUFTLENBQUMsT0FBTyxFQUFFLENBQUMsR0FBRyxRQUFRLENBQUM7WUFDMUYsSUFBSSxRQUFRLElBQUksQ0FBQyxJQUFJLFFBQVEsSUFBSSxDQUFDLENBQUMsdUJBQXVCLEVBQUUsQ0FBQztnQkFDM0QsUUFBUSxDQUFDLElBQUksQ0FBQztvQkFDWixJQUFJLEVBQUUsNEJBQTRCO29CQUNsQyxRQUFRLEVBQUUsTUFBTTtvQkFDaEIsUUFBUSxFQUFFLFVBQVUsUUFBUSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsbUJBQW1CLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsR0FBRywrQkFBK0I7MEJBQzlHLENBQUMsR0FBRyxDQUFDLGdCQUFnQixDQUFDLENBQUMsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxnQkFBZ0IsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7aUJBQy9ELENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO1FBRUQsMkRBQTJEO1FBQzNELElBQUksS0FBSyxDQUFDLG1CQUFtQixFQUFFLEdBQUcsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQztZQUNuRCxRQUFRLENBQUMsSUFBSSxDQUFDO2dCQUNaLElBQUksRUFBRSxtQkFBbUI7Z0JBQ3pCLFFBQVEsRUFBRSxNQUFNO2dCQUNoQixRQUFRLEVBQUUsMERBQTBEO2FBQ3JFLENBQUMsQ0FBQztRQUNMLENBQUM7UUFFRCx1RUFBdUU7UUFDdkUsMEVBQTBFO1FBQzFFLDBFQUEwRTtRQUMxRSxJQUFJLEtBQUssQ0FBQyxrQkFBa0IsRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDbEUsUUFBUSxDQUFDLElBQUksQ0FBQztnQkFDWixJQUFJLEVBQUUsb0JBQW9CO2dCQUMxQixRQUFRLEVBQUUsUUFBUTtnQkFDbEIsUUFBUSxFQUFFLG9FQUFvRTthQUMvRSxDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3hCLEtBQUssQ0FBQyxJQUFJLENBQUM7Z0JBQ1QsVUFBVSxFQUFFLEdBQUcsQ0FBQyxVQUFVO2dCQUMxQixZQUFZLEVBQUUsR0FBRyxDQUFDLFlBQVk7Z0JBQzlCLFFBQVE7Z0JBQ1IsUUFBUSxFQUFFLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUTthQUMxRSxDQUFDLENBQUM7UUFDTCxDQUFDO0lBQ0gsQ0FBQztJQUVELGdFQUFnRTtJQUNoRSxPQUFPLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUU7UUFDekIsSUFBSSxDQUFDLENBQUMsUUFBUSxLQUFLLENBQUMsQ0FBQyxRQUFRO1lBQUUsT0FBTyxDQUFDLENBQUMsUUFBUSxLQUFLLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNyRSxPQUFPLENBQUMsQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDO0lBQy9DLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxjQUFjLENBQUMsTUFBNEQ7SUFDekYsT0FBTyxFQUFFLEdBQUcsa0JBQWtCLEVBQUUsR0FBRyxDQUFDLE1BQU0sSUFBSSxFQUFFLENBQUMsRUFBRSxDQUFDO0FBQ3RELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBXaGljaCBwaXBlbGluZXMgbmVlZCBzb21lYm9keSB0byBsb29rIGF0IHRoZW0sIGFuZCBXSFkuXG4gKlxuICogVGhlIHJlc3Qgb2YgdGhlIHJlcG9ydCBkZXNjcmliZXM7IHRoaXMgZGVjaWRlcy4gVGhhdCBpcyBhIG1lYW5pbmdmdWwgZGlmZmVyZW5jZSxcbiAqIGFuZCBpdCBpcyB3aHkgZXZlcnkgZmluZGluZyBjYXJyaWVzIGl0cyBFVklERU5DRSDigJQgXCJwOTUgdXAgMzElLCA0bTEycyDihpIgNW0zMXNcIixcbiAqIG5vdCBcInBlcmZvcm1hbmNlIGRlZ3JhZGVkXCIuIEEgZmxhZyB3aXRob3V0IHRoZSBudW1iZXIgYmVoaW5kIGl0IGlzIGFuIG9waW5pb24gdGhlXG4gKiByZWFkZXIgaGFzIHRvIHRha2Ugb24gdHJ1c3QsIGFuZCB0aGUgZmlyc3QgdGltZSBvbmUgaXMgd3JvbmcgdGhlIHdob2xlIHNlY3Rpb25cbiAqIHN0b3BzIGJlaW5nIHJlYWQuXG4gKlxuICogVEhFIFRIUkVTSE9MRFMgQVJFIE9SRy1DT05GSUdVUkFCTEUsIHdpdGggZGVmYXVsdHMgc3RhdGVkIGJlbG93LiBUaGV5IGhhdmUgdG8gYmU6XG4gKiA5MCUgc3VjY2VzcyBpcyBhIGNyaXNpcyBmb3IgYSBkZXBsb3kgcGlwZWxpbmUgYW5kIHVucmVtYXJrYWJsZSBmb3IgYSBmbGFreVxuICogaW50ZWdyYXRpb24gc3VpdGUgc29tZWJvZHkgaXMgYWxyZWFkeSByZXdyaXRpbmcsIGFuZCBhIHBsYXRmb3JtLWltcG9zZWQgbnVtYmVyXG4gKiB3b3VsZCBtYWtlIHRoZSBzZWN0aW9uIHdyb25nIGZvciBvbmUgb2YgdGhlbSBieSBjb25zdHJ1Y3Rpb24uXG4gKlxuICogV0hBVCBJUyBERUxJQkVSQVRFTFkgTk9UIEEgUlVMRTogYW55dGhpbmcgcGVyLXBlcnNvbiwgYW55dGhpbmcgYWJvdXQgdm9sdW1lXG4gKiAoXCJ0aGlzIHRlYW0gc2hpcHBlZCBsZXNzIHRoYW4gdGhhdCB0ZWFtXCIpLCBhbmQgYW55dGhpbmcgY29tcGFyaW5nIG9uZSB0ZWFtIHRvXG4gKiBhbm90aGVyLiBUaG9zZSBhcmUgdGhlIGZsYWdzIHRoYXQgdHVybiBhIGRlbGl2ZXJ5IHJlcG9ydCBpbnRvIGEgcGVyZm9ybWFuY2VcbiAqIHJldmlldywgd2hpY2ggaXMgdGhlIGZhaWx1cmUgbW9kZSB0aGlzIHdob2xlIGZlYXR1cmUgaXMgdHJ5aW5nIHRvIGF2b2lkLlxuICovXG5cbmltcG9ydCB0eXBlIHsgUGlwZWxpbmVCcmVha2Rvd25Sb3cgfSBmcm9tICcuL2FuYWx5dGljcy1xdWVyaWVzLmpzJztcblxuLyoqIFRoZSB0aHJlc2hvbGRzIGEgcnVsZSB0cmlwcyBhdC4gRXZlcnkgb25lIGlzIG92ZXJyaWRhYmxlIHBlciBvcmcuICovXG5leHBvcnQgaW50ZXJmYWNlIE5lZWRzQXR0ZW50aW9uVGhyZXNob2xkcyB7XG4gIC8qKiBTdWNjZXNzIHJhdGUgYmVsb3cgdGhpcywgYXMgYSBwZXJjZW50YWdlLiAqL1xuICBzdWNjZXNzUmF0ZVBjdDogbnVtYmVyO1xuICAvKiogcDk1IGR1cmF0aW9uIHVwIGJ5IG1vcmUgdGhhbiB0aGlzLCBhcyBhIHBlcmNlbnRhZ2Ugb2YgdGhlIHByZXZpb3VzIHBlcmlvZC4gKi9cbiAgcDk1SW5jcmVhc2VQY3Q6IG51bWJlcjtcbiAgLyoqIFRoaXMgbWFueSBjb25zZWN1dGl2ZSBmYWlsdXJlcy4gKi9cbiAgY29uc2VjdXRpdmVGYWlsdXJlczogbnVtYmVyO1xuICAvKiogTm8gc3VjY2Vzc2Z1bCBydW4gaW4gdGhpcyBtYW55IGRheXMuICovXG4gIHN0YWxlRGF5czogbnVtYmVyO1xuICAvKiogQSBmYWlsdXJlIHdpdGhpbiB0aGlzIG1hbnkgaG91cnMgb2YgYSBwaXBlbGluZSBjb25maWd1cmF0aW9uIGNoYW5nZS4gKi9cbiAgZmFpbHVyZUFmdGVyQ2hhbmdlSG91cnM6IG51bWJlcjtcbn1cblxuLyoqXG4gKiBUaGUgZGVmYXVsdHMuXG4gKlxuICogRWFjaCBpcyB0aGUgcG9pbnQgYXQgd2hpY2ggYSByZWFzb25hYmxlIGxlYWQgd291bGQgd2FudCB0byBrbm93LCBub3QgdGhlIHBvaW50IGF0XG4gKiB3aGljaCBzb21ldGhpbmcgaXMgZGVmaW5pdGVseSB3cm9uZyDigJQgdGhlIHNlY3Rpb24ncyBqb2IgaXMgdG8gcmFpc2UgYXR0ZW50aW9uLFxuICogYW5kIGEgdGhyZXNob2xkIHNldCB3aGVyZSB0aGUgcHJvYmxlbSBpcyBhbHJlYWR5IHVuZGVuaWFibGUgcmFpc2VzIGl0IHRvbyBsYXRlLlxuICovXG5leHBvcnQgY29uc3QgREVGQVVMVF9USFJFU0hPTERTOiBSZWFkb25seTxOZWVkc0F0dGVudGlvblRocmVzaG9sZHM+ID0gT2JqZWN0LmZyZWV6ZSh7XG4gIHN1Y2Nlc3NSYXRlUGN0OiA5MCxcbiAgcDk1SW5jcmVhc2VQY3Q6IDI1LFxuICBjb25zZWN1dGl2ZUZhaWx1cmVzOiAzLFxuICBzdGFsZURheXM6IDcsXG4gIGZhaWx1cmVBZnRlckNoYW5nZUhvdXJzOiAyNCxcbn0pO1xuXG4vKiogV2h5IGEgcGlwZWxpbmUgd2FzIGZsYWdnZWQuIFRoZSBpZCBpcyBzdGFibGU7IHRoZSBldmlkZW5jZSBpcyBodW1hbi4gKi9cbmV4cG9ydCB0eXBlIEF0dGVudGlvblJ1bGVJZCA9XG4gIHwgJ2xvd19zdWNjZXNzX3JhdGUnXG4gIHwgJ3Nsb3dlcl9wOTUnXG4gIHwgJ2NvbnNlY3V0aXZlX2ZhaWx1cmVzJ1xuICB8ICdub19yZWNlbnRfc3VjY2VzcydcbiAgfCAnZmFpbGVkX2FmdGVyX2NvbmZpZ19jaGFuZ2UnXG4gIHwgJ3Z1bG5lcmFibGVfcGx1Z2luJ1xuICB8ICdub19kZXBsb3lfdHJhY2tpbmcnO1xuXG4vKiogT25lIGZpbmRpbmcgYWJvdXQgb25lIHBpcGVsaW5lLiAqL1xuZXhwb3J0IGludGVyZmFjZSBBdHRlbnRpb25GaW5kaW5nIHtcbiAgcnVsZTogQXR0ZW50aW9uUnVsZUlkO1xuICAvKiogVGhlIHNlbnRlbmNlIGEgbGVhZCByZWFkcywgd2l0aCB0aGUgbnVtYmVyIGluIGl0LiAqL1xuICBldmlkZW5jZTogc3RyaW5nO1xuICAvKiogYGhpZ2hgIG1lYW5zIFwidGhpcyBpcyBicm9rZW4gbm93XCI7IGBtZWRpdW1gIG1lYW5zIFwidGhpcyBpcyBkcmlmdGluZ1wiLiAqL1xuICBzZXZlcml0eTogJ2hpZ2gnIHwgJ21lZGl1bSc7XG59XG5cbi8qKiBBIGZsYWdnZWQgcGlwZWxpbmUgYW5kIGV2ZXJ5dGhpbmcgdGhhdCB0cmlwcGVkLiAqL1xuZXhwb3J0IGludGVyZmFjZSBBdHRlbnRpb25JdGVtIHtcbiAgcGlwZWxpbmVJZDogc3RyaW5nO1xuICBwaXBlbGluZU5hbWU6IHN0cmluZyB8IG51bGw7XG4gIGZpbmRpbmdzOiBBdHRlbnRpb25GaW5kaW5nW107XG4gIC8qKiBUaGUgd29yc3Qgc2V2ZXJpdHkgYW1vbmcgdGhlIGZpbmRpbmdzIOKAlCB3aGF0IHRoZSBsaXN0IHNvcnRzIG9uLiAqL1xuICBzZXZlcml0eTogJ2hpZ2gnIHwgJ21lZGl1bSc7XG59XG5cbi8qKiBXaGF0IHRoZSBldmFsdWF0b3IgbmVlZHMgYmV5b25kIHRoZSBjdXJyZW50IHBlcmlvZCdzIGJyZWFrZG93bi4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQXR0ZW50aW9uSW5wdXQge1xuICBjdXJyZW50OiByZWFkb25seSBQaXBlbGluZUJyZWFrZG93blJvd1tdO1xuICAvKiogVGhlIHByZXZpb3VzIHBlcmlvZCwgZm9yIHRoZSBwOTUgY29tcGFyaXNvbi4gQWJzZW50IOKHkiB0aGF0IHJ1bGUgaXMgc2tpcHBlZC4gKi9cbiAgcHJldmlvdXM/OiByZWFkb25seSBQaXBlbGluZUJyZWFrZG93blJvd1tdO1xuICAvKiogUGVyIHBpcGVsaW5lOiBjb25zZWN1dGl2ZSBmYWlsdXJlcyBlbmRpbmcgdGhlIHBlcmlvZCwgYW5kIGxhc3Qgc3VjY2Vzcy4gKi9cbiAgc3RyZWFrcz86IFJlYWRvbmx5TWFwPHN0cmluZywgeyBjb25zZWN1dGl2ZUZhaWx1cmVzOiBudW1iZXI7IGxhc3RTdWNjZXNzQXQ6IERhdGUgfCBudWxsIH0+O1xuICAvKiogUGVyIHBpcGVsaW5lOiB0aGUgbW9zdCByZWNlbnQgY29uZmlnIGNoYW5nZSwgZnJvbSB0aGUgYXVkaXQgdHJhaWwuICovXG4gIGNvbmZpZ0NoYW5nZXM/OiBSZWFkb25seU1hcDxzdHJpbmcsIERhdGU+O1xuICAvKiogUGlwZWxpbmVzIHdpdGggYW4gb3BlbiBDcml0aWNhbCBwbHVnaW4gZXhwb3N1cmUuICovXG4gIHZ1bG5lcmFibGVQaXBlbGluZXM/OiBSZWFkb25seVNldDxzdHJpbmc+O1xuICAvKiogUGlwZWxpbmVzIHRoYXQgcmFuIGJ1dCBwcm9kdWNlZCBubyBkZXBsb3kgcm93cyDigJQgRE9SQSBjYW5ub3Qgc2VlIHRoZW0uICovXG4gIHVudHJhY2tlZFBpcGVsaW5lcz86IFJlYWRvbmx5U2V0PHN0cmluZz47XG4gIHRocmVzaG9sZHM/OiBQYXJ0aWFsPE5lZWRzQXR0ZW50aW9uVGhyZXNob2xkcz47XG4gIG5vdz86IERhdGU7XG59XG5cbi8qKiBgMjcyMDAwYCDihpIgYDRtMzJzYCwgZm9yIGV2aWRlbmNlIGEgcGVyc29uIHJlYWRzLiAqL1xuZnVuY3Rpb24gZHVyYXRpb24obXM6IG51bWJlcik6IHN0cmluZyB7XG4gIGNvbnN0IHRvdGFsID0gTWF0aC5yb3VuZChtcyAvIDEwMDApO1xuICBjb25zdCBtID0gTWF0aC5mbG9vcih0b3RhbCAvIDYwKTtcbiAgY29uc3QgcyA9IHRvdGFsICUgNjA7XG4gIHJldHVybiBtID4gMCA/IGAke219bSR7U3RyaW5nKHMpLnBhZFN0YXJ0KDIsICcwJyl9c2AgOiBgJHtzfXNgO1xufVxuXG4vKipcbiAqIEV2YWx1YXRlIGV2ZXJ5IHJ1bGUgYWdhaW5zdCB0aGUgcGVyaW9kLlxuICpcbiAqIFJldHVybnMgb25seSB0aGUgcGlwZWxpbmVzIHRoYXQgdHJpcHBlZCBzb21ldGhpbmcsIHdvcnN0IGZpcnN0LCBzbyB0aGUgc2VjdGlvbiBpc1xuICogYSBzaG9ydCBsaXN0IG9mIHRoaW5ncyB0byBkbyByYXRoZXIgdGhhbiBhIHRhYmxlIG9mIGV2ZXJ5dGhpbmcuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBldmFsdWF0ZU5lZWRzQXR0ZW50aW9uKGlucHV0OiBBdHRlbnRpb25JbnB1dCk6IEF0dGVudGlvbkl0ZW1bXSB7XG4gIGNvbnN0IHQgPSB7IC4uLkRFRkFVTFRfVEhSRVNIT0xEUywgLi4uaW5wdXQudGhyZXNob2xkcyB9O1xuICBjb25zdCBub3cgPSBpbnB1dC5ub3cgPz8gbmV3IERhdGUoKTtcbiAgY29uc3QgcHJpb3JCeVBpcGVsaW5lID0gbmV3IE1hcCgoaW5wdXQucHJldmlvdXMgPz8gW10pLm1hcCgocCkgPT4gW3AucGlwZWxpbmVJZCwgcF0pKTtcbiAgY29uc3QgaXRlbXM6IEF0dGVudGlvbkl0ZW1bXSA9IFtdO1xuXG4gIGZvciAoY29uc3Qgcm93IG9mIGlucHV0LmN1cnJlbnQpIHtcbiAgICBjb25zdCBmaW5kaW5nczogQXR0ZW50aW9uRmluZGluZ1tdID0gW107XG5cbiAgICAvLyAxLiBTdWNjZXNzIHJhdGUuIFNraXBwZWQgZW50aXJlbHkgZm9yIGEgcGlwZWxpbmUgd2l0aCBubyBydW5zOiBhIHBpcGVsaW5lXG4gICAgLy8gbm9ib2R5IHJhbiBoYXMgbm8gc3VjY2VzcyByYXRlLCBhbmQgZmxhZ2dpbmcgaXQgYXMgMCUgd291bGQgcHV0IGV2ZXJ5XG4gICAgLy8gZG9ybWFudCBwaXBlbGluZSBhdCB0aGUgdG9wIG9mIHRoZSBsaXN0IGZvcmV2ZXIuXG4gICAgaWYgKHJvdy5ydW5zID4gMCAmJiByb3cuc3VjY2Vzc1BjdCAhPT0gbnVsbCAmJiByb3cuc3VjY2Vzc1BjdCA8IHQuc3VjY2Vzc1JhdGVQY3QpIHtcbiAgICAgIGZpbmRpbmdzLnB1c2goe1xuICAgICAgICBydWxlOiAnbG93X3N1Y2Nlc3NfcmF0ZScsXG4gICAgICAgIHNldmVyaXR5OiByb3cuc3VjY2Vzc1BjdCA8IHQuc3VjY2Vzc1JhdGVQY3QgLyAyID8gJ2hpZ2gnIDogJ21lZGl1bScsXG4gICAgICAgIGV2aWRlbmNlOiBgJHtyb3cuc3VjY2Vzc1BjdH0lIG9mICR7cm93LnJ1bnN9IHJ1bnMgc3VjY2VlZGVkIChiZWxvdyAke3Quc3VjY2Vzc1JhdGVQY3R9JSlgLFxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLy8gMi4gR2V0dGluZyBzbG93ZXIuIE5lZWRzIGJvdGggcGVyaW9kcyBBTkQgYSByZWFsIGJhc2VsaW5lIOKAlCBhIGp1bXAgZnJvbSAyc1xuICAgIC8vIHRvIDNzIGlzIDUwJSBhbmQgbWVhbnMgbm90aGluZywgc28gYSB0aW55IGJhc2VsaW5lIGlzIG5vdCBldmlkZW5jZS5cbiAgICBjb25zdCBwcmlvciA9IHByaW9yQnlQaXBlbGluZS5nZXQocm93LnBpcGVsaW5lSWQpO1xuICAgIGlmIChyb3cucDk1TXMgIT09IG51bGwgJiYgcHJpb3I/LnA5NU1zICYmIHByaW9yLnA5NU1zID4gMTBfMDAwKSB7XG4gICAgICBjb25zdCBkZWx0YVBjdCA9ICgocm93LnA5NU1zIC0gcHJpb3IucDk1TXMpIC8gcHJpb3IucDk1TXMpICogMTAwO1xuICAgICAgaWYgKGRlbHRhUGN0ID4gdC5wOTVJbmNyZWFzZVBjdCkge1xuICAgICAgICBmaW5kaW5ncy5wdXNoKHtcbiAgICAgICAgICBydWxlOiAnc2xvd2VyX3A5NScsXG4gICAgICAgICAgc2V2ZXJpdHk6IGRlbHRhUGN0ID4gdC5wOTVJbmNyZWFzZVBjdCAqIDIgPyAnaGlnaCcgOiAnbWVkaXVtJyxcbiAgICAgICAgICBldmlkZW5jZTogYHA5NSB1cCAke2RlbHRhUGN0LnRvRml4ZWQoMCl9JSwgJHtkdXJhdGlvbihwcmlvci5wOTVNcyl9IOKGkiAke2R1cmF0aW9uKHJvdy5wOTVNcyl9YCxcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgY29uc3Qgc3RyZWFrID0gaW5wdXQuc3RyZWFrcz8uZ2V0KHJvdy5waXBlbGluZUlkKTtcblxuICAgIC8vIDMuIENvbnNlY3V0aXZlIGZhaWx1cmVzLiBUaGUgY2xlYXJlc3QgXCJ0aGlzIGlzIGJyb2tlbiBub3dcIiBzaWduYWwgdGhlcmUgaXM6XG4gICAgLy8gYSA3MCUgc3VjY2VzcyByYXRlIG92ZXIgYSBtb250aCBhbmQgdGhyZWUgZmFpbHVyZXMgaW4gYSByb3cgYXJlIHZlcnlcbiAgICAvLyBkaWZmZXJlbnQgc2l0dWF0aW9ucywgYW5kIG9ubHkgdGhlIHNlY29uZCBuZWVkcyBzb21lYm9keSB0b2RheS5cbiAgICBpZiAoc3RyZWFrICYmIHN0cmVhay5jb25zZWN1dGl2ZUZhaWx1cmVzID49IHQuY29uc2VjdXRpdmVGYWlsdXJlcykge1xuICAgICAgZmluZGluZ3MucHVzaCh7XG4gICAgICAgIHJ1bGU6ICdjb25zZWN1dGl2ZV9mYWlsdXJlcycsXG4gICAgICAgIHNldmVyaXR5OiAnaGlnaCcsXG4gICAgICAgIGV2aWRlbmNlOiBgJHtzdHJlYWsuY29uc2VjdXRpdmVGYWlsdXJlc30gY29uc2VjdXRpdmUgZmFpbHVyZXNgLFxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLy8gNC4gTm90aGluZyBoYXMgd29ya2VkIGxhdGVseS4gRGlzdGluY3QgZnJvbSB0aGUgcmF0ZTogYSBwaXBlbGluZSB0aGF0XG4gICAgLy8gc3VjY2VlZGVkIDkwJSBvZiB0aGUgdGltZSBhbmQgdGhlbiBzdG9wcGVkIHN1Y2NlZWRpbmcgZW50aXJlbHkgbG9va3MgZmluZVxuICAgIC8vIGJ5IHJhdGUgYW5kIGlzIGNvbXBsZXRlbHkgYnJva2VuLlxuICAgIGlmIChzdHJlYWsgJiYgcm93LnJ1bnMgPiAwKSB7XG4gICAgICBjb25zdCBkYXlzID0gc3RyZWFrLmxhc3RTdWNjZXNzQXRcbiAgICAgICAgPyBNYXRoLmZsb29yKChub3cuZ2V0VGltZSgpIC0gc3RyZWFrLmxhc3RTdWNjZXNzQXQuZ2V0VGltZSgpKSAvIDg2XzQwMF8wMDApXG4gICAgICAgIDogbnVsbDtcbiAgICAgIGlmIChkYXlzID09PSBudWxsKSB7XG4gICAgICAgIGZpbmRpbmdzLnB1c2goe1xuICAgICAgICAgIHJ1bGU6ICdub19yZWNlbnRfc3VjY2VzcycsXG4gICAgICAgICAgc2V2ZXJpdHk6ICdoaWdoJyxcbiAgICAgICAgICBldmlkZW5jZTogYG5vIHN1Y2Nlc3NmdWwgcnVuIGluIHRoaXMgcGVyaW9kICgke3Jvdy5ydW5zfSBhdHRlbXB0cylgLFxuICAgICAgICB9KTtcbiAgICAgIH0gZWxzZSBpZiAoZGF5cyA+PSB0LnN0YWxlRGF5cykge1xuICAgICAgICBmaW5kaW5ncy5wdXNoKHtcbiAgICAgICAgICBydWxlOiAnbm9fcmVjZW50X3N1Y2Nlc3MnLFxuICAgICAgICAgIHNldmVyaXR5OiBkYXlzID49IHQuc3RhbGVEYXlzICogMiA/ICdoaWdoJyA6ICdtZWRpdW0nLFxuICAgICAgICAgIGV2aWRlbmNlOiBgbGFzdCBzdWNjZWVkZWQgJHtkYXlzfSBkYXlzIGFnb2AsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIDUuIEJyb2tlIHNob3J0bHkgYWZ0ZXIgc29tZWJvZHkgY2hhbmdlZCBpdC4gVGhlIG1vc3QgdXNlZnVsIGNvcnJlbGF0aW9uIGluXG4gICAgLy8gdGhlIHdob2xlIHNlY3Rpb24sIGJlY2F1c2UgaXQgbmFtZXMgYSBsaWtlbHkgY2F1c2UgcmF0aGVyIHRoYW4gYSBzeW1wdG9tIOKAlFxuICAgIC8vIGFuZCBpdCBpcyB0aGUgb25lIGEgaHVtYW4gd291bGQgd29yayBvdXQgYnkgaGFuZCBmcm9tIHR3byBzY3JlZW5zLlxuICAgIGNvbnN0IGNoYW5nZWRBdCA9IGlucHV0LmNvbmZpZ0NoYW5nZXM/LmdldChyb3cucGlwZWxpbmVJZCk7XG4gICAgaWYgKGNoYW5nZWRBdCAmJiByb3cubGFzdEZhaWx1cmVBdCkge1xuICAgICAgY29uc3QgZ2FwSG91cnMgPSAobmV3IERhdGUocm93Lmxhc3RGYWlsdXJlQXQpLmdldFRpbWUoKSAtIGNoYW5nZWRBdC5nZXRUaW1lKCkpIC8gMzYwMF8wMDA7XG4gICAgICBpZiAoZ2FwSG91cnMgPj0gMCAmJiBnYXBIb3VycyA8PSB0LmZhaWx1cmVBZnRlckNoYW5nZUhvdXJzKSB7XG4gICAgICAgIGZpbmRpbmdzLnB1c2goe1xuICAgICAgICAgIHJ1bGU6ICdmYWlsZWRfYWZ0ZXJfY29uZmlnX2NoYW5nZScsXG4gICAgICAgICAgc2V2ZXJpdHk6ICdoaWdoJyxcbiAgICAgICAgICBldmlkZW5jZTogYGZhaWxlZCAke2dhcEhvdXJzIDwgMSA/ICdsZXNzIHRoYW4gYW4gaG91cicgOiBgJHtNYXRoLnJvdW5kKGdhcEhvdXJzKX1oYH0gYWZ0ZXIgYSBjb25maWd1cmF0aW9uIGNoYW5nZWBcbiAgICAgICAgICAgICsgKHJvdy5sYXN0RmFpbHVyZVN0YWdlID8gYCAoJHtyb3cubGFzdEZhaWx1cmVTdGFnZX0pYCA6ICcnKSxcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gNi4gUnVubmluZyBhIHBsdWdpbiB2ZXJzaW9uIHdpdGggb3BlbiBDcml0aWNhbCBmaW5kaW5ncy5cbiAgICBpZiAoaW5wdXQudnVsbmVyYWJsZVBpcGVsaW5lcz8uaGFzKHJvdy5waXBlbGluZUlkKSkge1xuICAgICAgZmluZGluZ3MucHVzaCh7XG4gICAgICAgIHJ1bGU6ICd2dWxuZXJhYmxlX3BsdWdpbicsXG4gICAgICAgIHNldmVyaXR5OiAnaGlnaCcsXG4gICAgICAgIGV2aWRlbmNlOiAncnVucyBhIHBsdWdpbiB2ZXJzaW9uIHdpdGggb3BlbiBDcml0aWNhbCB2dWxuZXJhYmlsaXRpZXMnLFxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLy8gNy4gSW52aXNpYmxlIHRvIERPUkEuIE5vdCBhIGZhaWx1cmUg4oCUIGEgTUVBU1VSRU1FTlQgZ2FwLCBhbmQgZmxhZ2dlZFxuICAgIC8vIGJlY2F1c2UgYSBwaXBlbGluZSBub2JvZHkgY2FuIHNlZSBkZXBsb3lpbmcgaXMgdGhlIHJlYXNvbiBhIGRlcGxveW1lbnQtXG4gICAgLy8gZnJlcXVlbmN5IG51bWJlciBpcyB3cm9uZywgYW5kIG5vdGhpbmcgZWxzZSBpbiB0aGUgcmVwb3J0IHdvdWxkIHNheSBzby5cbiAgICBpZiAoaW5wdXQudW50cmFja2VkUGlwZWxpbmVzPy5oYXMocm93LnBpcGVsaW5lSWQpICYmIHJvdy5ydW5zID4gMCkge1xuICAgICAgZmluZGluZ3MucHVzaCh7XG4gICAgICAgIHJ1bGU6ICdub19kZXBsb3lfdHJhY2tpbmcnLFxuICAgICAgICBzZXZlcml0eTogJ21lZGl1bScsXG4gICAgICAgIGV2aWRlbmNlOiAncmFuLCBidXQgcHJvZHVjZWQgbm8gZGVwbG95IGRhdGEg4oCUIHJlLXN5bnRoIHRvIGluY2x1ZGUgZGVwbG95IHRhZ3MnLFxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgaWYgKGZpbmRpbmdzLmxlbmd0aCA+IDApIHtcbiAgICAgIGl0ZW1zLnB1c2goe1xuICAgICAgICBwaXBlbGluZUlkOiByb3cucGlwZWxpbmVJZCxcbiAgICAgICAgcGlwZWxpbmVOYW1lOiByb3cucGlwZWxpbmVOYW1lLFxuICAgICAgICBmaW5kaW5ncyxcbiAgICAgICAgc2V2ZXJpdHk6IGZpbmRpbmdzLnNvbWUoKGYpID0+IGYuc2V2ZXJpdHkgPT09ICdoaWdoJykgPyAnaGlnaCcgOiAnbWVkaXVtJyxcbiAgICAgIH0pO1xuICAgIH1cbiAgfVxuXG4gIC8vIEhpZ2ggZmlyc3QsIHRoZW4gdGhlIHBpcGVsaW5lcyB3aXRoIHRoZSBtb3N0IHdyb25nIHdpdGggdGhlbS5cbiAgcmV0dXJuIGl0ZW1zLnNvcnQoKGEsIGIpID0+IHtcbiAgICBpZiAoYS5zZXZlcml0eSAhPT0gYi5zZXZlcml0eSkgcmV0dXJuIGEuc2V2ZXJpdHkgPT09ICdoaWdoJyA/IC0xIDogMTtcbiAgICByZXR1cm4gYi5maW5kaW5ncy5sZW5ndGggLSBhLmZpbmRpbmdzLmxlbmd0aDtcbiAgfSk7XG59XG5cbi8qKlxuICogUmVhZCBhbiBvcmcncyB0aHJlc2hvbGRzIG91dCBvZiBzdG9yZWQgc2V0dGluZ3MsIGZhbGxpbmcgYmFjayBwZXIgZmllbGQuXG4gKlxuICogUGVyIEZJRUxELCBub3QgcGVyIG9iamVjdDogYW4gb3JnIHRoYXQgc2V0IG9ubHkgYHN1Y2Nlc3NSYXRlUGN0YCBrZWVwcyB0aGVcbiAqIGRlZmF1bHRzIGZvciBldmVyeXRoaW5nIGVsc2UsIHJhdGhlciB0aGFuIHNpbGVudGx5IGRpc2FibGluZyBmb3VyIHJ1bGVzIGJ5XG4gKiBjb25maWd1cmluZyBvbmUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB0aHJlc2hvbGRzRnJvbShzdG9yZWQ6IFBhcnRpYWw8TmVlZHNBdHRlbnRpb25UaHJlc2hvbGRzPiB8IG51bGwgfCB1bmRlZmluZWQpOiBOZWVkc0F0dGVudGlvblRocmVzaG9sZHMge1xuICByZXR1cm4geyAuLi5ERUZBVUxUX1RIUkVTSE9MRFMsIC4uLihzdG9yZWQgPz8ge30pIH07XG59XG4iXX0=
@@ -0,0 +1,129 @@
1
+ import type { ExposureSource } from '../../database/schema/reporting-analytics.js';
2
+ /** How far behind a resolved version is from the newest one available. */
3
+ export type VersionGap = 'major' | 'minor' | 'patch' | 'none';
4
+ /** One resolved step, as the writer takes it. */
5
+ export interface ResolvedStep {
6
+ orgId: string;
7
+ pipelineId: string;
8
+ stageName: string;
9
+ stepName: string;
10
+ pluginPublisher?: string | null;
11
+ pluginName: string;
12
+ declaredVersion?: string | null;
13
+ resolvedVersion?: string | null;
14
+ latestVersion?: string | null;
15
+ withinPolicy?: boolean;
16
+ }
17
+ /**
18
+ * How far `resolved` is behind `latest`.
19
+ *
20
+ * `null` when either side is unparseable, NOT `'none'`. "We could not tell" and
21
+ * "it is up to date" are opposite answers, and reporting the second for the first
22
+ * is how a pipeline on an ancient pre-release version gets a clean bill of health.
23
+ */
24
+ export declare function versionGap(resolved: string | null | undefined, latest: string | null | undefined): VersionGap | null;
25
+ /**
26
+ * Replace a pipeline's resolution rows.
27
+ *
28
+ * DELETE-then-INSERT for the pipeline, in one transaction, because a step can be
29
+ * REMOVED from a config: an upsert-only writer would leave the deleted step's row
30
+ * behind forever, and the outdated-plugin section would keep reporting a step the
31
+ * pipeline no longer has.
32
+ */
33
+ export declare function replacePipelineResolution(pipelineId: string, steps: readonly ResolvedStep[]): Promise<number>;
34
+ /**
35
+ * Refresh the `latest`/gap columns for one plugin across every pipeline that uses
36
+ * it, without re-resolving anything else.
37
+ *
38
+ * Called on publish. A publish changes what "latest" means for every pipeline in
39
+ * the instance that declares the plugin, and re-resolving each of those pipelines
40
+ * from scratch would make publishing cost O(pipelines) work — so only the two
41
+ * columns a publish can change are touched.
42
+ */
43
+ export declare function refreshLatestForPlugin(pluginName: string, pluginPublisher: string | null, latestVersion: string): Promise<number>;
44
+ /** One flagged version, as the rescan reports it. */
45
+ export interface ExposureInput {
46
+ orgId: string;
47
+ pipelineId: string;
48
+ pluginPublisher?: string | null;
49
+ pluginName: string;
50
+ pluginVersion: string;
51
+ imageDigest?: string | null;
52
+ source: ExposureSource;
53
+ criticalCount: number;
54
+ highCount: number;
55
+ topFindings?: Array<Record<string, unknown>>;
56
+ }
57
+ /**
58
+ * Open or refresh exposures.
59
+ *
60
+ * Upserts on (pipeline, plugin, version, source) so a nightly rescan REFRESHES the
61
+ * counts rather than opening a new row every night — otherwise the report would
62
+ * show one exposure per night of the same problem, which reads as an escalating
63
+ * situation when nothing changed.
64
+ *
65
+ * A refresh deliberately does NOT reset `triage_state`: an org that accepted a
66
+ * finding with a reason and a deadline has made a decision, and re-raising it every
67
+ * night is how a team learns to ignore the report. It DOES clear a stale
68
+ * acceptance, because that is what the deadline was for.
69
+ */
70
+ export declare function upsertExposures(inputs: readonly ExposureInput[], now?: Date): Promise<number>;
71
+ /**
72
+ * Close the exposures a pipeline has moved off.
73
+ *
74
+ * Called when a deploy records a new step manifest: anything flagged for that
75
+ * pipeline whose version is NOT in the set it now runs is fixed. Closing by
76
+ * absence rather than waiting for a rescan to notice is what makes "we fixed it"
77
+ * show up in the report on the next deploy instead of the next night.
78
+ */
79
+ export declare function closeFixedExposures(pipelineId: string, liveVersions: ReadonlyArray<{
80
+ pluginName: string;
81
+ pluginVersion: string;
82
+ }>, now?: Date): Promise<number>;
83
+ /**
84
+ * The org's live exposures, for the report section.
85
+ *
86
+ * `accepted` rows whose deadline has passed come back as OPEN even if no rescan
87
+ * has refreshed them yet: a read must not present an expired acceptance as a
88
+ * current one, and the report is read far more often than the rescan runs.
89
+ */
90
+ export declare function openExposures(orgId: string): Promise<Array<{
91
+ pipelineId: string;
92
+ pluginName: string;
93
+ pluginVersion: string;
94
+ pluginPublisher: string | null;
95
+ source: ExposureSource;
96
+ criticalCount: number;
97
+ highCount: number;
98
+ topFindings: Array<Record<string, unknown>>;
99
+ triageState: string;
100
+ flaggedAt: Date;
101
+ }>>;
102
+ /**
103
+ * Record a triage decision.
104
+ *
105
+ * `accepted` REQUIRES a deadline and a reason. A permanent, unexplained mute is
106
+ * how a finding stops being anybody's problem, and there is no good reason to
107
+ * support it — the caller gets an error rather than a silently unbounded
108
+ * acceptance.
109
+ */
110
+ export declare function triageExposure(orgId: string, id: string, decision: {
111
+ state: 'open' | 'accepted' | 'false_positive';
112
+ reason?: string;
113
+ acceptedUntil?: Date;
114
+ actor: string;
115
+ }): Promise<boolean>;
116
+ /**
117
+ * Exposures whose acceptance has lapsed, for the org's security notifications.
118
+ *
119
+ * Surfaced rather than silently reopened: somebody decided to accept this until a
120
+ * date, that date has passed, and the person who decided is the one who should
121
+ * hear about it.
122
+ */
123
+ export declare function lapsedAcceptances(orgId: string, now?: Date): Promise<Array<{
124
+ id: string;
125
+ pluginName: string;
126
+ pluginVersion: string;
127
+ triagedBy: string | null;
128
+ acceptedUntil: Date | null;
129
+ }>>;