sloppatrol 0.1.0 → 0.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/README.md +111 -160
- package/dist/census/index.js +4 -2
- package/dist/census/index.js.map +1 -1
- package/dist/census/lib/guardrails.js +38 -9
- package/dist/census/lib/guardrails.js.map +1 -1
- package/dist/census/lib/stack.d.ts +2 -1
- package/dist/census/lib/stack.js +8 -4
- package/dist/census/lib/stack.js.map +1 -1
- package/dist/cli/index.js +20 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/lib/browser.d.ts +2 -0
- package/dist/cli/lib/browser.js +13 -0
- package/dist/cli/lib/browser.js.map +1 -0
- package/dist/cli/lib/usage.d.ts +1 -1
- package/dist/cli/lib/usage.js +4 -3
- package/dist/cli/lib/usage.js.map +1 -1
- package/dist/config/lib/schema.d.ts +30 -0
- package/dist/config/lib/schema.js +16 -0
- package/dist/config/lib/schema.js.map +1 -1
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.js +6 -2
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/lib/snapshot.d.ts +168 -30
- package/dist/dashboard/lib/snapshot.js +104 -37
- package/dist/dashboard/lib/snapshot.js.map +1 -1
- package/dist/dashboard/lib/summary.d.ts +230 -0
- package/dist/dashboard/lib/summary.js +398 -0
- package/dist/dashboard/lib/summary.js.map +1 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +1 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/lib/cycle.d.ts +17 -0
- package/dist/domain/lib/cycle.js +2 -0
- package/dist/domain/lib/cycle.js.map +1 -0
- package/dist/{patrol → domain}/lib/pick.d.ts +2 -1
- package/dist/{patrol → domain}/lib/pick.js.map +1 -1
- package/dist/domain/lib/refactor.d.ts +2 -0
- package/dist/gate/index.d.ts +7 -0
- package/dist/gate/index.js +15 -0
- package/dist/gate/index.js.map +1 -1
- package/dist/patrol/lib/attempt.js +10 -2
- package/dist/patrol/lib/attempt.js.map +1 -1
- package/dist/patrol/lib/base.d.ts +10 -3
- package/dist/patrol/lib/base.js +14 -8
- package/dist/patrol/lib/base.js.map +1 -1
- package/dist/patrol/lib/cycle.d.ts +1 -1
- package/dist/patrol/lib/cycle.js +32 -1
- package/dist/patrol/lib/cycle.js.map +1 -1
- package/dist/patrol/lib/recover.d.ts +2 -1
- package/dist/patrol/lib/recover.js +16 -1
- package/dist/patrol/lib/recover.js.map +1 -1
- package/dist/patrol/lib/scorecard.js +12 -4
- package/dist/patrol/lib/scorecard.js.map +1 -1
- package/dist/patrol/lib/steps.d.ts +5 -2
- package/dist/patrol/lib/steps.js +9 -6
- package/dist/patrol/lib/steps.js.map +1 -1
- package/dist/playbook/index.js +3 -3
- package/dist/roles/lib/prompts.d.ts +1 -0
- package/dist/roles/lib/prompts.js +6 -0
- package/dist/roles/lib/prompts.js.map +1 -1
- package/dist/roles/lib/refactorer.d.ts +1 -0
- package/dist/roles/lib/refactorer.js +24 -6
- package/dist/roles/lib/refactorer.js.map +1 -1
- package/dist/roles/lib/scorecard-text.js +1 -1
- package/dist/roles/lib/scorecard-text.js.map +1 -1
- package/dist/store/index.d.ts +4 -1
- package/dist/store/index.js +7 -0
- package/dist/store/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/app.js +1236 -324
- package/ui/index.html +35 -13
- package/ui/style.css +1063 -340
- /package/dist/{patrol → domain}/lib/pick.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/dashboard/lib/snapshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/dashboard/lib/snapshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EACL,WAAW,EAGX,cAAc,EACd,WAAW,GAEZ,MAAM,cAAc,CAAC;AAEtB,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,8EAA8E;AAC9E,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAU,EAAE,MAAoB,EAAE,MAAe;IACnE,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;IAC9D,OAAO;QACL,EAAE;QACF,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,SAAS,EAAE,KAAK,CAAC,EAAE;QACnB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,yFAAyF;QACzF,KAAK,EACH,CAAC,KAAK,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;YACzC,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM;gBAChB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,SAAS;YACb,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO;gBACrB,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,MAAM;QACd,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QAChD,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;KAChH,CAAC;AACJ,CAAC;AAED,wFAAwF;AACxF,SAAS,KAAK,CAAqB,GAAM;IACvC,OAAO,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAC3G,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAY,EAAE,OAAe,EAAE,QAAkC,YAAY;IACrG,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,MAAM,WAAW,GAAG,KAAK,EAAE,GAAa,EAAE,MAAe,EAAE,EAAE,CAC3D,CACE,MAAM,OAAO,CAAC,GAAG,CACf,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACnB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,MAAM,GAAG,GAAG,UAAU,CAAC,EAAE,EAAE,MAAM,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,GAAG,EAAE,OAAO;YAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACrC,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CACH,CACF,CAAC,MAAM,CAAC,CAAC,GAAG,EAAqB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAE9C,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,GAC9F,MAAM,OAAO,CAAC,GAAG,CAAC;YAChB,KAAK,CAAC,WAAW,EAAE;YACnB,KAAK,CAAC,QAAQ,EAAE;YAChB,KAAK,CAAC,iBAAiB,EAAE;YACzB,KAAK,CAAC,aAAa,EAAE;YACrB,KAAK,CAAC,aAAa,EAAE;YACrB,KAAK,CAAC,QAAQ,EAAE;YAChB,KAAK,CAAC,cAAc,EAAE;YACtB,KAAK,CAAC,UAAU,EAAE;SACnB,CAAC,CAAC;QACL,MAAM,QAAQ,GAAiB,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACnD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC7B,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;QAC5E,CAAC,CAAC,CAAC;QACH,uEAAuE;QACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1D,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QACxG,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,cAAc,CAAC;YAC7B,OAAO;YACP,GAAG;YACH,MAAM;YACN,QAAQ;YACR,aAAa;YACb,SAAS,EAAE,YAAY;YACvB,SAAS,EAAE,OAAO;YAClB,UAAU;YACV,QAAQ;YACR,MAAM;SACP,CAAC,CAAC;QACH,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACxG,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,OAAO;YACX,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC;QAChC,CAAC;QACD,gEAAgE;QAChE,KAAK,CAAC,KAAK;YACT,MAAM,IAAI,GAAG,MAAM,IAAI,EAAE,CAAC;YAC1B,OAAO;gBACL,OAAO;gBACP,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE;gBAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE;gBAC3C,qEAAqE;gBACrE,SAAS,EAAE,IAAI,CAAC,YAAY;qBACzB,OAAO,EAAE;qBACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACT,CAAC,CAAC,OAAO;oBACP,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE;oBAClG,CAAC,CAAC,CAAC,CACN;gBACH,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC;gBAClE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,CAAC,OAAO,EAAE;gBACzD,yEAAyE;gBACzE,sDAAsD;gBACtD,UAAU,EAAE,IAAI,CAAC,UAAU;qBACxB,KAAK,CAAC,CAAC,iBAAiB,CAAC;qBACzB,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;gBAChF,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;aAClG,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { type AgentStatus, type CheckRun, type Cycle, type Opportunity, type Refactor, type Scorecard, type Strength } from '../../domain/index.js';
|
|
2
|
+
export type Tone = 'pass' | 'warn' | 'fail' | 'info' | 'run';
|
|
3
|
+
/** A status as the patrol wrote it, and whether its process still runs. */
|
|
4
|
+
export type LiveStatus = AgentStatus & {
|
|
5
|
+
alive: boolean;
|
|
6
|
+
stale: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** One step of a refactor's pipeline: done, failed, running, waiting for a person, or not reached. */
|
|
9
|
+
type Step = {
|
|
10
|
+
id: StepId;
|
|
11
|
+
label: string;
|
|
12
|
+
state: 'ok' | 'bad' | 'run' | 'wait' | '';
|
|
13
|
+
};
|
|
14
|
+
type StepId = 'refactorer' | 'guards' | 'checks' | 'mutation' | 'metrics' | 'review' | 'pr';
|
|
15
|
+
/**
|
|
16
|
+
* What became of a refactor, for people: it waits for a person (`waits`),
|
|
17
|
+
* the gate rejected it, it stopped with an error, or Sloppatrol stopped
|
|
18
|
+
* while it ran (`interrupted`).
|
|
19
|
+
*/
|
|
20
|
+
export type Outcome = 'working' | 'waits' | 'rejected' | 'error' | 'interrupted' | 'merged' | 'closed';
|
|
21
|
+
export type RefactorRow = Refactor & {
|
|
22
|
+
title: string;
|
|
23
|
+
kind?: Opportunity['kind'];
|
|
24
|
+
side?: Opportunity['side'];
|
|
25
|
+
outcome: Outcome;
|
|
26
|
+
/** The step that rejected it. */
|
|
27
|
+
rejectedAt?: StepId;
|
|
28
|
+
steps: Step[];
|
|
29
|
+
};
|
|
30
|
+
export type CheckRunRow = CheckRun & {
|
|
31
|
+
outcome: {
|
|
32
|
+
tone: 'ok' | 'bad' | 'run' | 'stop';
|
|
33
|
+
text: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A result that needs a person: red checks on main, a refactor that the
|
|
37
|
+
* gate failed, or a planted bug the tests missed. A red refactorer try is
|
|
38
|
+
* not one: the refactorer runs the checks to test its own change.
|
|
39
|
+
*/
|
|
40
|
+
problem: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type SessionRow = {
|
|
43
|
+
id: string;
|
|
44
|
+
role: string;
|
|
45
|
+
subject?: string;
|
|
46
|
+
startedAt: string;
|
|
47
|
+
endedAt?: string;
|
|
48
|
+
/** `stopped`: it has no end, and no Sloppatrol process runs. */
|
|
49
|
+
state: 'running' | 'done' | 'failed' | 'stopped';
|
|
50
|
+
last: string;
|
|
51
|
+
costUsd: number;
|
|
52
|
+
};
|
|
53
|
+
type AttentionItem = {
|
|
54
|
+
tone: Tone;
|
|
55
|
+
title: string;
|
|
56
|
+
detail: string;
|
|
57
|
+
command?: string;
|
|
58
|
+
/** A page of the dashboard, with a filter or a selected item. */
|
|
59
|
+
target?: {
|
|
60
|
+
tab: string;
|
|
61
|
+
filter?: string;
|
|
62
|
+
select?: string;
|
|
63
|
+
};
|
|
64
|
+
links?: {
|
|
65
|
+
label: string;
|
|
66
|
+
href: string;
|
|
67
|
+
select?: string;
|
|
68
|
+
}[];
|
|
69
|
+
};
|
|
70
|
+
export type ProjectSummary = ReturnType<typeof projectSummary>;
|
|
71
|
+
/**
|
|
72
|
+
* The pipeline of one refactor, from its own record and its check runs.
|
|
73
|
+
* The verdict and the proof hold the result of each step, so an old refactor
|
|
74
|
+
* shows the right steps also when its check runs are no longer on the page.
|
|
75
|
+
*/
|
|
76
|
+
export declare function refactorRow(refactor: Refactor, opportunity: Opportunity | undefined, runs: CheckRun[], active: boolean): RefactorRow;
|
|
77
|
+
/**
|
|
78
|
+
* The result of a check run, for people. A mutation run fails its tests on
|
|
79
|
+
* purpose: on the refactor, a failed run means the new tests caught the
|
|
80
|
+
* planted bug. On main, for new coverage, the old tests must miss it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function checkRunRow(run: CheckRun, active: boolean): CheckRunRow;
|
|
83
|
+
/** The numbers of the whole project. Each page reads them from here, so two pages cannot disagree. */
|
|
84
|
+
export declare function projectSummary(input: {
|
|
85
|
+
project: string;
|
|
86
|
+
now: Date;
|
|
87
|
+
patrol: {
|
|
88
|
+
lastSha?: string;
|
|
89
|
+
lastCycleAt?: string;
|
|
90
|
+
cycles: number;
|
|
91
|
+
};
|
|
92
|
+
statuses: LiveStatus[];
|
|
93
|
+
opportunities: Opportunity[];
|
|
94
|
+
refactors: RefactorRow[];
|
|
95
|
+
checkRuns: CheckRunRow[];
|
|
96
|
+
scorecards: Scorecard[];
|
|
97
|
+
sessions: SessionRow[];
|
|
98
|
+
cycles: Cycle[];
|
|
99
|
+
}): {
|
|
100
|
+
checks: {
|
|
101
|
+
caught?: number | undefined;
|
|
102
|
+
planted?: number | undefined;
|
|
103
|
+
runs: number;
|
|
104
|
+
problems: number;
|
|
105
|
+
base: {
|
|
106
|
+
runs: number;
|
|
107
|
+
bad: number;
|
|
108
|
+
problems: number;
|
|
109
|
+
};
|
|
110
|
+
gate: {
|
|
111
|
+
runs: number;
|
|
112
|
+
bad: number;
|
|
113
|
+
problems: number;
|
|
114
|
+
};
|
|
115
|
+
refactorer: {
|
|
116
|
+
runs: number;
|
|
117
|
+
bad: number;
|
|
118
|
+
problems: number;
|
|
119
|
+
};
|
|
120
|
+
mutation: {
|
|
121
|
+
runs: number;
|
|
122
|
+
bad: number;
|
|
123
|
+
problems: number;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
sessions: {
|
|
127
|
+
total: number;
|
|
128
|
+
running: number;
|
|
129
|
+
failed: number;
|
|
130
|
+
failedRecent: number;
|
|
131
|
+
stopped: number;
|
|
132
|
+
costUsd: number;
|
|
133
|
+
costRecentUsd: number;
|
|
134
|
+
byRole: {
|
|
135
|
+
[k: string]: number;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
attention: AttentionItem[];
|
|
139
|
+
quality?: {
|
|
140
|
+
/** The files that both of the last two scorecards scored: the change compares these only. */
|
|
141
|
+
compared: number;
|
|
142
|
+
trend: number[];
|
|
143
|
+
metrics: {
|
|
144
|
+
trend: number[];
|
|
145
|
+
delta?: number | undefined;
|
|
146
|
+
id: "tests" | "simplicity" | "modularity" | "injection" | "purity" | "clarity" | "guardrails";
|
|
147
|
+
name: string;
|
|
148
|
+
value: number;
|
|
149
|
+
}[];
|
|
150
|
+
coverage: {
|
|
151
|
+
files: number;
|
|
152
|
+
totalFiles: number;
|
|
153
|
+
share: number;
|
|
154
|
+
};
|
|
155
|
+
guardrails: {
|
|
156
|
+
present: number;
|
|
157
|
+
total: number;
|
|
158
|
+
};
|
|
159
|
+
delta?: number | undefined;
|
|
160
|
+
overall?: number | undefined;
|
|
161
|
+
sha: string;
|
|
162
|
+
at: string;
|
|
163
|
+
rubric: string;
|
|
164
|
+
} | undefined;
|
|
165
|
+
cycles: {
|
|
166
|
+
last?: {
|
|
167
|
+
at: string;
|
|
168
|
+
tone: Tone;
|
|
169
|
+
text: string;
|
|
170
|
+
sha?: string;
|
|
171
|
+
} | undefined;
|
|
172
|
+
count: number;
|
|
173
|
+
};
|
|
174
|
+
refactors: {
|
|
175
|
+
rejectedBy: {
|
|
176
|
+
step: string;
|
|
177
|
+
label: string;
|
|
178
|
+
count: number;
|
|
179
|
+
}[];
|
|
180
|
+
keptRate?: number | undefined;
|
|
181
|
+
total: number;
|
|
182
|
+
working: number;
|
|
183
|
+
waits: number;
|
|
184
|
+
rejected: number;
|
|
185
|
+
error: number;
|
|
186
|
+
merged: number;
|
|
187
|
+
closed: number;
|
|
188
|
+
judged: number;
|
|
189
|
+
kept: number;
|
|
190
|
+
};
|
|
191
|
+
decisions: {
|
|
192
|
+
toPublish: number;
|
|
193
|
+
toReview: number;
|
|
194
|
+
};
|
|
195
|
+
backlog: {
|
|
196
|
+
total: number;
|
|
197
|
+
open: number;
|
|
198
|
+
/** The open opportunities in the order the refactorer picks them. */
|
|
199
|
+
ready: string[];
|
|
200
|
+
/** Open, but a waiting refactor changes one of its files. */
|
|
201
|
+
busy: string[];
|
|
202
|
+
/** Open, and a refactor works on it now. */
|
|
203
|
+
working: string[];
|
|
204
|
+
byStrength: Record<Strength, number>;
|
|
205
|
+
verified: number;
|
|
206
|
+
proposed: number;
|
|
207
|
+
done: number;
|
|
208
|
+
exhausted: number;
|
|
209
|
+
dismissed: number;
|
|
210
|
+
};
|
|
211
|
+
main?: {
|
|
212
|
+
failed?: string | undefined;
|
|
213
|
+
sha: string;
|
|
214
|
+
state: "running" | "failed" | "passed";
|
|
215
|
+
runId: string;
|
|
216
|
+
at: string;
|
|
217
|
+
} | undefined;
|
|
218
|
+
project: string;
|
|
219
|
+
verdict: {
|
|
220
|
+
tone: Tone;
|
|
221
|
+
text: string;
|
|
222
|
+
detail: string;
|
|
223
|
+
};
|
|
224
|
+
activity: {
|
|
225
|
+
state: "running" | "waiting" | "stopped";
|
|
226
|
+
text: string;
|
|
227
|
+
detail: string;
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
export {};
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { pickOpportunities, } from '../../domain/index.js';
|
|
2
|
+
import { METRICS, meanScores, overallPercent, toPercent } from '../../metrics/index.js';
|
|
3
|
+
const STEP_LABELS = {
|
|
4
|
+
refactorer: 'Refactorer',
|
|
5
|
+
guards: 'Guards',
|
|
6
|
+
checks: 'Checks',
|
|
7
|
+
mutation: 'Mutation',
|
|
8
|
+
metrics: 'Metrics',
|
|
9
|
+
review: 'Review',
|
|
10
|
+
pr: 'Pull request',
|
|
11
|
+
};
|
|
12
|
+
const DAY = 24 * 3_600_000;
|
|
13
|
+
/**
|
|
14
|
+
* The pipeline of one refactor, from its own record and its check runs.
|
|
15
|
+
* The verdict and the proof hold the result of each step, so an old refactor
|
|
16
|
+
* shows the right steps also when its check runs are no longer on the page.
|
|
17
|
+
*/
|
|
18
|
+
export function refactorRow(refactor, opportunity, runs, active) {
|
|
19
|
+
const { verdict, proof } = refactor;
|
|
20
|
+
const interrupted = (refactor.status === 'working' && !active) ||
|
|
21
|
+
(refactor.status === 'failed' && /^Interrupted/.test(refactor.reason ?? ''));
|
|
22
|
+
const working = refactor.status === 'working' && !interrupted;
|
|
23
|
+
const mine = runs.filter((r) => r.refactorId === refactor.id);
|
|
24
|
+
const gate = (verdict?.checkRunId && runs.find((r) => r.id === verdict.checkRunId)) ||
|
|
25
|
+
mine.findLast((r) => r.purpose === 'gate');
|
|
26
|
+
const tests = opportunity?.side === 'tests';
|
|
27
|
+
const mutationRun = mine.findLast((r) => r.purpose === 'mutation');
|
|
28
|
+
const mutation = proof?.behaviour.mutation;
|
|
29
|
+
const states = {};
|
|
30
|
+
states.refactorer = refactor.headSha ? 'ok' : working ? 'run' : 'bad';
|
|
31
|
+
states.guards = verdict ? (verdict.guards.length ? 'bad' : 'ok') : gate ? 'ok' : '';
|
|
32
|
+
states.checks = gate
|
|
33
|
+
? gate.status === 'running'
|
|
34
|
+
? working
|
|
35
|
+
? 'run'
|
|
36
|
+
: ''
|
|
37
|
+
: gate.status === 'passed'
|
|
38
|
+
? 'ok'
|
|
39
|
+
: 'bad'
|
|
40
|
+
: verdict?.checks.some((c) => !c.ok)
|
|
41
|
+
? 'bad'
|
|
42
|
+
: '';
|
|
43
|
+
if (tests)
|
|
44
|
+
states.mutation = mutation
|
|
45
|
+
? mutation.caught && !mutation.caughtBefore
|
|
46
|
+
? 'ok'
|
|
47
|
+
: 'bad'
|
|
48
|
+
: working && mutationRun?.status === 'running'
|
|
49
|
+
? 'run'
|
|
50
|
+
: '';
|
|
51
|
+
const ready = states.checks === 'ok' && (!tests || states.mutation === 'ok');
|
|
52
|
+
states.metrics = proof?.improvement ? (proof.improvement.improved ? 'ok' : 'bad') : working && ready ? 'run' : '';
|
|
53
|
+
states.review = verdict?.review
|
|
54
|
+
? verdict.review.approve
|
|
55
|
+
? 'ok'
|
|
56
|
+
: 'bad'
|
|
57
|
+
: working && states.metrics === 'ok'
|
|
58
|
+
? 'run'
|
|
59
|
+
: '';
|
|
60
|
+
states.pr = refactor.prUrl
|
|
61
|
+
? refactor.status === 'closed'
|
|
62
|
+
? 'bad'
|
|
63
|
+
: 'ok'
|
|
64
|
+
: refactor.status === 'verified'
|
|
65
|
+
? 'wait'
|
|
66
|
+
: '';
|
|
67
|
+
const order = [
|
|
68
|
+
'refactorer',
|
|
69
|
+
'guards',
|
|
70
|
+
'checks',
|
|
71
|
+
...(tests ? ['mutation'] : []),
|
|
72
|
+
'metrics',
|
|
73
|
+
'review',
|
|
74
|
+
'pr',
|
|
75
|
+
];
|
|
76
|
+
const steps = order.map((id) => ({
|
|
77
|
+
id,
|
|
78
|
+
label: id === 'pr' && refactor.status === 'merged' ? 'Merged' : STEP_LABELS[id],
|
|
79
|
+
state: states[id] ?? '',
|
|
80
|
+
}));
|
|
81
|
+
const outcome = interrupted
|
|
82
|
+
? 'interrupted'
|
|
83
|
+
: working
|
|
84
|
+
? 'working'
|
|
85
|
+
: refactor.status === 'verified' || refactor.status === 'published'
|
|
86
|
+
? 'waits'
|
|
87
|
+
: refactor.status === 'failed'
|
|
88
|
+
? 'error'
|
|
89
|
+
: refactor.status;
|
|
90
|
+
const rejectedAt = refactor.status === 'rejected' ? (steps.find((s) => s.state === 'bad')?.id ?? 'refactorer') : undefined;
|
|
91
|
+
return {
|
|
92
|
+
...refactor,
|
|
93
|
+
title: opportunity?.title ?? refactor.branch,
|
|
94
|
+
...(opportunity ? { kind: opportunity.kind, side: opportunity.side } : {}),
|
|
95
|
+
outcome,
|
|
96
|
+
...(rejectedAt ? { rejectedAt } : {}),
|
|
97
|
+
steps,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The result of a check run, for people. A mutation run fails its tests on
|
|
102
|
+
* purpose: on the refactor, a failed run means the new tests caught the
|
|
103
|
+
* planted bug. On main, for new coverage, the old tests must miss it.
|
|
104
|
+
*/
|
|
105
|
+
export function checkRunRow(run, active) {
|
|
106
|
+
const outcome = () => {
|
|
107
|
+
if (run.status === 'running')
|
|
108
|
+
return active ? { tone: 'run', text: 'running' } : { tone: 'stop', text: 'stopped' };
|
|
109
|
+
if (run.purpose !== 'mutation')
|
|
110
|
+
return run.status === 'passed' ? { tone: 'ok', text: 'passed' } : { tone: 'bad', text: 'failed' };
|
|
111
|
+
const caught = run.status === 'failed';
|
|
112
|
+
if (run.branch === 'main')
|
|
113
|
+
return caught ? { tone: 'bad', text: 'old tests catch it' } : { tone: 'ok', text: 'old tests miss it' };
|
|
114
|
+
return caught ? { tone: 'ok', text: 'bug caught' } : { tone: 'bad', text: 'bug missed' };
|
|
115
|
+
};
|
|
116
|
+
const result = outcome();
|
|
117
|
+
return { ...run, outcome: result, problem: result.tone === 'bad' && run.purpose !== 'refactorer' };
|
|
118
|
+
}
|
|
119
|
+
/** The numbers of the whole project. Each page reads them from here, so two pages cannot disagree. */
|
|
120
|
+
export function projectSummary(input) {
|
|
121
|
+
const { refactors, checkRuns, opportunities, sessions } = input;
|
|
122
|
+
const now = input.now.getTime();
|
|
123
|
+
const count = (list, test) => list.filter(test).length;
|
|
124
|
+
const recent = (iso) => !!iso && now - Date.parse(iso) < 7 * DAY;
|
|
125
|
+
// Main: the newest finished run of the checks on the patrolled commit.
|
|
126
|
+
const baseRuns = checkRuns.filter((r) => r.purpose === 'base');
|
|
127
|
+
const lastBase = baseRuns.findLast((r) => r.outcome.tone !== 'stop');
|
|
128
|
+
const main = lastBase
|
|
129
|
+
? {
|
|
130
|
+
sha: lastBase.sha,
|
|
131
|
+
state: lastBase.status,
|
|
132
|
+
runId: lastBase.id,
|
|
133
|
+
at: lastBase.finishedAt ?? lastBase.startedAt,
|
|
134
|
+
...(lastBase.status === 'failed' ? { failed: lastBase.results.find((r) => !r.ok)?.name ?? 'a check' } : {}),
|
|
135
|
+
}
|
|
136
|
+
: undefined;
|
|
137
|
+
const blocked = main?.state === 'failed';
|
|
138
|
+
const outcomes = (outcome) => count(refactors, (r) => r.outcome === outcome);
|
|
139
|
+
const judged = refactors.filter((r) => ['waits', 'rejected', 'merged', 'closed'].includes(r.outcome));
|
|
140
|
+
const kept = count(judged, (r) => r.outcome !== 'rejected');
|
|
141
|
+
const rejectedBy = new Map();
|
|
142
|
+
for (const r of refactors)
|
|
143
|
+
if (r.rejectedAt)
|
|
144
|
+
rejectedBy.set(r.rejectedAt, (rejectedBy.get(r.rejectedAt) ?? 0) + 1);
|
|
145
|
+
const toPublish = refactors.filter((r) => r.status === 'verified');
|
|
146
|
+
const toReview = refactors.filter((r) => r.status === 'published');
|
|
147
|
+
const open = opportunities.filter((o) => o.status === 'open' && !toPublish.some((r) => r.opportunityId === o.id));
|
|
148
|
+
const working = new Set(refactors.filter((r) => r.outcome === 'working').map((r) => r.opportunityId));
|
|
149
|
+
const ready = pickOpportunities(opportunities, refactors, {
|
|
150
|
+
count: Number.POSITIVE_INFINITY,
|
|
151
|
+
maxOpen: Number.POSITIVE_INFINITY,
|
|
152
|
+
}).filter((o) => !working.has(o.id));
|
|
153
|
+
const byStrength = { strong: 0, 'worth-exploring': 0, speculative: 0 };
|
|
154
|
+
for (const o of open)
|
|
155
|
+
byStrength[o.strength]++;
|
|
156
|
+
const patrolStatus = input.statuses.find((s) => s.role === 'patrol');
|
|
157
|
+
const runningStatus = input.statuses.find((s) => s.alive && s.state === 'running' && s.role !== 'patrol');
|
|
158
|
+
const activity = patrolStatus?.alive && patrolStatus.state === 'running'
|
|
159
|
+
? { state: 'running', text: 'Patrol runs', detail: patrolStatus.detail }
|
|
160
|
+
: runningStatus
|
|
161
|
+
? { state: 'running', text: `${capital(runningStatus.role)} runs`, detail: runningStatus.detail }
|
|
162
|
+
: patrolStatus?.alive
|
|
163
|
+
? { state: 'waiting', text: 'Patrol waits', detail: patrolStatus.detail }
|
|
164
|
+
: { state: 'stopped', text: 'Not running', detail: 'No Sloppatrol process runs now.' };
|
|
165
|
+
const lastCycle = input.cycles.at(-1);
|
|
166
|
+
const cycle = lastCycle
|
|
167
|
+
? cycleResult(lastCycle)
|
|
168
|
+
: input.patrol.lastCycleAt
|
|
169
|
+
? { at: input.patrol.lastCycleAt, tone: 'info', text: 'Finished' }
|
|
170
|
+
: undefined;
|
|
171
|
+
const quality = qualityOf(input.scorecards);
|
|
172
|
+
const failedSessions = sessions.filter((s) => s.state === 'failed' && recent(s.startedAt));
|
|
173
|
+
const stale = input.statuses.filter((s) => s.stale);
|
|
174
|
+
const interrupted = refactors.filter((r) => r.outcome === 'interrupted' && r.status === 'working');
|
|
175
|
+
const attention = [];
|
|
176
|
+
if (main && blocked)
|
|
177
|
+
attention.push({
|
|
178
|
+
tone: 'fail',
|
|
179
|
+
title: `The check ${main.failed} fails on main`,
|
|
180
|
+
detail: `Commit ${main.sha.slice(0, 10)}. No refactor can be verified until the checks pass on main. Fix main, then run the checks again.`,
|
|
181
|
+
command: 'sloppatrol check',
|
|
182
|
+
target: { tab: 'checks', select: `check:${main.runId}` },
|
|
183
|
+
});
|
|
184
|
+
if (interrupted.length || stale.length)
|
|
185
|
+
attention.push({
|
|
186
|
+
tone: 'warn',
|
|
187
|
+
title: 'Sloppatrol stopped during its work',
|
|
188
|
+
detail: interrupted.length
|
|
189
|
+
? `${plural(interrupted.length, 'refactor')} did not finish. The next patrol cleans up the worktree and tries again.`
|
|
190
|
+
: `The ${stale.map((s) => s.role).join(', ')} stopped without an end. The next patrol cleans up.`,
|
|
191
|
+
command: 'sloppatrol',
|
|
192
|
+
target: { tab: 'refactors', filter: 'error' },
|
|
193
|
+
});
|
|
194
|
+
if (toPublish.length)
|
|
195
|
+
attention.push({
|
|
196
|
+
tone: 'warn',
|
|
197
|
+
title: `${plural(toPublish.length, 'verified refactor')} ${toPublish.length === 1 ? 'waits' : 'wait'} to be published`,
|
|
198
|
+
detail: toPublish.length === 1
|
|
199
|
+
? 'The gate kept it. Publish it to open its pull request.'
|
|
200
|
+
: 'The gate kept them. Publish them to open a pull request for each.',
|
|
201
|
+
command: 'sloppatrol publish',
|
|
202
|
+
target: { tab: 'refactors', filter: 'waits' },
|
|
203
|
+
links: toPublish.map((r) => ({ label: r.title, href: '', select: `refactor:${r.id}` })),
|
|
204
|
+
});
|
|
205
|
+
if (toReview.length)
|
|
206
|
+
attention.push({
|
|
207
|
+
tone: 'info',
|
|
208
|
+
title: `${plural(toReview.length, 'pull request')} ${toReview.length === 1 ? 'waits' : 'wait'} for your review`,
|
|
209
|
+
detail: 'Merge a pull request to keep the change. Close it to dismiss the opportunity for good.',
|
|
210
|
+
target: { tab: 'refactors', filter: 'waits' },
|
|
211
|
+
links: toReview.map((r) => ({ label: r.title, href: r.prUrl ?? '', select: `refactor:${r.id}` })),
|
|
212
|
+
});
|
|
213
|
+
if (failedSessions.length)
|
|
214
|
+
attention.push({
|
|
215
|
+
tone: 'warn',
|
|
216
|
+
title: `${plural(failedSessions.length, 'agent session')} failed in the last 7 days`,
|
|
217
|
+
detail: `Last error: ${failedSessions.at(-1).last}`,
|
|
218
|
+
command: 'sloppatrol doctor',
|
|
219
|
+
target: { tab: 'sessions', filter: 'failed' },
|
|
220
|
+
});
|
|
221
|
+
if (main && !blocked && !open.length && opportunities.length)
|
|
222
|
+
attention.push({
|
|
223
|
+
tone: 'info',
|
|
224
|
+
title: 'The backlog is empty',
|
|
225
|
+
detail: 'The refactorer has no open opportunity. The surveyor records more when the backlog is low.',
|
|
226
|
+
command: 'sloppatrol survey',
|
|
227
|
+
target: { tab: 'backlog' },
|
|
228
|
+
});
|
|
229
|
+
const started = !!(main ||
|
|
230
|
+
refactors.length ||
|
|
231
|
+
opportunities.length ||
|
|
232
|
+
input.scorecards.length ||
|
|
233
|
+
input.cycles.length);
|
|
234
|
+
const errors = interrupted.length + stale.length + failedSessions.length > 0;
|
|
235
|
+
const waiting = toPublish.length + toReview.length;
|
|
236
|
+
const verdict = !started
|
|
237
|
+
? { tone: 'info', text: 'Not started', detail: 'Sloppatrol has not run on this repo yet.' }
|
|
238
|
+
: blocked
|
|
239
|
+
? { tone: 'fail', text: 'Blocked', detail: 'The checks fail on main, so no refactor can be verified.' }
|
|
240
|
+
: errors
|
|
241
|
+
? { tone: 'warn', text: 'Needs a look', detail: 'Some work stopped with an error.' }
|
|
242
|
+
: waiting
|
|
243
|
+
? {
|
|
244
|
+
tone: 'warn',
|
|
245
|
+
text: 'Waits for you',
|
|
246
|
+
detail: `${plural(waiting, 'refactor')} ${waiting === 1 ? 'waits' : 'wait'} for a decision.`,
|
|
247
|
+
}
|
|
248
|
+
: !main
|
|
249
|
+
? { tone: 'info', text: 'No checks yet', detail: 'The checks have not run on main yet.' }
|
|
250
|
+
: { tone: 'pass', text: 'Healthy', detail: 'The checks pass on main, and nothing waits for you.' };
|
|
251
|
+
const mutationRuns = checkRuns.filter((r) => r.purpose === 'mutation' && r.branch !== 'main' && r.outcome.tone !== 'run');
|
|
252
|
+
const byPurpose = (purpose) => {
|
|
253
|
+
const list = checkRuns.filter((r) => r.purpose === purpose);
|
|
254
|
+
return {
|
|
255
|
+
runs: list.length,
|
|
256
|
+
bad: count(list, (r) => r.outcome.tone === 'bad'),
|
|
257
|
+
problems: count(list, (r) => r.problem),
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
const cost = (list) => Math.round(list.reduce((sum, s) => sum + s.costUsd, 0) * 100) / 100;
|
|
261
|
+
return {
|
|
262
|
+
project: input.project,
|
|
263
|
+
verdict,
|
|
264
|
+
activity,
|
|
265
|
+
...(main ? { main } : {}),
|
|
266
|
+
cycles: { count: input.patrol.cycles, ...(cycle ? { last: cycle } : {}) },
|
|
267
|
+
refactors: {
|
|
268
|
+
total: refactors.length,
|
|
269
|
+
working: outcomes('working'),
|
|
270
|
+
waits: outcomes('waits'),
|
|
271
|
+
rejected: outcomes('rejected'),
|
|
272
|
+
error: outcomes('error') + outcomes('interrupted'),
|
|
273
|
+
merged: outcomes('merged'),
|
|
274
|
+
closed: outcomes('closed'),
|
|
275
|
+
judged: judged.length,
|
|
276
|
+
kept,
|
|
277
|
+
...(judged.length ? { keptRate: Math.round((kept / judged.length) * 100) } : {}),
|
|
278
|
+
rejectedBy: [...rejectedBy]
|
|
279
|
+
.map(([step, n]) => ({ step, label: STEP_LABELS[step], count: n }))
|
|
280
|
+
.sort((a, b) => b.count - a.count),
|
|
281
|
+
},
|
|
282
|
+
decisions: { toPublish: toPublish.length, toReview: toReview.length },
|
|
283
|
+
backlog: {
|
|
284
|
+
total: opportunities.length,
|
|
285
|
+
open: open.length,
|
|
286
|
+
/** The open opportunities in the order the refactorer picks them. */
|
|
287
|
+
ready: ready.map((o) => o.id),
|
|
288
|
+
/** Open, but a waiting refactor changes one of its files. */
|
|
289
|
+
busy: open.filter((o) => !ready.includes(o) && !working.has(o.id)).map((o) => o.id),
|
|
290
|
+
/** Open, and a refactor works on it now. */
|
|
291
|
+
working: [...working],
|
|
292
|
+
byStrength,
|
|
293
|
+
verified: toPublish.length,
|
|
294
|
+
proposed: count(opportunities, (o) => o.status === 'proposed'),
|
|
295
|
+
done: count(opportunities, (o) => o.status === 'done'),
|
|
296
|
+
exhausted: count(opportunities, (o) => o.status === 'exhausted'),
|
|
297
|
+
dismissed: count(opportunities, (o) => o.status === 'dismissed'),
|
|
298
|
+
},
|
|
299
|
+
...(quality ? { quality } : {}),
|
|
300
|
+
checks: {
|
|
301
|
+
runs: checkRuns.length,
|
|
302
|
+
problems: count(checkRuns, (r) => r.problem),
|
|
303
|
+
base: byPurpose('base'),
|
|
304
|
+
gate: byPurpose('gate'),
|
|
305
|
+
refactorer: byPurpose('refactorer'),
|
|
306
|
+
mutation: byPurpose('mutation'),
|
|
307
|
+
...(mutationRuns.length
|
|
308
|
+
? { caught: count(mutationRuns, (r) => r.outcome.tone === 'ok'), planted: mutationRuns.length }
|
|
309
|
+
: {}),
|
|
310
|
+
},
|
|
311
|
+
sessions: {
|
|
312
|
+
total: sessions.length,
|
|
313
|
+
running: count(sessions, (s) => s.state === 'running'),
|
|
314
|
+
failed: count(sessions, (s) => s.state === 'failed'),
|
|
315
|
+
failedRecent: failedSessions.length,
|
|
316
|
+
stopped: count(sessions, (s) => s.state === 'stopped'),
|
|
317
|
+
costUsd: cost(sessions),
|
|
318
|
+
costRecentUsd: cost(sessions.filter((s) => recent(s.startedAt))),
|
|
319
|
+
byRole: Object.fromEntries([...new Set(sessions.map((s) => s.role))].map((role) => [role, count(sessions, (s) => s.role === role)])),
|
|
320
|
+
},
|
|
321
|
+
attention,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function cycleResult(cycle) {
|
|
325
|
+
const kept = cycle.refactors.filter((r) => r.status === 'verified').length;
|
|
326
|
+
const parts = [
|
|
327
|
+
cycle.surveyed ? `${plural(cycle.surveyed, 'opportunity', 'opportunities')} found` : '',
|
|
328
|
+
cycle.refactors.length ? `${kept} of ${plural(cycle.refactors.length, 'refactor')} kept` : '',
|
|
329
|
+
cycle.published ? `${plural(cycle.published, 'pull request')} opened` : '',
|
|
330
|
+
].filter(Boolean);
|
|
331
|
+
const base = { at: cycle.finishedAt, ...(cycle.sha ? { sha: cycle.sha } : {}) };
|
|
332
|
+
if (cycle.error)
|
|
333
|
+
return { ...base, tone: 'fail', text: `Failed: ${cycle.error}` };
|
|
334
|
+
if (cycle.blocked)
|
|
335
|
+
return { ...base, tone: 'fail', text: 'Blocked: the checks fail on main' };
|
|
336
|
+
return {
|
|
337
|
+
...base,
|
|
338
|
+
tone: kept || cycle.published ? 'pass' : 'info',
|
|
339
|
+
text: parts.length ? capital(parts.join(', ')) : 'No new work',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The newest scorecard, with the change since the one before. The change
|
|
344
|
+
* compares like with like: only the files that both scorecards scored, so
|
|
345
|
+
* a file that joins the panel does not read as a change in quality.
|
|
346
|
+
*/
|
|
347
|
+
function qualityOf(scorecards) {
|
|
348
|
+
const current = scorecards.at(-1);
|
|
349
|
+
if (!current)
|
|
350
|
+
return undefined;
|
|
351
|
+
const previous = scorecards.at(-2);
|
|
352
|
+
const before = new Map(previous?.units.map((u) => [u.path, u]) ?? []);
|
|
353
|
+
const common = current.units.filter((u) => before.has(u.path));
|
|
354
|
+
const now = meanScores(common);
|
|
355
|
+
const then = meanScores(common.map((u) => before.get(u.path)));
|
|
356
|
+
const shared = Object.keys(now).filter((id) => then[id] !== undefined);
|
|
357
|
+
const pick = (means) => Object.fromEntries(shared.map((id) => [id, means[id]]));
|
|
358
|
+
const overallNow = overallPercent(pick(now));
|
|
359
|
+
const overallThen = overallPercent(pick(then));
|
|
360
|
+
const c = current.coverage;
|
|
361
|
+
const overall = overallPercent(current.means);
|
|
362
|
+
return {
|
|
363
|
+
sha: current.sha,
|
|
364
|
+
at: current.at,
|
|
365
|
+
rubric: current.rubric,
|
|
366
|
+
...(overall !== undefined ? { overall } : {}),
|
|
367
|
+
...(overallNow !== undefined && overallThen !== undefined ? { delta: overallNow - overallThen } : {}),
|
|
368
|
+
/** The files that both of the last two scorecards scored: the change compares these only. */
|
|
369
|
+
compared: common.length,
|
|
370
|
+
trend: scorecards.map((card) => overallPercent(card.means)).filter((v) => v !== undefined),
|
|
371
|
+
metrics: METRICS.filter((m) => current.means[m.id] !== undefined).map((m) => ({
|
|
372
|
+
id: m.id,
|
|
373
|
+
name: m.name,
|
|
374
|
+
value: toPercent(current.means[m.id]),
|
|
375
|
+
...(shared.includes(m.id) ? { delta: toPercent(now[m.id]) - toPercent(then[m.id]) } : {}),
|
|
376
|
+
trend: scorecards
|
|
377
|
+
.map((card) => card.means[m.id])
|
|
378
|
+
.filter((v) => v !== undefined)
|
|
379
|
+
.map(toPercent),
|
|
380
|
+
})),
|
|
381
|
+
coverage: {
|
|
382
|
+
files: c.files,
|
|
383
|
+
totalFiles: c.totalFiles,
|
|
384
|
+
share: c.totalLines ? Math.round((c.lines / c.totalLines) * 100) : 0,
|
|
385
|
+
},
|
|
386
|
+
guardrails: {
|
|
387
|
+
present: (current.guardrails ?? []).filter((g) => g.present).length,
|
|
388
|
+
total: (current.guardrails ?? []).length,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function plural(n, word, many = `${word}s`) {
|
|
393
|
+
return `${n.toLocaleString('en-US')} ${n === 1 ? word : many}`;
|
|
394
|
+
}
|
|
395
|
+
function capital(text) {
|
|
396
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
397
|
+
}
|
|
398
|
+
//# sourceMappingURL=summary.js.map
|