@mastra/factory 0.6.0-alpha.17 → 0.6.0-alpha.19
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/CHANGELOG.md +39 -0
- package/dist/storage/domains/work-items/metrics.d.ts +32 -41
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +58 -83
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +1 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @mastra/factory
|
|
2
2
|
|
|
3
|
+
## 0.6.0-alpha.19
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed the Factory review handoff turning finding references into GitHub links. A re-review that pointed back at "Blocking `#1`" published a link to issue 1 of the repository; findings are now named by subject and `file:line`. ([#21263](https://github.com/mastra-ai/mastra/pull/21263))
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`296dc9a`](https://github.com/mastra-ai/mastra/commit/296dc9af29f3616e786c7825ec32e0df92d754c5), [`1670533`](https://github.com/mastra-ai/mastra/commit/1670533986f6bacf567746245348125e3a106448), [`4a09a9c`](https://github.com/mastra-ai/mastra/commit/4a09a9c0474ef643558fcb5f0edc542b82f1cab0), [`1e83a47`](https://github.com/mastra-ai/mastra/commit/1e83a4734ab61ba5926af6793e3569a78b72ed37), [`ff28284`](https://github.com/mastra-ai/mastra/commit/ff2828416f14daff9d956e6a352fdaa23c950979), [`1670533`](https://github.com/mastra-ai/mastra/commit/1670533986f6bacf567746245348125e3a106448)]:
|
|
10
|
+
- @mastra/core@1.58.0-alpha.16
|
|
11
|
+
- @mastra/code-sdk@1.2.0-alpha.18
|
|
12
|
+
|
|
13
|
+
## 0.6.0-alpha.18
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Fixed the Factory metrics so the same date range always reports the same numbers, and dropped the response fields that nothing displayed. ([#21256](https://github.com/mastra-ai/mastra/pull/21256))
|
|
18
|
+
|
|
19
|
+
**Completions are events, not the board's current state.** Throughput and lead time now count entries into `done` in the stage history, so reopening a card no longer erases the day it shipped and a card that shipped twice counts twice. The per-day rate divides by the days the board actually existed, so a 12-month range on a two-week-old board no longer reads as ~0 per day.
|
|
20
|
+
|
|
21
|
+
**Automation numbers stop counting the wrong things.** A card landing on the board when it is created is no longer counted as an automated stage move, which used to credit every webhook-synced card. Automation coverage measures the first pass through each stage only — a redo used to add a second entry to the denominator alone, capping a fully automated stage at 50% — and each pass's outcome is now frozen at the end of the window instead of reflecting where the card sits today.
|
|
22
|
+
|
|
23
|
+
**Response shape.** `stageDurations`, `wip`, `agingWip` and `earliestItemAt` are gone: nothing rendered them, and live in-flight work is already covered by the queue-health chart. `windowDays` is now `daysCovered` (the window clipped to the board's life) and `cycleTime` is `leadTime`, which is what it always measured — card creation through to `done`.
|
|
24
|
+
|
|
25
|
+
The metrics endpoint (`GET /web/factory/projects/:id/metrics`) renames two fields:
|
|
26
|
+
|
|
27
|
+
```jsonc
|
|
28
|
+
// before
|
|
29
|
+
{ "metrics": { "windowDays": 30, "cycleTime": { "medianMs": 7200000 } } }
|
|
30
|
+
// after
|
|
31
|
+
{ "metrics": { "daysCovered": 30, "leadTime": { "medianMs": 7200000 } } }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A corrupt stage-history timestamp now throws instead of being read as 1970.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [[`dc4a25d`](https://github.com/mastra-ai/mastra/commit/dc4a25d41af4e2fe97a816070eaec6aa963ab53b), [`dc4a25d`](https://github.com/mastra-ai/mastra/commit/dc4a25d41af4e2fe97a816070eaec6aa963ab53b)]:
|
|
39
|
+
- @mastra/core@1.58.0-alpha.15
|
|
40
|
+
- @mastra/code-sdk@1.2.0-alpha.17
|
|
41
|
+
|
|
3
42
|
## 0.6.0-alpha.17
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Aggregation math for the Factory
|
|
2
|
+
* Aggregation math for the Factory Overview page.
|
|
3
3
|
*
|
|
4
|
-
* Pure functions over `work_items` rows —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Pure functions over `work_items` rows — throughput, lead time, in-flight
|
|
5
|
+
* count, demand mix and per-stage automation, all read from the server-appended
|
|
6
|
+
* `stageHistory` log. Keeping this DB-free makes the math unit testable and lets
|
|
7
|
+
* the route stay a thin shell.
|
|
8
|
+
*
|
|
9
|
+
* Everything windowed is counted as an event that happened inside the window,
|
|
10
|
+
* never as "the state the board happens to be in now", so re-querying a past
|
|
11
|
+
* window always returns the same numbers.
|
|
8
12
|
*/
|
|
9
13
|
import type { WorkItemRow } from './base.js';
|
|
10
14
|
/** Default window span (days) when the request omits or malforms the range. */
|
|
@@ -12,68 +16,55 @@ export declare const DEFAULT_METRICS_WINDOW = 30;
|
|
|
12
16
|
/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */
|
|
13
17
|
export declare const MAX_METRICS_WINDOW = 366;
|
|
14
18
|
export interface FactoryMetrics {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Days the series covers: the requested window clipped to the board's life.
|
|
21
|
+
* Days before the first card could hold no completion, so counting them would
|
|
22
|
+
* drag the per-day rate toward zero on a young board.
|
|
23
|
+
*/
|
|
24
|
+
daysCovered: number;
|
|
25
|
+
/** Entries into `done` per UTC day, gap-filled across the covered days. */
|
|
20
26
|
throughput: {
|
|
21
27
|
date: string;
|
|
22
28
|
count: number;
|
|
23
29
|
}[];
|
|
24
|
-
/** Card creation → `done`
|
|
25
|
-
|
|
30
|
+
/** Card creation → `done` for every completion that landed in the window. */
|
|
31
|
+
leadTime: {
|
|
26
32
|
medianMs: number | null;
|
|
27
33
|
p90Ms: number | null;
|
|
28
34
|
samples: number;
|
|
29
35
|
};
|
|
30
|
-
/** Median time spent per stage, over visits that ended inside the window. */
|
|
31
|
-
stageDurations: {
|
|
32
|
-
stage: string;
|
|
33
|
-
medianMs: number;
|
|
34
|
-
samples: number;
|
|
35
|
-
}[];
|
|
36
|
-
/** Current cards per stage (window-independent). */
|
|
37
|
-
wip: {
|
|
38
|
-
stage: string;
|
|
39
|
-
count: number;
|
|
40
|
-
}[];
|
|
41
36
|
/** Distinct in-flight cards (at least one non-terminal stage). */
|
|
42
37
|
wipTotal: number;
|
|
43
|
-
/** Oldest in-flight cards by time in their current stage. */
|
|
44
|
-
agingWip: {
|
|
45
|
-
id: string;
|
|
46
|
-
title: string;
|
|
47
|
-
stage: string;
|
|
48
|
-
enteredAt: string;
|
|
49
|
-
url: string | null;
|
|
50
|
-
}[];
|
|
51
38
|
/** Cards created in the window, by source. */
|
|
52
39
|
sourceMix: {
|
|
53
40
|
source: string;
|
|
54
41
|
count: number;
|
|
55
42
|
}[];
|
|
56
|
-
/** Stage moves in the window: human-performed vs total. */
|
|
43
|
+
/** Stage moves in the window, card creation excluded: human-performed vs total. */
|
|
57
44
|
transitions: {
|
|
58
45
|
human: number;
|
|
59
46
|
total: number;
|
|
60
47
|
};
|
|
61
|
-
/** Per-stage automation over
|
|
48
|
+
/** Per-stage automation over first visits that ended in the window. */
|
|
62
49
|
stageAutomation: {
|
|
63
50
|
stage: string;
|
|
64
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* First visits to this stage that ended in the window. Repeat visits are
|
|
53
|
+
* excluded from both sides: they are rework, already reported as such, and
|
|
54
|
+
* counting them in the denominator alone caps a fully automated stage below
|
|
55
|
+
* 100%.
|
|
56
|
+
*/
|
|
65
57
|
exits: number;
|
|
66
58
|
/**
|
|
67
|
-
* Of those:
|
|
68
|
-
*
|
|
69
|
-
* written before exit stamping) counts as human.
|
|
59
|
+
* Of those: passes entered *and* exited by an automation actor. Missing
|
|
60
|
+
* `exitedBy` (entries written before exit stamping) counts as human.
|
|
70
61
|
*/
|
|
71
62
|
automated: number;
|
|
72
63
|
/**
|
|
73
|
-
* Outcomes of the automated passes' items
|
|
74
|
-
* wins: `reworked` (a later visit to the same stage
|
|
75
|
-
* outranks `done`:
|
|
76
|
-
* failure even if the item eventually merged), then `done`, then
|
|
64
|
+
* Outcomes of the automated passes' items as of the window's end, mutually
|
|
65
|
+
* exclusive, first match wins: `reworked` (a later visit to the same stage
|
|
66
|
+
* — deliberately outranks `done`: a pass that needed a redo is an
|
|
67
|
+
* automation failure even if the item eventually merged), then `done`, then
|
|
77
68
|
* `canceled`, then `inFlight`.
|
|
78
69
|
*/
|
|
79
70
|
outcomes: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/work-items/metrics.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/work-items/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAiBtC,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9C,6EAA6E;IAC7E,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/C,mFAAmF;IACnF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,uEAAuE;IACvE,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd;;;;;WAKG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;WAGG;QACH,SAAS,EAAE,MAAM,CAAC;QAClB;;;;;;WAMG;QACH,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KAClF,EAAE,CAAC;CACL;AAqBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,IAAI,GACR;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAY5C;AA2CD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,WAAW,EAAE,EACpB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/C,cAAc,CAiHhB"}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { isAutomationActor } from "./base.js";
|
|
2
2
|
//#region src/storage/domains/work-items/metrics.ts
|
|
3
3
|
/**
|
|
4
|
-
* Aggregation math for the Factory
|
|
4
|
+
* Aggregation math for the Factory Overview page.
|
|
5
5
|
*
|
|
6
|
-
* Pure functions over `work_items` rows —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Pure functions over `work_items` rows — throughput, lead time, in-flight
|
|
7
|
+
* count, demand mix and per-stage automation, all read from the server-appended
|
|
8
|
+
* `stageHistory` log. Keeping this DB-free makes the math unit testable and lets
|
|
9
|
+
* the route stay a thin shell.
|
|
10
|
+
*
|
|
11
|
+
* Everything windowed is counted as an event that happened inside the window,
|
|
12
|
+
* never as "the state the board happens to be in now", so re-querying a past
|
|
13
|
+
* window always returns the same numbers.
|
|
10
14
|
*/
|
|
11
15
|
/** Default window span (days) when the request omits or malforms the range. */
|
|
12
16
|
const DEFAULT_METRICS_WINDOW = 30;
|
|
@@ -19,11 +23,10 @@ const DONE_STAGE = "done";
|
|
|
19
23
|
const CANCELED_STAGE = "canceled";
|
|
20
24
|
/**
|
|
21
25
|
* Terminal stages — items holding only these are not in-flight. `done` is a
|
|
22
|
-
* completion (feeds throughput/
|
|
26
|
+
* completion (feeds throughput/lead time); `canceled` is a tracked
|
|
23
27
|
* non-completion outcome and feeds neither.
|
|
24
28
|
*/
|
|
25
29
|
const TERMINAL_STAGES = /* @__PURE__ */ new Set([DONE_STAGE, CANCELED_STAGE]);
|
|
26
|
-
const AGING_WIP_LIMIT = 10;
|
|
27
30
|
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
28
31
|
/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */
|
|
29
32
|
const ZONED_DATETIME_RE = /(?:[Zz]|[+-]\d{2}:?\d{2})$/;
|
|
@@ -59,9 +62,18 @@ function parseMetricsRange(fromParam, toParam, now) {
|
|
|
59
62
|
windowEnd
|
|
60
63
|
};
|
|
61
64
|
}
|
|
65
|
+
/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */
|
|
62
66
|
function parseTime(iso) {
|
|
63
67
|
const time = Date.parse(iso);
|
|
64
|
-
|
|
68
|
+
if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);
|
|
69
|
+
return time;
|
|
70
|
+
}
|
|
71
|
+
/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */
|
|
72
|
+
function assertParsableHistory(items) {
|
|
73
|
+
for (const item of items) for (const entry of item.stageHistory) {
|
|
74
|
+
parseTime(entry.enteredAt);
|
|
75
|
+
if (entry.exitedAt !== void 0) parseTime(entry.exitedAt);
|
|
76
|
+
}
|
|
65
77
|
}
|
|
66
78
|
/** Nearest-rank percentile over an unsorted sample list. */
|
|
67
79
|
function percentile(samples, fraction) {
|
|
@@ -73,75 +85,47 @@ function percentile(samples, fraction) {
|
|
|
73
85
|
function utcDay(time) {
|
|
74
86
|
return new Date(time).toISOString().slice(0, 10);
|
|
75
87
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (let i = item.stageHistory.length - 1; i >= 0; i--) {
|
|
84
|
-
const entry = item.stageHistory[i];
|
|
85
|
-
if (entry.stage === DONE_STAGE && entry.exitedAt === void 0) return parseTime(entry.enteredAt);
|
|
88
|
+
/** Stages the item was holding at `time`, replayed from its history. */
|
|
89
|
+
function stagesHeldAt(item, time) {
|
|
90
|
+
const held = /* @__PURE__ */ new Set();
|
|
91
|
+
for (const entry of item.stageHistory) {
|
|
92
|
+
if (parseTime(entry.enteredAt) >= time) continue;
|
|
93
|
+
if (entry.exitedAt !== void 0 && parseTime(entry.exitedAt) <= time) continue;
|
|
94
|
+
held.add(entry.stage);
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
/** Open (no `exitedAt`) history entry for a currently-held non-terminal stage. */
|
|
89
|
-
function openEntries(item) {
|
|
90
|
-
return item.stageHistory.filter((entry) => entry.exitedAt === void 0 && !TERMINAL_STAGES.has(entry.stage) && item.stages.includes(entry.stage));
|
|
96
|
+
return held;
|
|
91
97
|
}
|
|
92
98
|
function computeFactoryMetrics(items, opts) {
|
|
93
99
|
const { windowStart, windowEnd } = opts;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
100
|
+
assertParsableHistory(items);
|
|
101
|
+
let earliestItem = Infinity;
|
|
102
|
+
for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());
|
|
103
|
+
const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;
|
|
104
|
+
const firstDay = Math.max(utcDayStart(windowStart), boardStart);
|
|
97
105
|
const throughputByDay = /* @__PURE__ */ new Map();
|
|
98
|
-
const firstDay = utcDayStart(windowStart);
|
|
99
106
|
for (let day = firstDay; day < windowEnd; day += DAY_MS) throughputByDay.set(utcDay(day), 0);
|
|
100
|
-
const
|
|
101
|
-
for (const item of items) {
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
const leadSamples = [];
|
|
108
|
+
for (const item of items) for (const entry of item.stageHistory) {
|
|
109
|
+
if (entry.stage !== DONE_STAGE) continue;
|
|
110
|
+
const doneAt = parseTime(entry.enteredAt);
|
|
111
|
+
if (doneAt < windowStart || doneAt >= windowEnd) continue;
|
|
104
112
|
const day = utcDay(doneAt);
|
|
105
113
|
throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
const durationsByStage = /* @__PURE__ */ new Map();
|
|
109
|
-
for (const item of items) for (const entry of item.stageHistory) {
|
|
110
|
-
if (entry.exitedAt === void 0 || TERMINAL_STAGES.has(entry.stage)) continue;
|
|
111
|
-
const exited = parseTime(entry.exitedAt);
|
|
112
|
-
if (exited < windowStart || exited >= windowEnd) continue;
|
|
113
|
-
const duration = Math.max(0, exited - parseTime(entry.enteredAt));
|
|
114
|
-
const samples = durationsByStage.get(entry.stage) ?? [];
|
|
115
|
-
samples.push(duration);
|
|
116
|
-
durationsByStage.set(entry.stage, samples);
|
|
114
|
+
leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));
|
|
117
115
|
}
|
|
118
|
-
const wipByStage = /* @__PURE__ */ new Map();
|
|
119
116
|
let wipTotal = 0;
|
|
120
|
-
const
|
|
121
|
-
for (const item of items) {
|
|
122
|
-
for (const stage of item.stages) wipByStage.set(stage, (wipByStage.get(stage) ?? 0) + 1);
|
|
123
|
-
const inFlightStages = item.stages.filter((stage) => !TERMINAL_STAGES.has(stage));
|
|
124
|
-
if (inFlightStages.length === 0) continue;
|
|
125
|
-
wipTotal += 1;
|
|
126
|
-
const oldest = openEntries(item).reduce((best, entry) => !best || parseTime(entry.enteredAt) < parseTime(best.enteredAt) ? entry : best, void 0);
|
|
127
|
-
aging.push({
|
|
128
|
-
id: item.id,
|
|
129
|
-
title: item.title,
|
|
130
|
-
stage: oldest?.stage ?? inFlightStages[0],
|
|
131
|
-
enteredAt: oldest?.enteredAt ?? item.createdAt.toISOString(),
|
|
132
|
-
url: item.externalSource?.url ?? null
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
aging.sort((a, b) => parseTime(a.enteredAt) - parseTime(b.enteredAt));
|
|
117
|
+
for (const item of items) if (item.stages.some((stage) => !TERMINAL_STAGES.has(stage))) wipTotal += 1;
|
|
136
118
|
const sourceCounts = /* @__PURE__ */ new Map();
|
|
137
119
|
let transitionsTotal = 0;
|
|
138
120
|
let transitionsHuman = 0;
|
|
139
121
|
for (const item of items) {
|
|
140
|
-
|
|
122
|
+
const created = item.createdAt.getTime();
|
|
123
|
+
if (created >= windowStart && created < windowEnd) {
|
|
141
124
|
const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : "manual";
|
|
142
125
|
sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);
|
|
143
126
|
}
|
|
144
|
-
for (
|
|
127
|
+
for (let i = 1; i < item.stageHistory.length; i++) {
|
|
128
|
+
const entry = item.stageHistory[i];
|
|
145
129
|
const entered = parseTime(entry.enteredAt);
|
|
146
130
|
if (entered < windowStart || entered >= windowEnd) continue;
|
|
147
131
|
transitionsTotal += 1;
|
|
@@ -150,11 +134,13 @@ function computeFactoryMetrics(items, opts) {
|
|
|
150
134
|
}
|
|
151
135
|
const automationByStage = /* @__PURE__ */ new Map();
|
|
152
136
|
for (const item of items) {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
137
|
+
const heldAtWindowEnd = stagesHeldAt(item, windowEnd);
|
|
138
|
+
const visited = /* @__PURE__ */ new Set();
|
|
155
139
|
for (let i = 0; i < item.stageHistory.length; i++) {
|
|
156
140
|
const entry = item.stageHistory[i];
|
|
157
|
-
if (entry.
|
|
141
|
+
if (TERMINAL_STAGES.has(entry.stage) || visited.has(entry.stage)) continue;
|
|
142
|
+
visited.add(entry.stage);
|
|
143
|
+
if (entry.exitedAt === void 0) continue;
|
|
158
144
|
const exited = parseTime(entry.exitedAt);
|
|
159
145
|
if (exited < windowStart || exited >= windowEnd) continue;
|
|
160
146
|
let row = automationByStage.get(entry.stage);
|
|
@@ -173,37 +159,26 @@ function computeFactoryMetrics(items, opts) {
|
|
|
173
159
|
automationByStage.set(entry.stage, row);
|
|
174
160
|
}
|
|
175
161
|
row.exits += 1;
|
|
176
|
-
if (
|
|
162
|
+
if (!isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;
|
|
177
163
|
row.automated += 1;
|
|
178
|
-
if (item.stageHistory.some((
|
|
179
|
-
else if (
|
|
180
|
-
else if (
|
|
164
|
+
if (item.stageHistory.some((later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd)) row.outcomes.reworked += 1;
|
|
165
|
+
else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;
|
|
166
|
+
else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;
|
|
181
167
|
else row.outcomes.inFlight += 1;
|
|
182
168
|
}
|
|
183
169
|
}
|
|
184
170
|
return {
|
|
185
|
-
|
|
186
|
-
earliestItemAt,
|
|
171
|
+
daysCovered: throughputByDay.size,
|
|
187
172
|
throughput: [...throughputByDay.entries()].map(([date, count]) => ({
|
|
188
173
|
date,
|
|
189
174
|
count
|
|
190
175
|
})).sort((a, b) => a.date.localeCompare(b.date)),
|
|
191
|
-
|
|
192
|
-
medianMs: percentile(
|
|
193
|
-
p90Ms: percentile(
|
|
194
|
-
samples:
|
|
176
|
+
leadTime: {
|
|
177
|
+
medianMs: percentile(leadSamples, .5),
|
|
178
|
+
p90Ms: percentile(leadSamples, .9),
|
|
179
|
+
samples: leadSamples.length
|
|
195
180
|
},
|
|
196
|
-
stageDurations: [...durationsByStage.entries()].map(([stage, samples]) => ({
|
|
197
|
-
stage,
|
|
198
|
-
medianMs: percentile(samples, .5),
|
|
199
|
-
samples: samples.length
|
|
200
|
-
})),
|
|
201
|
-
wip: [...wipByStage.entries()].map(([stage, count]) => ({
|
|
202
|
-
stage,
|
|
203
|
-
count
|
|
204
|
-
})),
|
|
205
181
|
wipTotal,
|
|
206
|
-
agingWip: aging.slice(0, AGING_WIP_LIMIT),
|
|
207
182
|
sourceMix: [...sourceCounts.entries()].map(([source, count]) => ({
|
|
208
183
|
source,
|
|
209
184
|
count
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Metrics page.\n *\n * Pure functions over `work_items` rows — flow metrics (throughput, cycle\n * time, stage durations, WIP, aging WIP) plus demand mix, all derived from the\n * server-appended `stageHistory` log. Keeping this DB-free makes the math unit\n * testable and lets the route stay a thin shell.\n */\n\nimport { isAutomationActor } from './base.js';\nimport type { WorkItemRow, WorkItemStageEntry } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/cycle time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nconst AGING_WIP_LIMIT = 10;\n\nexport interface FactoryMetrics {\n windowDays: number;\n /** Earliest work-item creation time (ISO, window-independent) — the natural\n * lower bound for a date-range control. `null` when the board is empty. */\n earliestItemAt: string | null;\n /** Items reaching `done` per UTC day, gap-filled across the window. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` duration for items completed in the window. */\n cycleTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Median time spent per stage, over visits that ended inside the window. */\n stageDurations: { stage: string; medianMs: number; samples: number }[];\n /** Current cards per stage (window-independent). */\n wip: { stage: string; count: number }[];\n /** Distinct in-flight cards (at least one non-terminal stage). */\n wipTotal: number;\n /** Oldest in-flight cards by time in their current stage. */\n agingWip: { id: string; title: string; stage: string; enteredAt: string; url: string | null }[];\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Stage moves in the window: human-performed vs total. */\n transitions: { human: number; total: number };\n /** Per-stage automation over completed visits that exited in the window. */\n stageAutomation: {\n stage: string;\n /** Completed visits (entered+exited) to this stage that exited in the window. */\n exits: number;\n /**\n * Of those: clean automated passes — the item's first visit to the stage,\n * entered *and* exited by an automation actor. Missing `exitedBy` (entries\n * written before exit stamping) counts as human.\n */\n automated: number;\n /**\n * Outcomes of the automated passes' items, mutually exclusive, first match\n * wins: `reworked` (a later visit to the same stage exists — deliberately\n * outranks `done`: an automated pass that needed a redo is an automation\n * failure even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n return Number.isNaN(time) ? 0 : time;\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/**\n * The item's completion time: the `enteredAt` of its still-open `done` entry.\n * `undefined` when the item isn't currently done (including when it was pulled\n * back out of done — that visit has `exitedAt` and doesn't count).\n */\nfunction completedAt(item: WorkItemRow): number | undefined {\n if (!item.stages.includes(DONE_STAGE)) return undefined;\n for (let i = item.stageHistory.length - 1; i >= 0; i--) {\n const entry = item.stageHistory[i]!;\n if (entry.stage === DONE_STAGE && entry.exitedAt === undefined) return parseTime(entry.enteredAt);\n }\n return undefined;\n}\n\n/** Open (no `exitedAt`) history entry for a currently-held non-terminal stage. */\nfunction openEntries(item: WorkItemRow): WorkItemStageEntry[] {\n return item.stageHistory.filter(\n entry => entry.exitedAt === undefined && !TERMINAL_STAGES.has(entry.stage) && item.stages.includes(entry.stage),\n );\n}\n\nexport function computeFactoryMetrics(\n items: WorkItemRow[],\n opts: { windowStart: number; windowEnd: number },\n): FactoryMetrics {\n const { windowStart, windowEnd } = opts;\n\n // Earliest creation across all items (window-independent) — the range lower bound.\n let earliest = Infinity;\n for (const item of items) earliest = Math.min(earliest, item.createdAt.getTime());\n const earliestItemAt = Number.isFinite(earliest) ? new Date(earliest).toISOString() : null;\n\n // ── Throughput + cycle time (completed in window) ─────────────────────────\n // Gap-fill every UTC calendar date intersecting the half-open window.\n const throughputByDay = new Map<string, number>();\n const firstDay = utcDayStart(windowStart);\n for (let day = firstDay; day < windowEnd; day += DAY_MS) {\n throughputByDay.set(utcDay(day), 0);\n }\n const cycleSamples: number[] = [];\n for (const item of items) {\n const doneAt = completedAt(item);\n if (doneAt === undefined || doneAt < windowStart || doneAt >= windowEnd) continue;\n const day = utcDay(doneAt);\n throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);\n cycleSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n\n // ── Stage durations (visits that ended in window) ─────────────────────────\n const durationsByStage = new Map<string, number[]>();\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.exitedAt === undefined || TERMINAL_STAGES.has(entry.stage)) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n const duration = Math.max(0, exited - parseTime(entry.enteredAt));\n const samples = durationsByStage.get(entry.stage) ?? [];\n samples.push(duration);\n durationsByStage.set(entry.stage, samples);\n }\n }\n\n // ── Current WIP + aging (window-independent) ──────────────────────────────\n const wipByStage = new Map<string, number>();\n let wipTotal = 0;\n const aging: FactoryMetrics['agingWip'] = [];\n for (const item of items) {\n for (const stage of item.stages) {\n wipByStage.set(stage, (wipByStage.get(stage) ?? 0) + 1);\n }\n const inFlightStages = item.stages.filter(stage => !TERMINAL_STAGES.has(stage));\n if (inFlightStages.length === 0) continue;\n wipTotal += 1;\n // Age the card by its longest-held current stage; fall back to creation\n // time if history is missing an open entry (shouldn't happen — history is\n // server-appended).\n const open = openEntries(item);\n const oldest = open.reduce<WorkItemStageEntry | undefined>(\n (best, entry) => (!best || parseTime(entry.enteredAt) < parseTime(best.enteredAt) ? entry : best),\n undefined,\n );\n aging.push({\n id: item.id,\n title: item.title,\n stage: oldest?.stage ?? inFlightStages[0]!,\n enteredAt: oldest?.enteredAt ?? item.createdAt.toISOString(),\n url: item.externalSource?.url ?? null,\n });\n }\n aging.sort((a, b) => parseTime(a.enteredAt) - parseTime(b.enteredAt));\n\n // ── Demand mix + transitions (window) ─────────────────────────────────────\n const sourceCounts = new Map<string, number>();\n let transitionsTotal = 0;\n let transitionsHuman = 0;\n for (const item of items) {\n if (item.createdAt.getTime() >= windowStart && item.createdAt.getTime() < windowEnd) {\n const source = item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}`\n : 'manual';\n sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);\n }\n for (const entry of item.stageHistory) {\n const entered = parseTime(entry.enteredAt);\n if (entered < windowStart || entered >= windowEnd) continue;\n transitionsTotal += 1;\n if (!isAutomationActor(entry.by)) transitionsHuman += 1;\n }\n }\n\n // ── Per-stage automation (completed visits that exited in window) ─────────\n // Rows appear in insertion order of each stage's first counted exit;\n // terminal stages never get rows (they have no meaningful \"pass through\").\n const automationByStage = new Map<string, FactoryMetrics['stageAutomation'][number]>();\n for (const item of items) {\n const itemDone = completedAt(item) !== undefined;\n const itemCanceled = item.stages.includes(CANCELED_STAGE);\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (entry.exitedAt === undefined || TERMINAL_STAGES.has(entry.stage)) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n let row = automationByStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n exits: 0,\n automated: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n automationByStage.set(entry.stage, row);\n }\n row.exits += 1;\n // A clean automated pass: automation entered AND exited it, and this is\n // the item's first visit to the stage (a re-run is rework, not clean\n // automation). Missing `exitedBy` → human-exited → not automated.\n const firstVisitIndex = item.stageHistory.findIndex(e => e.stage === entry.stage);\n if (firstVisitIndex !== i || !isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;\n row.automated += 1;\n const reworked = item.stageHistory.some((e, j) => j > i && e.stage === entry.stage);\n if (reworked) row.outcomes.reworked += 1;\n else if (itemDone) row.outcomes.done += 1;\n else if (itemCanceled) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n\n return {\n windowDays: throughputByDay.size,\n earliestItemAt,\n throughput: [...throughputByDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n cycleTime: {\n medianMs: percentile(cycleSamples, 0.5),\n p90Ms: percentile(cycleSamples, 0.9),\n samples: cycleSamples.length,\n },\n stageDurations: [...durationsByStage.entries()].map(([stage, samples]) => ({\n stage,\n medianMs: percentile(samples, 0.5)!,\n samples: samples.length,\n })),\n wip: [...wipByStage.entries()].map(([stage, count]) => ({ stage, count })),\n wipTotal,\n agingWip: aging.slice(0, AGING_WIP_LIMIT),\n sourceMix: [...sourceCounts.entries()]\n .map(([source, count]) => ({ source, count }))\n .sort((a, b) => b.count - a.count),\n transitions: { human: transitionsHuman, total: transitionsTotal },\n stageAutomation: [...automationByStage.values()],\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Overview page.\n *\n * Pure functions over `work_items` rows — throughput, lead time, in-flight\n * count, demand mix and per-stage automation, all read from the server-appended\n * `stageHistory` log. Keeping this DB-free makes the math unit testable and lets\n * the route stay a thin shell.\n *\n * Everything windowed is counted as an event that happened inside the window,\n * never as \"the state the board happens to be in now\", so re-querying a past\n * window always returns the same numbers.\n */\n\nimport { isAutomationActor } from './base.js';\nimport type { WorkItemRow } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/lead time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nexport interface FactoryMetrics {\n /**\n * Days the series covers: the requested window clipped to the board's life.\n * Days before the first card could hold no completion, so counting them would\n * drag the per-day rate toward zero on a young board.\n */\n daysCovered: number;\n /** Entries into `done` per UTC day, gap-filled across the covered days. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` for every completion that landed in the window. */\n leadTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Distinct in-flight cards (at least one non-terminal stage). */\n wipTotal: number;\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Stage moves in the window, card creation excluded: human-performed vs total. */\n transitions: { human: number; total: number };\n /** Per-stage automation over first visits that ended in the window. */\n stageAutomation: {\n stage: string;\n /**\n * First visits to this stage that ended in the window. Repeat visits are\n * excluded from both sides: they are rework, already reported as such, and\n * counting them in the denominator alone caps a fully automated stage below\n * 100%.\n */\n exits: number;\n /**\n * Of those: passes entered *and* exited by an automation actor. Missing\n * `exitedBy` (entries written before exit stamping) counts as human.\n */\n automated: number;\n /**\n * Outcomes of the automated passes' items as of the window's end, mutually\n * exclusive, first match wins: `reworked` (a later visit to the same stage\n * — deliberately outranks `done`: a pass that needed a redo is an\n * automation failure even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\n/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);\n return time;\n}\n\n/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */\nfunction assertParsableHistory(items: WorkItemRow[]): void {\n for (const item of items) {\n for (const entry of item.stageHistory) {\n parseTime(entry.enteredAt);\n if (entry.exitedAt !== undefined) parseTime(entry.exitedAt);\n }\n }\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/** Stages the item was holding at `time`, replayed from its history. */\nfunction stagesHeldAt(item: WorkItemRow, time: number): Set<string> {\n const held = new Set<string>();\n for (const entry of item.stageHistory) {\n if (parseTime(entry.enteredAt) >= time) continue;\n if (entry.exitedAt !== undefined && parseTime(entry.exitedAt) <= time) continue;\n held.add(entry.stage);\n }\n return held;\n}\n\nexport function computeFactoryMetrics(\n items: WorkItemRow[],\n opts: { windowStart: number; windowEnd: number },\n): FactoryMetrics {\n const { windowStart, windowEnd } = opts;\n assertParsableHistory(items);\n\n // ── Throughput + lead time (completions in window) ────────────────────────\n let earliestItem = Infinity;\n for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());\n const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;\n const firstDay = Math.max(utcDayStart(windowStart), boardStart);\n\n const throughputByDay = new Map<string, number>();\n for (let day = firstDay; day < windowEnd; day += DAY_MS) {\n throughputByDay.set(utcDay(day), 0);\n }\n // A completion is an entry *into* `done`, not the state of the card now: a\n // card reopened today must not erase the day it shipped, and a card that\n // shipped twice shipped twice.\n const leadSamples: number[] = [];\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.stage !== DONE_STAGE) continue;\n const doneAt = parseTime(entry.enteredAt);\n if (doneAt < windowStart || doneAt >= windowEnd) continue;\n const day = utcDay(doneAt);\n throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);\n leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n }\n\n // ── Current in-flight count (window-independent) ──────────────────────────\n let wipTotal = 0;\n for (const item of items) {\n if (item.stages.some(stage => !TERMINAL_STAGES.has(stage))) wipTotal += 1;\n }\n\n // ── Demand mix + transitions (window) ─────────────────────────────────────\n const sourceCounts = new Map<string, number>();\n let transitionsTotal = 0;\n let transitionsHuman = 0;\n for (const item of items) {\n const created = item.createdAt.getTime();\n if (created >= windowStart && created < windowEnd) {\n const source = item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}`\n : 'manual';\n sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);\n }\n // The first entry is where the card landed on creation, not a move —\n // counting it credits every webhook-synced card as an automated transition.\n for (let i = 1; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n const entered = parseTime(entry.enteredAt);\n if (entered < windowStart || entered >= windowEnd) continue;\n transitionsTotal += 1;\n if (!isAutomationActor(entry.by)) transitionsHuman += 1;\n }\n }\n\n // ── Per-stage automation (first visits that exited in window) ─────────────\n // Rows appear in insertion order of each stage's first counted exit;\n // terminal stages never get rows (they have no meaningful \"pass through\").\n const automationByStage = new Map<string, FactoryMetrics['stageAutomation'][number]>();\n for (const item of items) {\n const heldAtWindowEnd = stagesHeldAt(item, windowEnd);\n const visited = new Set<string>();\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (TERMINAL_STAGES.has(entry.stage) || visited.has(entry.stage)) continue;\n visited.add(entry.stage);\n if (entry.exitedAt === undefined) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n let row = automationByStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n exits: 0,\n automated: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n automationByStage.set(entry.stage, row);\n }\n row.exits += 1;\n // Missing `exitedBy` → human-exited → not an automated pass.\n if (!isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;\n row.automated += 1;\n const reworked = item.stageHistory.some(\n (later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd,\n );\n if (reworked) row.outcomes.reworked += 1;\n else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;\n else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n\n return {\n daysCovered: throughputByDay.size,\n throughput: [...throughputByDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n leadTime: {\n medianMs: percentile(leadSamples, 0.5),\n p90Ms: percentile(leadSamples, 0.9),\n samples: leadSamples.length,\n },\n wipTotal,\n sourceMix: [...sourceCounts.entries()]\n .map(([source, count]) => ({ source, count }))\n .sort((a, b) => b.count - a.count),\n transitions: { human: transitionsHuman, total: transitionsTotal },\n stageAutomation: [...automationByStage.values()],\n };\n}\n"],"mappings":"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"}
|
|
@@ -117,7 +117,7 @@ First, compose the **review handoff** — don't send it to the conversation yet;
|
|
|
117
117
|
|
|
118
118
|
- **Findings** — correctness assessment, test assessment, scope assessment, pattern-consistency notes, each grounded in the history you traced. Distill — this is a handoff, not a transcript.
|
|
119
119
|
- **Verification** — every command you executed (tests, typecheck, repros) with its outcome, or an explicit statement that nothing was executed and why.
|
|
120
|
-
- **Existing review disposition** — every substantive finding from prior reviewers (bots included) with its classification: confirmed, addressed, or refuted with evidence. A major bot comment must never be silently dropped.
|
|
120
|
+
- **Existing review disposition** — every substantive finding from prior reviewers (bots included, your own earlier passes included) with its classification: confirmed, addressed, or refuted with evidence. A major bot comment must never be silently dropped. Name each by subject and `file:line`, and remember the body lands as GitHub markdown — `#1` publishes as a link to issue 1.
|
|
121
121
|
- **Adversarial check** (approve only) — the one-line record of why the strongest request-changes case fails.
|
|
122
122
|
- **Requested changes** — one entry per change, concrete enough to act on (for a request-changes verdict).
|
|
123
123
|
- **Assumptions** — every recorded judgment call from the run.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/factory",
|
|
3
|
-
"version": "0.6.0-alpha.
|
|
3
|
+
"version": "0.6.0-alpha.19",
|
|
4
4
|
"description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"zod": "^4.3.6",
|
|
54
54
|
"@mastra/auth-studio": "1.3.3",
|
|
55
55
|
"@mastra/auth-workos": "1.6.4",
|
|
56
|
-
"@mastra/code-sdk": "1.2.0-alpha.
|
|
57
|
-
"@mastra/core": "1.58.0-alpha.
|
|
56
|
+
"@mastra/code-sdk": "1.2.0-alpha.18",
|
|
57
|
+
"@mastra/core": "1.58.0-alpha.16",
|
|
58
58
|
"@mastra/slack": "1.6.1-alpha.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"typescript": "^6.0.3",
|
|
65
65
|
"typescript-eslint": "^8.57.0",
|
|
66
66
|
"vitest": "4.1.10",
|
|
67
|
-
"@mastra/libsql": "1.20.0-alpha.
|
|
68
|
-
"@mastra/pg": "1.20.0-alpha.
|
|
69
|
-
"@internal/
|
|
70
|
-
"@internal/
|
|
67
|
+
"@mastra/libsql": "1.20.0-alpha.3",
|
|
68
|
+
"@mastra/pg": "1.20.0-alpha.4",
|
|
69
|
+
"@internal/lint": "0.0.121",
|
|
70
|
+
"@internal/types-builder": "0.0.96"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=22.19.0"
|