@aipper/aiws 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -33
- package/package.json +3 -3
- package/src/cli.js +40 -2
- package/src/codex-skills.js +4 -4
- package/src/commands/change-advice.js +191 -0
- package/src/commands/change-evidence-command.js +242 -0
- package/src/commands/change-evidence-entry.js +40 -0
- package/src/commands/change-evidence.js +338 -0
- package/src/commands/change-finish.js +484 -0
- package/src/commands/change-git-status.js +53 -0
- package/src/commands/change-lifecycle-entry.js +60 -0
- package/src/commands/change-lifecycle.js +234 -0
- package/src/commands/change-metrics-command.js +110 -0
- package/src/commands/change-next-command.js +132 -0
- package/src/commands/change-review-gates.js +315 -0
- package/src/commands/change-scope-gate.js +34 -0
- package/src/commands/change-start.js +138 -0
- package/src/commands/change-state-command.js +40 -0
- package/src/commands/change-status-command.js +126 -0
- package/src/commands/change-status-context.js +202 -0
- package/src/commands/change-validate-entry.js +45 -0
- package/src/commands/change-validate.js +127 -0
- package/src/commands/change.js +540 -1692
- package/src/commands/dashboard.js +38 -12
- package/src/commands/init.js +1 -1
- package/src/commands/opencode-status.js +61 -0
- package/src/commands/update.js +5 -1
- package/src/dashboard/app.js +205 -2
- package/src/dashboard/index.html +5 -2
- package/src/dashboard/style.css +86 -0
- package/src/governance.js +157 -0
- package/src/json-schema-lite.js +164 -0
- package/src/opencode-env.js +76 -0
- package/src/spec.js +131 -0
- package/src/template.js +42 -2
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} gitRoot
|
|
5
|
+
* @param {string} changeId
|
|
6
|
+
* @param {{
|
|
7
|
+
* currentBranch: (gitRoot: string) => Promise<string>,
|
|
8
|
+
* listGitWorktrees: (gitRoot: string) => Promise<Array<{ worktree: string, branch: string }>>,
|
|
9
|
+
* pathExists: (path: string) => Promise<boolean>,
|
|
10
|
+
* isGitRepository: (path: string) => Promise<boolean>,
|
|
11
|
+
* inferChangeIdFromBranch: (branch: string) => string
|
|
12
|
+
* }} deps
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveChangeStatusContext(gitRoot, changeId, deps) {
|
|
15
|
+
const rootBranch = await deps.currentBranch(gitRoot);
|
|
16
|
+
/** @type {Array<{ worktree: string, branch: string }>} */
|
|
17
|
+
let worktrees = [];
|
|
18
|
+
try {
|
|
19
|
+
worktrees = await deps.listGitWorktrees(gitRoot);
|
|
20
|
+
} catch {
|
|
21
|
+
worktrees = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const changeRef = `refs/heads/change/${changeId}`;
|
|
25
|
+
const changeWorktree = worktrees.find((worktree) => String(worktree.branch || "") === changeRef)?.worktree || "";
|
|
26
|
+
const rootWorktree = path.resolve(gitRoot);
|
|
27
|
+
let contextWorktree = rootWorktree;
|
|
28
|
+
let source = "current_worktree";
|
|
29
|
+
let warning = "";
|
|
30
|
+
if (changeWorktree && path.resolve(changeWorktree) !== rootWorktree) {
|
|
31
|
+
const candidate = path.resolve(changeWorktree);
|
|
32
|
+
const candidateExists = await deps.pathExists(candidate);
|
|
33
|
+
const candidateReady = candidateExists ? await deps.isGitRepository(candidate) : false;
|
|
34
|
+
if (candidateReady) {
|
|
35
|
+
contextWorktree = candidate;
|
|
36
|
+
source = "change_worktree";
|
|
37
|
+
} else {
|
|
38
|
+
warning = `stale change worktree metadata: ${candidate} (run \`git worktree prune\`)`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const contextBranch = await deps.currentBranch(contextWorktree);
|
|
42
|
+
const inferred = deps.inferChangeIdFromBranch(contextBranch);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
worktree: contextWorktree,
|
|
46
|
+
branch: contextBranch,
|
|
47
|
+
branchMatchesChange: inferred === changeId,
|
|
48
|
+
source,
|
|
49
|
+
warning,
|
|
50
|
+
currentWorktree: rootWorktree,
|
|
51
|
+
currentBranch: rootBranch,
|
|
52
|
+
changeWorktree: changeWorktree ? path.resolve(changeWorktree) : "",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} changeDir
|
|
58
|
+
* @param {{ pathExists: (path: string) => Promise<boolean>, readText: (path: string) => Promise<string> }} deps
|
|
59
|
+
*/
|
|
60
|
+
export async function readChangeMetrics(changeDir, deps) {
|
|
61
|
+
const metricsAbs = path.join(changeDir, "metrics.json");
|
|
62
|
+
if (!(await deps.pathExists(metricsAbs))) return { state: "missing", abs: metricsAbs, events: [] };
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(await deps.readText(metricsAbs));
|
|
65
|
+
const events = Array.isArray(parsed?.events) ? parsed.events : [];
|
|
66
|
+
return { state: "ok", abs: metricsAbs, events };
|
|
67
|
+
} catch {
|
|
68
|
+
return { state: "invalid", abs: metricsAbs, events: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FINISH_DONE_COMPLETED_CLEANUPS = new Set([
|
|
73
|
+
"removed",
|
|
74
|
+
"pruned-missing",
|
|
75
|
+
"skipped:no_separate_change_worktree",
|
|
76
|
+
"skipped:current_worktree",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {any} ev
|
|
81
|
+
* @returns {{ state: string, reason: string, eventType: string } | null}
|
|
82
|
+
*/
|
|
83
|
+
function classifyFinishLifecycleEvent(ev) {
|
|
84
|
+
const type = String(ev?.type || "").trim();
|
|
85
|
+
if (!type) return null;
|
|
86
|
+
if (type === "finish_local") {
|
|
87
|
+
return { state: "local", reason: "", eventType: type };
|
|
88
|
+
}
|
|
89
|
+
if (type === "finish_failed") {
|
|
90
|
+
return { state: "failed", reason: String(ev?.payload?.error || ""), eventType: type };
|
|
91
|
+
}
|
|
92
|
+
if (type === "finish_cleanup_pending") {
|
|
93
|
+
return {
|
|
94
|
+
state: "cleanup_pending",
|
|
95
|
+
reason: String(ev?.payload?.reason || ev?.payload?.cleanup || ""),
|
|
96
|
+
eventType: type,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (type === "finish") {
|
|
100
|
+
return { state: "done", reason: "", eventType: type };
|
|
101
|
+
}
|
|
102
|
+
if (type !== "finish_done") return null;
|
|
103
|
+
|
|
104
|
+
const payload = ev?.payload && typeof ev.payload === "object" ? ev.payload : null;
|
|
105
|
+
const hasPush = payload ? Object.prototype.hasOwnProperty.call(payload, "push") : false;
|
|
106
|
+
const pushCompleted = hasPush ? payload?.push === true : null;
|
|
107
|
+
const cleanup = String(payload?.cleanup || "").trim();
|
|
108
|
+
|
|
109
|
+
if (pushCompleted === false || cleanup === "not_requested") {
|
|
110
|
+
return { state: "local", reason: "", eventType: type };
|
|
111
|
+
}
|
|
112
|
+
if (!cleanup) {
|
|
113
|
+
return { state: "done", reason: "", eventType: type };
|
|
114
|
+
}
|
|
115
|
+
if (FINISH_DONE_COMPLETED_CLEANUPS.has(cleanup)) {
|
|
116
|
+
return { state: "done", reason: "", eventType: type };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
state: "cleanup_pending",
|
|
121
|
+
reason: cleanup.startsWith("skipped:") ? cleanup.slice("skipped:".length) : cleanup,
|
|
122
|
+
eventType: type,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {{ state: string, events: any[] }} metrics
|
|
128
|
+
*/
|
|
129
|
+
export function summarizeLifecycleSignals(metrics) {
|
|
130
|
+
const events = Array.isArray(metrics?.events) ? metrics.events : [];
|
|
131
|
+
/** @type {Record<string, number>} */
|
|
132
|
+
const eventCounts = {};
|
|
133
|
+
let finishState = "";
|
|
134
|
+
let finishStateReason = "";
|
|
135
|
+
let finishStateEventType = "";
|
|
136
|
+
for (const ev of events) {
|
|
137
|
+
const type = String(ev?.type || "").trim();
|
|
138
|
+
if (!type) continue;
|
|
139
|
+
eventCounts[type] = (eventCounts[type] || 0) + 1;
|
|
140
|
+
const finishIsTerminal = finishState === "done";
|
|
141
|
+
if (finishIsTerminal && type !== "finish_done" && type !== "finish") continue;
|
|
142
|
+
const classified = classifyFinishLifecycleEvent(ev);
|
|
143
|
+
if (!classified) continue;
|
|
144
|
+
finishState = classified.state;
|
|
145
|
+
finishStateReason = classified.reason;
|
|
146
|
+
finishStateEventType = classified.eventType;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const latest = events.length > 0 ? events[events.length - 1] : null;
|
|
150
|
+
const validateEvents = events.filter((ev) => ev?.type === "validate");
|
|
151
|
+
const latestStrictValidate = [...validateEvents].reverse().find((ev) => ev?.payload?.strict === true) || null;
|
|
152
|
+
const latestStrictScopeValidate =
|
|
153
|
+
[...validateEvents].reverse().find((ev) => ev?.payload?.strict === true && ev?.payload?.check_scope === true) || null;
|
|
154
|
+
const strictScopeValidateRuns = validateEvents.filter((ev) => ev?.payload?.strict === true && ev?.payload?.check_scope === true).length;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
metricsState: String(metrics?.state || "missing"),
|
|
158
|
+
latestEventType: String(latest?.type || ""),
|
|
159
|
+
eventCounts,
|
|
160
|
+
evidenceRuns: eventCounts.evidence || 0,
|
|
161
|
+
finishRuns: (eventCounts.finish || 0) + (eventCounts.finish_done || 0),
|
|
162
|
+
finishCleanupPendingReason: finishState === "cleanup_pending" ? finishStateReason : "",
|
|
163
|
+
finishState,
|
|
164
|
+
finishStateReason,
|
|
165
|
+
finishStateEventType,
|
|
166
|
+
archiveRuns: eventCounts.archive || 0,
|
|
167
|
+
strictValidatePass: latestStrictValidate ? latestStrictValidate?.payload?.ok === true : false,
|
|
168
|
+
strictScopeValidateRuns,
|
|
169
|
+
strictScopeValidatePass: latestStrictScopeValidate ? latestStrictScopeValidate?.payload?.ok === true : false,
|
|
170
|
+
strictScopeValidateAt: String(latestStrictScopeValidate?.timestamp || ""),
|
|
171
|
+
strictScopeValidateErrors: Number(latestStrictScopeValidate?.payload?.errors || 0),
|
|
172
|
+
strictScopeValidateWarnings: Number(latestStrictScopeValidate?.payload?.warnings || 0),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {Record<string, string | null>} curTruth
|
|
178
|
+
* @param {any} baseline
|
|
179
|
+
* @returns {{ baselineLabel: string, baselineAt: string, driftFiles: string[] }}
|
|
180
|
+
*/
|
|
181
|
+
export function truthDrift(curTruth, baseline) {
|
|
182
|
+
/** @type {string[]} */
|
|
183
|
+
const driftFiles = [];
|
|
184
|
+
|
|
185
|
+
const createdTruth = baseline?.base_truth_files && typeof baseline.base_truth_files === "object" ? baseline.base_truth_files : {};
|
|
186
|
+
const syncedTruth = baseline?.synced_truth_files && typeof baseline.synced_truth_files === "object" ? baseline.synced_truth_files : {};
|
|
187
|
+
const effective = Object.keys(syncedTruth).length > 0 ? syncedTruth : createdTruth;
|
|
188
|
+
const baselineLabel = Object.keys(syncedTruth).length > 0 ? "sync" : "creation";
|
|
189
|
+
const baselineAt = Object.keys(syncedTruth).length > 0 ? String(baseline?.synced_at || "") : String(baseline?.created_at || "");
|
|
190
|
+
|
|
191
|
+
for (const [rel, info] of Object.entries(effective || {})) {
|
|
192
|
+
const baseSha = info && typeof info === "object" ? String(info.sha256 || "") : "";
|
|
193
|
+
const curSha = curTruth[rel] ?? null;
|
|
194
|
+
if (curSha == null) {
|
|
195
|
+
driftFiles.push(rel);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (baseSha && curSha !== baseSha) driftFiles.push(rel);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { baselineLabel, baselineAt, driftFiles };
|
|
202
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{
|
|
3
|
+
* gitRoot: string,
|
|
4
|
+
* changeId: string,
|
|
5
|
+
* options: { strict: boolean, allowTruthDrift: boolean, checkEvidence: boolean, checkScope: boolean }
|
|
6
|
+
* }} input
|
|
7
|
+
* @param {{
|
|
8
|
+
* appendMetricsEvent: (gitRoot: string, changeId: string, type: string, payload: any) => Promise<void>,
|
|
9
|
+
* validateChangeArtifacts: (gitRoot: string, changeId: string, options: any) => Promise<any>,
|
|
10
|
+
* UserError: typeof import("../errors.js").UserError
|
|
11
|
+
* }} deps
|
|
12
|
+
*/
|
|
13
|
+
export async function runChangeValidateCommand(input, deps) {
|
|
14
|
+
const { gitRoot, changeId, options } = input;
|
|
15
|
+
const result = await deps.validateChangeArtifacts(gitRoot, changeId, {
|
|
16
|
+
strict: options.strict === true,
|
|
17
|
+
allowTruthDrift: options.allowTruthDrift === true,
|
|
18
|
+
checkEvidence: options.checkEvidence === true,
|
|
19
|
+
checkScope: options.checkScope === true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await deps.appendMetricsEvent(gitRoot, changeId, "validate", {
|
|
23
|
+
ok: result.ok === true,
|
|
24
|
+
strict: options.strict === true,
|
|
25
|
+
allow_truth_drift: options.allowTruthDrift === true,
|
|
26
|
+
check_evidence: options.checkEvidence === true,
|
|
27
|
+
check_scope: options.checkScope === true,
|
|
28
|
+
errors: Array.isArray(result.errors) ? result.errors.length : 0,
|
|
29
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.length : 0,
|
|
30
|
+
exit_code: result.exitCode,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result.raw) {
|
|
34
|
+
return {
|
|
35
|
+
output: result.ok ? `ok: change validated (${changeId})\n` : "",
|
|
36
|
+
stderr: result.raw + "\n",
|
|
37
|
+
errorToThrow: result.ok ? null : new deps.UserError(""),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
output: result.ok ? `ok: change validated (${changeId})\n` : "",
|
|
42
|
+
stderr: "",
|
|
43
|
+
errorToThrow: result.ok ? null : new deps.UserError(""),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
function parseChangeCheckerOutput(stdout, stderr) {
|
|
2
|
+
const text = [String(stdout || ""), String(stderr || "")].filter(Boolean).join("\n");
|
|
3
|
+
/** @type {string[]} */
|
|
4
|
+
const errors = [];
|
|
5
|
+
/** @type {string[]} */
|
|
6
|
+
const warnings = [];
|
|
7
|
+
for (const raw of text.split("\n")) {
|
|
8
|
+
const line = raw.trim();
|
|
9
|
+
if (!line) continue;
|
|
10
|
+
if (line.startsWith("error: ")) errors.push(line.replace(/^error:\s*/, ""));
|
|
11
|
+
else if (line.startsWith("warn: ")) warnings.push(line.replace(/^warn:\s*/, ""));
|
|
12
|
+
}
|
|
13
|
+
return { errors, warnings, raw: text.trim() };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function classifyCheckMessage(msg) {
|
|
17
|
+
const s = String(msg || "");
|
|
18
|
+
if (s.includes("truth file") || s.includes("truth drift") || s.includes("allow-truth-drift")) return "truth_drift";
|
|
19
|
+
if (s.includes("WS:TODO") || s.includes("unrendered template placeholders")) return "placeholders";
|
|
20
|
+
if (s.includes("missing:") || s.includes("empty:") || s.includes("Missing change dir")) return "missing_files";
|
|
21
|
+
if (
|
|
22
|
+
s.includes("Change_ID") ||
|
|
23
|
+
s.includes("Req_ID") ||
|
|
24
|
+
s.includes("Problem_ID") ||
|
|
25
|
+
s.includes("Contract_Row") ||
|
|
26
|
+
s.includes("Plan_File") ||
|
|
27
|
+
s.includes("Evidence_Path")
|
|
28
|
+
) {
|
|
29
|
+
return "bindings";
|
|
30
|
+
}
|
|
31
|
+
if (s.includes("scope check") || s.includes("out-of-scope")) return "scope_gate";
|
|
32
|
+
if (
|
|
33
|
+
s.includes("finish gate") ||
|
|
34
|
+
s.includes("validate stamp") ||
|
|
35
|
+
s.includes("verify-before-complete") ||
|
|
36
|
+
s.includes("spec review") ||
|
|
37
|
+
s.includes("quality review")
|
|
38
|
+
) {
|
|
39
|
+
return "finish_gate";
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
s.includes("missing required sections") ||
|
|
43
|
+
s.includes("has empty sections") ||
|
|
44
|
+
s.includes("Plan section") ||
|
|
45
|
+
s.includes("Verify section") ||
|
|
46
|
+
s.includes("scope is too broad") ||
|
|
47
|
+
s.includes("too abstract")
|
|
48
|
+
) {
|
|
49
|
+
return "plan_quality";
|
|
50
|
+
}
|
|
51
|
+
return "other";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function groupCheckMessages(errors, warnings) {
|
|
55
|
+
const groups = {
|
|
56
|
+
truth_drift: { errors: [], warnings: [] },
|
|
57
|
+
missing_files: { errors: [], warnings: [] },
|
|
58
|
+
placeholders: { errors: [], warnings: [] },
|
|
59
|
+
bindings: { errors: [], warnings: [] },
|
|
60
|
+
scope_gate: { errors: [], warnings: [] },
|
|
61
|
+
finish_gate: { errors: [], warnings: [] },
|
|
62
|
+
plan_quality: { errors: [], warnings: [] },
|
|
63
|
+
other: { errors: [], warnings: [] },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const message of errors || []) {
|
|
67
|
+
const group = classifyCheckMessage(message);
|
|
68
|
+
groups[group].errors.push(message);
|
|
69
|
+
}
|
|
70
|
+
for (const message of warnings || []) {
|
|
71
|
+
const group = classifyCheckMessage(message);
|
|
72
|
+
groups[group].warnings.push(message);
|
|
73
|
+
}
|
|
74
|
+
return groups;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} gitRoot
|
|
79
|
+
* @param {string} changeId
|
|
80
|
+
* @param {{ strict: boolean, allowTruthDrift: boolean, checkEvidence?: boolean, checkScope?: boolean }} options
|
|
81
|
+
* @param {{
|
|
82
|
+
* assertValidChangeId: (changeId: string) => void,
|
|
83
|
+
* collectFinishGateErrors: (changeId: string, reviewGates: any) => string[],
|
|
84
|
+
* computeChangeStatus: (gitRoot: string, changeId: string) => Promise<any>,
|
|
85
|
+
* ensureTruthFiles: (gitRoot: string) => Promise<void>,
|
|
86
|
+
* resolveWsChangeChecker: (gitRoot: string) => Promise<{ args: string[] }>,
|
|
87
|
+
* runPython: (cwd: string, args: string[]) => Promise<{ code: number, stdout: string, stderr: string }>
|
|
88
|
+
* }} deps
|
|
89
|
+
*/
|
|
90
|
+
export async function validateChangeArtifacts(gitRoot, changeId, options, deps) {
|
|
91
|
+
deps.assertValidChangeId(changeId);
|
|
92
|
+
await deps.ensureTruthFiles(gitRoot);
|
|
93
|
+
|
|
94
|
+
const checker = await deps.resolveWsChangeChecker(gitRoot);
|
|
95
|
+
const args = [...checker.args, "--workspace-root", gitRoot, "--change-id", changeId];
|
|
96
|
+
if (options.strict) args.push("--strict");
|
|
97
|
+
if (options.allowTruthDrift) args.push("--allow-truth-drift");
|
|
98
|
+
if (options.checkEvidence) args.push("--check-evidence");
|
|
99
|
+
if (options.checkScope) args.push("--check-scope");
|
|
100
|
+
|
|
101
|
+
const res = await deps.runPython(gitRoot, ["-u", ...args]);
|
|
102
|
+
const parsed = parseChangeCheckerOutput(res.stdout, res.stderr);
|
|
103
|
+
const errors = [...parsed.errors];
|
|
104
|
+
const warnings = [...parsed.warnings];
|
|
105
|
+
|
|
106
|
+
if (options.checkEvidence === true) {
|
|
107
|
+
const status = await deps.computeChangeStatus(gitRoot, changeId);
|
|
108
|
+
errors.push(...deps.collectFinishGateErrors(changeId, status.reviewGates));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rawLines = [];
|
|
112
|
+
if (parsed.raw) rawLines.push(parsed.raw);
|
|
113
|
+
for (const message of errors.slice(parsed.errors.length)) rawLines.push(`error: ${message}`);
|
|
114
|
+
for (const message of warnings.slice(parsed.warnings.length)) rawLines.push(`warn: ${message}`);
|
|
115
|
+
const exitCode = res.code === 0 && errors.length > 0 ? 1 : res.code;
|
|
116
|
+
return {
|
|
117
|
+
ok: exitCode === 0,
|
|
118
|
+
changeId,
|
|
119
|
+
strict: options.strict === true,
|
|
120
|
+
allowTruthDrift: options.allowTruthDrift === true,
|
|
121
|
+
exitCode,
|
|
122
|
+
errors,
|
|
123
|
+
warnings,
|
|
124
|
+
groups: groupCheckMessages(errors, warnings),
|
|
125
|
+
raw: rawLines.filter(Boolean).join("\n").trim(),
|
|
126
|
+
};
|
|
127
|
+
}
|