@pipeline-builder/pipeline-data 4.1.1 → 4.1.2
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.
- package/lib/api/reporting/caches.d.ts +2 -0
- package/lib/api/reporting/caches.js +14 -0
- package/lib/api/reporting/dora-scoring.d.ts +71 -0
- package/lib/api/reporting/dora-scoring.js +126 -0
- package/lib/api/reporting/dora.d.ts +47 -0
- package/lib/api/reporting/dora.js +501 -0
- package/lib/api/reporting/query-scope.d.ts +21 -0
- package/lib/api/reporting/query-scope.js +62 -0
- package/lib/api/reporting/retention.d.ts +44 -0
- package/lib/api/reporting/retention.js +64 -0
- package/lib/api/reporting/sql-helpers.d.ts +47 -0
- package/lib/api/reporting/sql-helpers.js +65 -0
- package/lib/api/reporting/types.d.ts +469 -0
- package/lib/api/reporting/types.js +4 -0
- package/lib/api/reporting-service.d.ts +7 -500
- package/lib/api/reporting-service.js +46 -760
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* The two report caches. A separate module because both the service (for
|
|
5
|
+
* invalidation) and the extracted query modules (via runReport) need them, and a
|
|
6
|
+
* shared singleton must have exactly one definition site.
|
|
7
|
+
*
|
|
8
|
+
* Inventory reads (plugin summary/distribution/versions) change rarely — 5 min.
|
|
9
|
+
* Timeseries reads (execution/build metrics over a range) — 2 min.
|
|
10
|
+
*/
|
|
11
|
+
import { createCacheService } from '@pipeline-builder/api-core';
|
|
12
|
+
export const inventoryCache = createCacheService('report:inv:', parseInt(process.env.CACHE_TTL_REPORT_INVENTORY || '300', 10));
|
|
13
|
+
export const timeseriesCache = createCacheService('report:ts:', parseInt(process.env.CACHE_TTL_REPORT_TIMESERIES || '120', 10));
|
|
14
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FjaGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvY2FjaGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEM7Ozs7Ozs7R0FPRztBQUVILE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRWhFLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxrQkFBa0IsQ0FBQyxhQUFhLEVBQUUsUUFBUSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsMEJBQTBCLElBQUksS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFDL0gsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLGtCQUFrQixDQUFDLFlBQVksRUFBRSxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQkFBMkIsSUFBSSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFRoZSB0d28gcmVwb3J0IGNhY2hlcy4gQSBzZXBhcmF0ZSBtb2R1bGUgYmVjYXVzZSBib3RoIHRoZSBzZXJ2aWNlIChmb3JcbiAqIGludmFsaWRhdGlvbikgYW5kIHRoZSBleHRyYWN0ZWQgcXVlcnkgbW9kdWxlcyAodmlhIHJ1blJlcG9ydCkgbmVlZCB0aGVtLCBhbmQgYVxuICogc2hhcmVkIHNpbmdsZXRvbiBtdXN0IGhhdmUgZXhhY3RseSBvbmUgZGVmaW5pdGlvbiBzaXRlLlxuICpcbiAqIEludmVudG9yeSByZWFkcyAocGx1Z2luIHN1bW1hcnkvZGlzdHJpYnV0aW9uL3ZlcnNpb25zKSBjaGFuZ2UgcmFyZWx5IOKAlCA1IG1pbi5cbiAqIFRpbWVzZXJpZXMgcmVhZHMgKGV4ZWN1dGlvbi9idWlsZCBtZXRyaWNzIG92ZXIgYSByYW5nZSkg4oCUIDIgbWluLlxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUNhY2hlU2VydmljZSB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcblxuZXhwb3J0IGNvbnN0IGludmVudG9yeUNhY2hlID0gY3JlYXRlQ2FjaGVTZXJ2aWNlKCdyZXBvcnQ6aW52OicsIHBhcnNlSW50KHByb2Nlc3MuZW52LkNBQ0hFX1RUTF9SRVBPUlRfSU5WRU5UT1JZIHx8ICczMDAnLCAxMCkpO1xuZXhwb3J0IGNvbnN0IHRpbWVzZXJpZXNDYWNoZSA9IGNyZWF0ZUNhY2hlU2VydmljZSgncmVwb3J0OnRzOicsIHBhcnNlSW50KHByb2Nlc3MuZW52LkNBQ0hFX1RUTF9SRVBPUlRfVElNRVNFUklFUyB8fCAnMTIwJywgMTApKTtcbiJdfQ==
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { DoraLevel } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* DORA performance banding plus the small statistics helpers it needs.
|
|
4
|
+
* Pure functions — no database access, no I/O — which is what lets the
|
|
5
|
+
* thresholds be unit-tested independently of the queries that feed them.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The DORA headline environment. Deployment to this literal env name is the
|
|
9
|
+
* industry-standard DORA signal; MTTR is measured production-only, and this
|
|
10
|
+
* environment's card is the summary shown at the top of the panel.
|
|
11
|
+
*/
|
|
12
|
+
export declare const HEADLINE_ENV = "production";
|
|
13
|
+
/**
|
|
14
|
+
* Incident→deploy correlation window (Phase 5). An incident is attributed to the
|
|
15
|
+
* most recent SUCCESSFUL deploy to its environment whose `completed_at` is within
|
|
16
|
+
* this many hours before the incident's `opened_at`. Default 24h; override via
|
|
17
|
+
* `DORA_INCIDENT_WINDOW_HOURS`. Guarded to a positive finite number.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DORA_INCIDENT_WINDOW_HOURS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the effective incident→deploy correlation window (Phase 5b). A per-org
|
|
22
|
+
* override (from `dora_settings.incident_window_hours`, surfaced through the
|
|
23
|
+
* settings endpoint and threaded into {@link DoraOptions.incidentWindowHours})
|
|
24
|
+
* wins when it is a positive finite number; otherwise the global env default
|
|
25
|
+
* `DORA_INCIDENT_WINDOW_HOURS` applies. Centralized so the DORA correlation, the
|
|
26
|
+
* incidents list, and the wiring-test dry-run all agree on the same rule.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveIncidentWindowHours(override?: number | null): number;
|
|
29
|
+
/**
|
|
30
|
+
* Reporting retention windows (Phase 7). Records in `pipeline_events`,
|
|
31
|
+
* `deployment_outcomes`, and `incidents` grow unbounded without a sweep, so a
|
|
32
|
+
* split, per-org retention purge (see {@link ReportingService.purgeExpiredReportingData})
|
|
33
|
+
* hard-deletes rows older than these windows, by `created_at`:
|
|
34
|
+
* - **Standard events** — `pipeline_events` with `environment IS NULL`
|
|
35
|
+
* (non-deploy STAGE/ACTION/build). High volume → short default (30 days).
|
|
36
|
+
* - **DORA source** — `pipeline_events` with `environment IS NOT NULL` (deploy
|
|
37
|
+
* stages) plus all of `deployment_outcomes` and `incidents`. Low volume →
|
|
38
|
+
* longer default (180 days). DORA history is therefore bounded by this window;
|
|
39
|
+
* the report query still hard-caps at 365 days (a longer per-org override only
|
|
40
|
+
* retains raw source rows, never widens a report).
|
|
41
|
+
* `ingest_health` and `dora_settings` are never purged. Env-overridable globals;
|
|
42
|
+
* a per-org override in `dora_settings` wins (see {@link resolveEventRetentionDays}).
|
|
43
|
+
*/
|
|
44
|
+
/** Deployment frequency by deploys/day: elite ≥1/day, high ≥1/week, medium ≥1/month. */
|
|
45
|
+
export declare function doraLevelForFrequency(perDay: number, deployments: number): DoraLevel;
|
|
46
|
+
/** Change failure rate by percent: elite ≤5%, high ≤10%, medium ≤15%. */
|
|
47
|
+
export declare function doraLevelForChangeFailure(pct: number, total: number): DoraLevel;
|
|
48
|
+
/** Time to restore by seconds: elite <1h, high <1 day, medium <1 week. */
|
|
49
|
+
export declare function doraLevelForRestore(avgSeconds: number | null): DoraLevel;
|
|
50
|
+
/** Lead time by seconds: elite <1 day, high <1 week, medium <1 month. */
|
|
51
|
+
export declare function doraLevelForLeadTime(medianSeconds: number | null): DoraLevel;
|
|
52
|
+
/**
|
|
53
|
+
* Round `value` to `digits` decimal places, returning a real number. Small
|
|
54
|
+
* module-private helper so the DORA shaping doesn't repeat the
|
|
55
|
+
* `Number(Number(x).toFixed(n))` pattern (toFixed alone yields a string).
|
|
56
|
+
*/
|
|
57
|
+
export declare function round(value: number, digits: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* Median of a numeric sample, or `null` when empty. Used for measured lead time
|
|
60
|
+
* and MTTR — both are cross-source gaps computed in JS from clamped deltas, so
|
|
61
|
+
* the median lives here rather than in SQL PERCENTILE_CONT (keeps the golden
|
|
62
|
+
* fixture tests able to prove the metric math without a live Postgres).
|
|
63
|
+
*/
|
|
64
|
+
export declare function median(values: number[]): number | null;
|
|
65
|
+
/**
|
|
66
|
+
* Roll a group of deploy/stage events (grouped per execution) up to one terminal
|
|
67
|
+
* status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).
|
|
68
|
+
* Shared by the three per-(env|stage, execution) rollup CTEs — DORA metrics, DORA
|
|
69
|
+
* trend, and build health — so the precedence can't drift between them. Assumes the
|
|
70
|
+
* grouped event alias is `e`.
|
|
71
|
+
*/
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* DORA performance banding plus the small statistics helpers it needs.
|
|
5
|
+
* Pure functions — no database access, no I/O — which is what lets the
|
|
6
|
+
* thresholds be unit-tested independently of the queries that feed them.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The DORA headline environment. Deployment to this literal env name is the
|
|
10
|
+
* industry-standard DORA signal; MTTR is measured production-only, and this
|
|
11
|
+
* environment's card is the summary shown at the top of the panel.
|
|
12
|
+
*/
|
|
13
|
+
export const HEADLINE_ENV = 'production';
|
|
14
|
+
/**
|
|
15
|
+
* Incident→deploy correlation window (Phase 5). An incident is attributed to the
|
|
16
|
+
* most recent SUCCESSFUL deploy to its environment whose `completed_at` is within
|
|
17
|
+
* this many hours before the incident's `opened_at`. Default 24h; override via
|
|
18
|
+
* `DORA_INCIDENT_WINDOW_HOURS`. Guarded to a positive finite number.
|
|
19
|
+
*/
|
|
20
|
+
export const DORA_INCIDENT_WINDOW_HOURS = (() => {
|
|
21
|
+
const raw = Number(process.env.DORA_INCIDENT_WINDOW_HOURS);
|
|
22
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 24;
|
|
23
|
+
})();
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the effective incident→deploy correlation window (Phase 5b). A per-org
|
|
26
|
+
* override (from `dora_settings.incident_window_hours`, surfaced through the
|
|
27
|
+
* settings endpoint and threaded into {@link DoraOptions.incidentWindowHours})
|
|
28
|
+
* wins when it is a positive finite number; otherwise the global env default
|
|
29
|
+
* `DORA_INCIDENT_WINDOW_HOURS` applies. Centralized so the DORA correlation, the
|
|
30
|
+
* incidents list, and the wiring-test dry-run all agree on the same rule.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveIncidentWindowHours(override) {
|
|
33
|
+
return override != null && Number.isFinite(override) && override > 0 ? override : DORA_INCIDENT_WINDOW_HOURS;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reporting retention windows (Phase 7). Records in `pipeline_events`,
|
|
37
|
+
* `deployment_outcomes`, and `incidents` grow unbounded without a sweep, so a
|
|
38
|
+
* split, per-org retention purge (see {@link ReportingService.purgeExpiredReportingData})
|
|
39
|
+
* hard-deletes rows older than these windows, by `created_at`:
|
|
40
|
+
* - **Standard events** — `pipeline_events` with `environment IS NULL`
|
|
41
|
+
* (non-deploy STAGE/ACTION/build). High volume → short default (30 days).
|
|
42
|
+
* - **DORA source** — `pipeline_events` with `environment IS NOT NULL` (deploy
|
|
43
|
+
* stages) plus all of `deployment_outcomes` and `incidents`. Low volume →
|
|
44
|
+
* longer default (180 days). DORA history is therefore bounded by this window;
|
|
45
|
+
* the report query still hard-caps at 365 days (a longer per-org override only
|
|
46
|
+
* retains raw source rows, never widens a report).
|
|
47
|
+
* `ingest_health` and `dora_settings` are never purged. Env-overridable globals;
|
|
48
|
+
* a per-org override in `dora_settings` wins (see {@link resolveEventRetentionDays}).
|
|
49
|
+
*/
|
|
50
|
+
/** Deployment frequency by deploys/day: elite ≥1/day, high ≥1/week, medium ≥1/month. */
|
|
51
|
+
export function doraLevelForFrequency(perDay, deployments) {
|
|
52
|
+
if (deployments <= 0)
|
|
53
|
+
return null;
|
|
54
|
+
if (perDay >= 1)
|
|
55
|
+
return 'elite';
|
|
56
|
+
if (perDay >= 1 / 7)
|
|
57
|
+
return 'high';
|
|
58
|
+
if (perDay >= 1 / 30)
|
|
59
|
+
return 'medium';
|
|
60
|
+
return 'low';
|
|
61
|
+
}
|
|
62
|
+
/** Change failure rate by percent: elite ≤5%, high ≤10%, medium ≤15%. */
|
|
63
|
+
export function doraLevelForChangeFailure(pct, total) {
|
|
64
|
+
if (total <= 0)
|
|
65
|
+
return null;
|
|
66
|
+
if (pct <= 5)
|
|
67
|
+
return 'elite';
|
|
68
|
+
if (pct <= 10)
|
|
69
|
+
return 'high';
|
|
70
|
+
if (pct <= 15)
|
|
71
|
+
return 'medium';
|
|
72
|
+
return 'low';
|
|
73
|
+
}
|
|
74
|
+
/** Time to restore by seconds: elite <1h, high <1 day, medium <1 week. */
|
|
75
|
+
export function doraLevelForRestore(avgSeconds) {
|
|
76
|
+
if (avgSeconds == null)
|
|
77
|
+
return null;
|
|
78
|
+
if (avgSeconds < 3600)
|
|
79
|
+
return 'elite';
|
|
80
|
+
if (avgSeconds < 86400)
|
|
81
|
+
return 'high';
|
|
82
|
+
if (avgSeconds < 604800)
|
|
83
|
+
return 'medium';
|
|
84
|
+
return 'low';
|
|
85
|
+
}
|
|
86
|
+
/** Lead time by seconds: elite <1 day, high <1 week, medium <1 month. */
|
|
87
|
+
export function doraLevelForLeadTime(medianSeconds) {
|
|
88
|
+
if (medianSeconds == null)
|
|
89
|
+
return null;
|
|
90
|
+
if (medianSeconds < 86400)
|
|
91
|
+
return 'elite';
|
|
92
|
+
if (medianSeconds < 604800)
|
|
93
|
+
return 'high';
|
|
94
|
+
if (medianSeconds < 2592000)
|
|
95
|
+
return 'medium';
|
|
96
|
+
return 'low';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Round `value` to `digits` decimal places, returning a real number. Small
|
|
100
|
+
* module-private helper so the DORA shaping doesn't repeat the
|
|
101
|
+
* `Number(Number(x).toFixed(n))` pattern (toFixed alone yields a string).
|
|
102
|
+
*/
|
|
103
|
+
export function round(value, digits) {
|
|
104
|
+
return Number(value.toFixed(digits));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Median of a numeric sample, or `null` when empty. Used for measured lead time
|
|
108
|
+
* and MTTR — both are cross-source gaps computed in JS from clamped deltas, so
|
|
109
|
+
* the median lives here rather than in SQL PERCENTILE_CONT (keeps the golden
|
|
110
|
+
* fixture tests able to prove the metric math without a live Postgres).
|
|
111
|
+
*/
|
|
112
|
+
export function median(values) {
|
|
113
|
+
if (values.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const s = [...values].sort((a, b) => a - b);
|
|
116
|
+
const mid = Math.floor(s.length / 2);
|
|
117
|
+
return s.length % 2 === 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Roll a group of deploy/stage events (grouped per execution) up to one terminal
|
|
121
|
+
* status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).
|
|
122
|
+
* Shared by the three per-(env|stage, execution) rollup CTEs — DORA metrics, DORA
|
|
123
|
+
* trend, and build health — so the precedence can't drift between them. Assumes the
|
|
124
|
+
* grouped event alias is `e`.
|
|
125
|
+
*/
|
|
126
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZG9yYS1zY29yaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvZG9yYS1zY29yaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFJdEM7Ozs7R0FJRztBQUVIOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxZQUFZLEdBQUcsWUFBWSxDQUFDO0FBRXpDOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQUcsQ0FBQyxHQUFHLEVBQUU7SUFDOUMsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsMEJBQTBCLENBQUMsQ0FBQztJQUMzRCxPQUFPLE1BQU0sQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7QUFDcEQsQ0FBQyxDQUFDLEVBQUUsQ0FBQztBQUVMOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsMEJBQTBCLENBQUMsUUFBd0I7SUFDakUsT0FBTyxRQUFRLElBQUksSUFBSSxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQywwQkFBMEIsQ0FBQztBQUMvRyxDQUFDO0FBRUQ7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFFSCx3RkFBd0Y7QUFDeEYsTUFBTSxVQUFVLHFCQUFxQixDQUFDLE1BQWMsRUFBRSxXQUFtQjtJQUN2RSxJQUFJLFdBQVcsSUFBSSxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDbEMsSUFBSSxNQUFNLElBQUksQ0FBQztRQUFFLE9BQU8sT0FBTyxDQUFDO0lBQ2hDLElBQUksTUFBTSxJQUFJLENBQUMsR0FBRyxDQUFDO1FBQUUsT0FBTyxNQUFNLENBQUM7SUFDbkMsSUFBSSxNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUU7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUN0QyxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFDRCx5RUFBeUU7QUFDekUsTUFBTSxVQUFVLHlCQUF5QixDQUFDLEdBQVcsRUFBRSxLQUFhO0lBQ2xFLElBQUksS0FBSyxJQUFJLENBQUM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUM1QixJQUFJLEdBQUcsSUFBSSxDQUFDO1FBQUUsT0FBTyxPQUFPLENBQUM7SUFDN0IsSUFBSSxHQUFHLElBQUksRUFBRTtRQUFFLE9BQU8sTUFBTSxDQUFDO0lBQzdCLElBQUksR0FBRyxJQUFJLEVBQUU7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUMvQixPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFDRCwwRUFBMEU7QUFDMUUsTUFBTSxVQUFVLG1CQUFtQixDQUFDLFVBQXlCO0lBQzNELElBQUksVUFBVSxJQUFJLElBQUk7UUFBRSxPQUFPLElBQUksQ0FBQztJQUNwQyxJQUFJLFVBQVUsR0FBRyxJQUFJO1FBQUUsT0FBTyxPQUFPLENBQUM7SUFDdEMsSUFBSSxVQUFVLEdBQUcsS0FBSztRQUFFLE9BQU8sTUFBTSxDQUFDO0lBQ3RDLElBQUksVUFBVSxHQUFHLE1BQU07UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUN6QyxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFDRCx5RUFBeUU7QUFDekUsTUFBTSxVQUFVLG9CQUFvQixDQUFDLGFBQTRCO0lBQy9ELElBQUksYUFBYSxJQUFJLElBQUk7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN2QyxJQUFJLGFBQWEsR0FBRyxLQUFLO1FBQUUsT0FBTyxPQUFPLENBQUM7SUFDMUMsSUFBSSxhQUFhLEdBQUcsTUFBTTtRQUFFLE9BQU8sTUFBTSxDQUFDO0lBQzFDLElBQUksYUFBYSxHQUFHLE9BQU87UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUM3QyxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLEtBQUssQ0FBQyxLQUFhLEVBQUUsTUFBYztJQUNqRCxPQUFPLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7QUFDdkMsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLE1BQU0sQ0FBQyxNQUFnQjtJQUNyQyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3JDLE1BQU0sQ0FBQyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDNUMsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQ3JDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUM7QUFDakUsQ0FBQztBQUVEOzs7Ozs7R0FNRyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgdHlwZSB7IERvcmFMZXZlbCB9IGZyb20gJy4vdHlwZXMuanMnO1xuXG4vKipcbiAqIERPUkEgcGVyZm9ybWFuY2UgYmFuZGluZyBwbHVzIHRoZSBzbWFsbCBzdGF0aXN0aWNzIGhlbHBlcnMgaXQgbmVlZHMuXG4gKiBQdXJlIGZ1bmN0aW9ucyDigJQgbm8gZGF0YWJhc2UgYWNjZXNzLCBubyBJL08g4oCUIHdoaWNoIGlzIHdoYXQgbGV0cyB0aGVcbiAqIHRocmVzaG9sZHMgYmUgdW5pdC10ZXN0ZWQgaW5kZXBlbmRlbnRseSBvZiB0aGUgcXVlcmllcyB0aGF0IGZlZWQgdGhlbS5cbiAqL1xuXG4vKipcbiAqIFRoZSBET1JBIGhlYWRsaW5lIGVudmlyb25tZW50LiBEZXBsb3ltZW50IHRvIHRoaXMgbGl0ZXJhbCBlbnYgbmFtZSBpcyB0aGVcbiAqIGluZHVzdHJ5LXN0YW5kYXJkIERPUkEgc2lnbmFsOyBNVFRSIGlzIG1lYXN1cmVkIHByb2R1Y3Rpb24tb25seSwgYW5kIHRoaXNcbiAqIGVudmlyb25tZW50J3MgY2FyZCBpcyB0aGUgc3VtbWFyeSBzaG93biBhdCB0aGUgdG9wIG9mIHRoZSBwYW5lbC5cbiAqL1xuZXhwb3J0IGNvbnN0IEhFQURMSU5FX0VOViA9ICdwcm9kdWN0aW9uJztcblxuLyoqXG4gKiBJbmNpZGVudOKGkmRlcGxveSBjb3JyZWxhdGlvbiB3aW5kb3cgKFBoYXNlIDUpLiBBbiBpbmNpZGVudCBpcyBhdHRyaWJ1dGVkIHRvIHRoZVxuICogbW9zdCByZWNlbnQgU1VDQ0VTU0ZVTCBkZXBsb3kgdG8gaXRzIGVudmlyb25tZW50IHdob3NlIGBjb21wbGV0ZWRfYXRgIGlzIHdpdGhpblxuICogdGhpcyBtYW55IGhvdXJzIGJlZm9yZSB0aGUgaW5jaWRlbnQncyBgb3BlbmVkX2F0YC4gRGVmYXVsdCAyNGg7IG92ZXJyaWRlIHZpYVxuICogYERPUkFfSU5DSURFTlRfV0lORE9XX0hPVVJTYC4gR3VhcmRlZCB0byBhIHBvc2l0aXZlIGZpbml0ZSBudW1iZXIuXG4gKi9cbmV4cG9ydCBjb25zdCBET1JBX0lOQ0lERU5UX1dJTkRPV19IT1VSUyA9ICgoKSA9PiB7XG4gIGNvbnN0IHJhdyA9IE51bWJlcihwcm9jZXNzLmVudi5ET1JBX0lOQ0lERU5UX1dJTkRPV19IT1VSUyk7XG4gIHJldHVybiBOdW1iZXIuaXNGaW5pdGUocmF3KSAmJiByYXcgPiAwID8gcmF3IDogMjQ7XG59KSgpO1xuXG4vKipcbiAqIFJlc29sdmUgdGhlIGVmZmVjdGl2ZSBpbmNpZGVudOKGkmRlcGxveSBjb3JyZWxhdGlvbiB3aW5kb3cgKFBoYXNlIDViKS4gQSBwZXItb3JnXG4gKiBvdmVycmlkZSAoZnJvbSBgZG9yYV9zZXR0aW5ncy5pbmNpZGVudF93aW5kb3dfaG91cnNgLCBzdXJmYWNlZCB0aHJvdWdoIHRoZVxuICogc2V0dGluZ3MgZW5kcG9pbnQgYW5kIHRocmVhZGVkIGludG8ge0BsaW5rIERvcmFPcHRpb25zLmluY2lkZW50V2luZG93SG91cnN9KVxuICogd2lucyB3aGVuIGl0IGlzIGEgcG9zaXRpdmUgZmluaXRlIG51bWJlcjsgb3RoZXJ3aXNlIHRoZSBnbG9iYWwgZW52IGRlZmF1bHRcbiAqIGBET1JBX0lOQ0lERU5UX1dJTkRPV19IT1VSU2AgYXBwbGllcy4gQ2VudHJhbGl6ZWQgc28gdGhlIERPUkEgY29ycmVsYXRpb24sIHRoZVxuICogaW5jaWRlbnRzIGxpc3QsIGFuZCB0aGUgd2lyaW5nLXRlc3QgZHJ5LXJ1biBhbGwgYWdyZWUgb24gdGhlIHNhbWUgcnVsZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlc29sdmVJbmNpZGVudFdpbmRvd0hvdXJzKG92ZXJyaWRlPzogbnVtYmVyIHwgbnVsbCk6IG51bWJlciB7XG4gIHJldHVybiBvdmVycmlkZSAhPSBudWxsICYmIE51bWJlci5pc0Zpbml0ZShvdmVycmlkZSkgJiYgb3ZlcnJpZGUgPiAwID8gb3ZlcnJpZGUgOiBET1JBX0lOQ0lERU5UX1dJTkRPV19IT1VSUztcbn1cblxuLyoqXG4gKiBSZXBvcnRpbmcgcmV0ZW50aW9uIHdpbmRvd3MgKFBoYXNlIDcpLiBSZWNvcmRzIGluIGBwaXBlbGluZV9ldmVudHNgLFxuICogYGRlcGxveW1lbnRfb3V0Y29tZXNgLCBhbmQgYGluY2lkZW50c2AgZ3JvdyB1bmJvdW5kZWQgd2l0aG91dCBhIHN3ZWVwLCBzbyBhXG4gKiBzcGxpdCwgcGVyLW9yZyByZXRlbnRpb24gcHVyZ2UgKHNlZSB7QGxpbmsgUmVwb3J0aW5nU2VydmljZS5wdXJnZUV4cGlyZWRSZXBvcnRpbmdEYXRhfSlcbiAqIGhhcmQtZGVsZXRlcyByb3dzIG9sZGVyIHRoYW4gdGhlc2Ugd2luZG93cywgYnkgYGNyZWF0ZWRfYXRgOlxuICogIC0gKipTdGFuZGFyZCBldmVudHMqKiDigJQgYHBpcGVsaW5lX2V2ZW50c2Agd2l0aCBgZW52aXJvbm1lbnQgSVMgTlVMTGBcbiAqICAgIChub24tZGVwbG95IFNUQUdFL0FDVElPTi9idWlsZCkuIEhpZ2ggdm9sdW1lIOKGkiBzaG9ydCBkZWZhdWx0ICgzMCBkYXlzKS5cbiAqICAtICoqRE9SQSBzb3VyY2UqKiDigJQgYHBpcGVsaW5lX2V2ZW50c2Agd2l0aCBgZW52aXJvbm1lbnQgSVMgTk9UIE5VTExgIChkZXBsb3lcbiAqICAgIHN0YWdlcykgcGx1cyBhbGwgb2YgYGRlcGxveW1lbnRfb3V0Y29tZXNgIGFuZCBgaW5jaWRlbnRzYC4gTG93IHZvbHVtZSDihpJcbiAqICAgIGxvbmdlciBkZWZhdWx0ICgxODAgZGF5cykuIERPUkEgaGlzdG9yeSBpcyB0aGVyZWZvcmUgYm91bmRlZCBieSB0aGlzIHdpbmRvdztcbiAqICAgIHRoZSByZXBvcnQgcXVlcnkgc3RpbGwgaGFyZC1jYXBzIGF0IDM2NSBkYXlzIChhIGxvbmdlciBwZXItb3JnIG92ZXJyaWRlIG9ubHlcbiAqICAgIHJldGFpbnMgcmF3IHNvdXJjZSByb3dzLCBuZXZlciB3aWRlbnMgYSByZXBvcnQpLlxuICogYGluZ2VzdF9oZWFsdGhgIGFuZCBgZG9yYV9zZXR0aW5nc2AgYXJlIG5ldmVyIHB1cmdlZC4gRW52LW92ZXJyaWRhYmxlIGdsb2JhbHM7XG4gKiBhIHBlci1vcmcgb3ZlcnJpZGUgaW4gYGRvcmFfc2V0dGluZ3NgIHdpbnMgKHNlZSB7QGxpbmsgcmVzb2x2ZUV2ZW50UmV0ZW50aW9uRGF5c30pLlxuICovXG5cbi8qKiBEZXBsb3ltZW50IGZyZXF1ZW5jeSBieSBkZXBsb3lzL2RheTogZWxpdGUg4omlMS9kYXksIGhpZ2gg4omlMS93ZWVrLCBtZWRpdW0g4omlMS9tb250aC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkb3JhTGV2ZWxGb3JGcmVxdWVuY3kocGVyRGF5OiBudW1iZXIsIGRlcGxveW1lbnRzOiBudW1iZXIpOiBEb3JhTGV2ZWwge1xuICBpZiAoZGVwbG95bWVudHMgPD0gMCkgcmV0dXJuIG51bGw7XG4gIGlmIChwZXJEYXkgPj0gMSkgcmV0dXJuICdlbGl0ZSc7XG4gIGlmIChwZXJEYXkgPj0gMSAvIDcpIHJldHVybiAnaGlnaCc7XG4gIGlmIChwZXJEYXkgPj0gMSAvIDMwKSByZXR1cm4gJ21lZGl1bSc7XG4gIHJldHVybiAnbG93Jztcbn1cbi8qKiBDaGFuZ2UgZmFpbHVyZSByYXRlIGJ5IHBlcmNlbnQ6IGVsaXRlIOKJpDUlLCBoaWdoIOKJpDEwJSwgbWVkaXVtIOKJpDE1JS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkb3JhTGV2ZWxGb3JDaGFuZ2VGYWlsdXJlKHBjdDogbnVtYmVyLCB0b3RhbDogbnVtYmVyKTogRG9yYUxldmVsIHtcbiAgaWYgKHRvdGFsIDw9IDApIHJldHVybiBudWxsO1xuICBpZiAocGN0IDw9IDUpIHJldHVybiAnZWxpdGUnO1xuICBpZiAocGN0IDw9IDEwKSByZXR1cm4gJ2hpZ2gnO1xuICBpZiAocGN0IDw9IDE1KSByZXR1cm4gJ21lZGl1bSc7XG4gIHJldHVybiAnbG93Jztcbn1cbi8qKiBUaW1lIHRvIHJlc3RvcmUgYnkgc2Vjb25kczogZWxpdGUgPDFoLCBoaWdoIDwxIGRheSwgbWVkaXVtIDwxIHdlZWsuICovXG5leHBvcnQgZnVuY3Rpb24gZG9yYUxldmVsRm9yUmVzdG9yZShhdmdTZWNvbmRzOiBudW1iZXIgfCBudWxsKTogRG9yYUxldmVsIHtcbiAgaWYgKGF2Z1NlY29uZHMgPT0gbnVsbCkgcmV0dXJuIG51bGw7XG4gIGlmIChhdmdTZWNvbmRzIDwgMzYwMCkgcmV0dXJuICdlbGl0ZSc7XG4gIGlmIChhdmdTZWNvbmRzIDwgODY0MDApIHJldHVybiAnaGlnaCc7XG4gIGlmIChhdmdTZWNvbmRzIDwgNjA0ODAwKSByZXR1cm4gJ21lZGl1bSc7XG4gIHJldHVybiAnbG93Jztcbn1cbi8qKiBMZWFkIHRpbWUgYnkgc2Vjb25kczogZWxpdGUgPDEgZGF5LCBoaWdoIDwxIHdlZWssIG1lZGl1bSA8MSBtb250aC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkb3JhTGV2ZWxGb3JMZWFkVGltZShtZWRpYW5TZWNvbmRzOiBudW1iZXIgfCBudWxsKTogRG9yYUxldmVsIHtcbiAgaWYgKG1lZGlhblNlY29uZHMgPT0gbnVsbCkgcmV0dXJuIG51bGw7XG4gIGlmIChtZWRpYW5TZWNvbmRzIDwgODY0MDApIHJldHVybiAnZWxpdGUnO1xuICBpZiAobWVkaWFuU2Vjb25kcyA8IDYwNDgwMCkgcmV0dXJuICdoaWdoJztcbiAgaWYgKG1lZGlhblNlY29uZHMgPCAyNTkyMDAwKSByZXR1cm4gJ21lZGl1bSc7XG4gIHJldHVybiAnbG93Jztcbn1cblxuLyoqXG4gKiBSb3VuZCBgdmFsdWVgIHRvIGBkaWdpdHNgIGRlY2ltYWwgcGxhY2VzLCByZXR1cm5pbmcgYSByZWFsIG51bWJlci4gU21hbGxcbiAqIG1vZHVsZS1wcml2YXRlIGhlbHBlciBzbyB0aGUgRE9SQSBzaGFwaW5nIGRvZXNuJ3QgcmVwZWF0IHRoZVxuICogYE51bWJlcihOdW1iZXIoeCkudG9GaXhlZChuKSlgIHBhdHRlcm4gKHRvRml4ZWQgYWxvbmUgeWllbGRzIGEgc3RyaW5nKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJvdW5kKHZhbHVlOiBudW1iZXIsIGRpZ2l0czogbnVtYmVyKTogbnVtYmVyIHtcbiAgcmV0dXJuIE51bWJlcih2YWx1ZS50b0ZpeGVkKGRpZ2l0cykpO1xufVxuXG4vKipcbiAqIE1lZGlhbiBvZiBhIG51bWVyaWMgc2FtcGxlLCBvciBgbnVsbGAgd2hlbiBlbXB0eS4gVXNlZCBmb3IgbWVhc3VyZWQgbGVhZCB0aW1lXG4gKiBhbmQgTVRUUiDigJQgYm90aCBhcmUgY3Jvc3Mtc291cmNlIGdhcHMgY29tcHV0ZWQgaW4gSlMgZnJvbSBjbGFtcGVkIGRlbHRhcywgc29cbiAqIHRoZSBtZWRpYW4gbGl2ZXMgaGVyZSByYXRoZXIgdGhhbiBpbiBTUUwgUEVSQ0VOVElMRV9DT05UIChrZWVwcyB0aGUgZ29sZGVuXG4gKiBmaXh0dXJlIHRlc3RzIGFibGUgdG8gcHJvdmUgdGhlIG1ldHJpYyBtYXRoIHdpdGhvdXQgYSBsaXZlIFBvc3RncmVzKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG1lZGlhbih2YWx1ZXM6IG51bWJlcltdKTogbnVtYmVyIHwgbnVsbCB7XG4gIGlmICh2YWx1ZXMubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbDtcbiAgY29uc3QgcyA9IFsuLi52YWx1ZXNdLnNvcnQoKGEsIGIpID0+IGEgLSBiKTtcbiAgY29uc3QgbWlkID0gTWF0aC5mbG9vcihzLmxlbmd0aCAvIDIpO1xuICByZXR1cm4gcy5sZW5ndGggJSAyID09PSAxID8gc1ttaWRdIDogKHNbbWlkIC0gMV0gKyBzW21pZF0pIC8gMjtcbn1cblxuLyoqXG4gKiBSb2xsIGEgZ3JvdXAgb2YgZGVwbG95L3N0YWdlIGV2ZW50cyAoZ3JvdXBlZCBwZXIgZXhlY3V0aW9uKSB1cCB0byBvbmUgdGVybWluYWxcbiAqIHN0YXR1czogRkFJTEVEIHdpbnMsIHRoZW4gU1VDQ0VFREVELCBlbHNlIE9USEVSIChzdGlsbC1ydW5uaW5nIC8gbm9uLXRlcm1pbmFsKS5cbiAqIFNoYXJlZCBieSB0aGUgdGhyZWUgcGVyLShlbnZ8c3RhZ2UsIGV4ZWN1dGlvbikgcm9sbHVwIENURXMg4oCUIERPUkEgbWV0cmljcywgRE9SQVxuICogdHJlbmQsIGFuZCBidWlsZCBoZWFsdGgg4oCUIHNvIHRoZSBwcmVjZWRlbmNlIGNhbid0IGRyaWZ0IGJldHdlZW4gdGhlbS4gQXNzdW1lcyB0aGVcbiAqIGdyb3VwZWQgZXZlbnQgYWxpYXMgaXMgYGVgLlxuICovXG4iXX0=
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DoraOptions, DoraMetrics, DoraTrendPoint, BuildHealth } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 1.9 DORA metrics over a [from,to] deploy-completion window, org-scoped +
|
|
4
|
+
* rollup-aware. DEPLOY-BASIS ONLY — every metric derives from real deploy-stage
|
|
5
|
+
* executions (`event_type='STAGE'` with a non-null `environment`, set by the
|
|
6
|
+
* forwarder for the stages a user declared in `pb.deploys`). No run-based
|
|
7
|
+
* fallback: a pipeline with no deploy stage produces no DORA data.
|
|
8
|
+
*
|
|
9
|
+
* Four scoped SQL scans run inside one tx (each org-gated by `p.org_id ${pred}`
|
|
10
|
+
* / `o.org_id ${pred}` so a rollup passes the org→team subtree and a foreign
|
|
11
|
+
* org's rows never enter): (1) terminal deploy rows PER (execution, env) — one
|
|
12
|
+
* "deployment" = one execution reaching an env (D1) — → DF / deploy-time CFR /
|
|
13
|
+
* measured lead time (lead time joins the execution's earliest commit, D2);
|
|
14
|
+
* the scan also reaches back `windowHours` before `from` so incidents opened
|
|
15
|
+
* near the window start can correlate to a just-prior deploy; (2) `deployment_outcomes`
|
|
16
|
+
* in-window → post-deploy CFR component; (3) production restored/failed
|
|
17
|
+
* outcomes joined to their deploy → MTTR; (4) registry vs deploying → coverage.
|
|
18
|
+
* The cross-source medians (lead time, MTTR) are computed in JS from clamped
|
|
19
|
+
* (≥0) deltas.
|
|
20
|
+
*
|
|
21
|
+
* @param opts.pipelineId restrict to one pipeline (per-pipeline DORA)
|
|
22
|
+
* @param opts.environment restrict to one deploy environment
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDoraMetrics(orgId: string, from: string, to: string, orgIds?: string[], opts?: DoraOptions): Promise<DoraMetrics>;
|
|
25
|
+
/**
|
|
26
|
+
* 1.9b DORA trend — deployment frequency + deploy-time change-failure rate
|
|
27
|
+
* bucketed by `interval` (day/week/month) for a sparkline. Deploy-basis like
|
|
28
|
+
* getDoraMetrics: buckets terminal deploy-stage executions on their
|
|
29
|
+
* `completed_at`. Shares the org/rollup + pipelineId/environment scoping. MTTR,
|
|
30
|
+
* lead time, and post-deploy CFR are intentionally omitted (too heavy to bucket).
|
|
31
|
+
*/
|
|
32
|
+
export declare function getDoraTrend(orgId: string, interval: string, from: string, to: string, orgIds?: string[], opts?: DoraOptions): Promise<DoraTrendPoint[]>;
|
|
33
|
+
/**
|
|
34
|
+
* 1.10 Per-pipeline BUILD HEALTH (Phase 6) — a standard (NOT `advanced_reporting`)
|
|
35
|
+
* per-stage breakdown for one pipeline over a [from,to] window. Aggregates the
|
|
36
|
+
* existing `pipeline_events` STAGE rows: each stage is rolled up per execution
|
|
37
|
+
* to a terminal status (FAILED wins, then SUCCEEDED) + its max duration, then
|
|
38
|
+
* grouped per stage into run/success/failure counts, a success rate, and
|
|
39
|
+
* duration percentiles (p50/p90/p99). Totals sum across stages. Org-scoped via
|
|
40
|
+
* the pipeline join (`p.org_id ${pred}`) + rollup-aware like the sibling reports;
|
|
41
|
+
* a pipelineId owned by another org returns an empty breakdown.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getBuildHealth(orgId: string, pipelineId: string, from: string, to: string, orgIds?: string[]): Promise<BuildHealth>;
|
|
44
|
+
/** 2.1 Plugin summary — counts and breakdowns.
|
|
45
|
+
* INTENTIONALLY SINGLE-ORG (no rollup): plugin inventory is an org-owned
|
|
46
|
+
* asset count, not an execution/build activity report, so a parent's view is
|
|
47
|
+
* its own plugins — teams manage their own inventory. Single `= $org` scope. */
|