@jevitate/cli 0.8.0 → 0.10.0
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/dist/anchor-baselines.d.ts +26 -0
- package/dist/anchor-baselines.js +25 -0
- package/dist/anchor-baselines.js.map +1 -0
- package/dist/approval-provenance.d.ts +48 -2
- package/dist/approval-provenance.js +110 -6
- package/dist/approval-provenance.js.map +1 -1
- package/dist/bin.js +25111 -17835
- package/dist/browser-cli.d.ts +10 -0
- package/dist/browser-cli.js +50 -0
- package/dist/browser-cli.js.map +1 -0
- package/dist/build-info.generated.js +2 -2
- package/dist/catalog-analysis.js +14 -0
- package/dist/catalog-analysis.js.map +1 -1
- package/dist/catalog-api.js +12 -4
- package/dist/catalog-api.js.map +1 -1
- package/dist/catalog-bundle-api.d.ts +73 -0
- package/dist/catalog-bundle-api.js +206 -0
- package/dist/catalog-bundle-api.js.map +1 -0
- package/dist/catalog-bundle-findings.d.ts +54 -0
- package/dist/catalog-bundle-findings.js +252 -0
- package/dist/catalog-bundle-findings.js.map +1 -0
- package/dist/catalog-bundle.d.ts +192 -0
- package/dist/catalog-bundle.js +578 -0
- package/dist/catalog-bundle.js.map +1 -0
- package/dist/catalog-cli.d.ts +6 -0
- package/dist/catalog-cli.js +180 -5
- package/dist/catalog-cli.js.map +1 -1
- package/dist/catalog-refs.d.ts +42 -0
- package/dist/catalog-refs.js +188 -0
- package/dist/catalog-refs.js.map +1 -0
- package/dist/catalog-review.d.ts +3 -1
- package/dist/catalog-review.js +61 -0
- package/dist/catalog-review.js.map +1 -1
- package/dist/catalog.d.ts +27 -3
- package/dist/catalog.js +46 -2
- package/dist/catalog.js.map +1 -1
- package/dist/change-context.d.ts +44 -0
- package/dist/change-context.js +203 -0
- package/dist/change-context.js.map +1 -0
- package/dist/check-api.d.ts +1 -1
- package/dist/check-api.js +206 -16
- package/dist/check-api.js.map +1 -1
- package/dist/check-cli.d.ts +2 -1
- package/dist/check-cli.js +117 -8
- package/dist/check-cli.js.map +1 -1
- package/dist/check-execute.d.ts +28 -1
- package/dist/check-execute.js +110 -27
- package/dist/check-execute.js.map +1 -1
- package/dist/check-plan.d.ts +14 -0
- package/dist/check-plan.js +24 -1
- package/dist/check-plan.js.map +1 -1
- package/dist/check-types.d.ts +137 -5
- package/dist/check-types.js +8 -0
- package/dist/check-types.js.map +1 -1
- package/dist/cli-output.d.ts +6 -0
- package/dist/cli-output.js +26 -1
- package/dist/cli-output.js.map +1 -1
- package/dist/cli-shared.d.ts +10 -0
- package/dist/cli-shared.js +0 -4
- package/dist/cli-shared.js.map +1 -1
- package/dist/exit-codes.d.ts +6 -3
- package/dist/exit-codes.js +24 -3
- package/dist/exit-codes.js.map +1 -1
- package/dist/explore-adversarial.js +2 -2
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-cli.js +3 -2
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-coverage.js +3 -3
- package/dist/explore-coverage.js.map +1 -1
- package/dist/explore-feature.js +3 -3
- package/dist/explore-feature.js.map +1 -1
- package/dist/explore-goal.js +11 -5
- package/dist/explore-goal.js.map +1 -1
- package/dist/findings-filing.d.ts +6 -0
- package/dist/findings-filing.js +15 -1
- package/dist/findings-filing.js.map +1 -1
- package/dist/forge-verify.d.ts +224 -0
- package/dist/forge-verify.js +649 -0
- package/dist/forge-verify.js.map +1 -0
- package/dist/heal-probe-guard.d.ts +9 -0
- package/dist/heal-probe-guard.js +113 -0
- package/dist/heal-probe-guard.js.map +1 -0
- package/dist/host-health-run.d.ts +32 -1
- package/dist/host-health-run.js +38 -1
- package/dist/host-health-run.js.map +1 -1
- package/dist/init-codeowners.d.ts +19 -0
- package/dist/init-codeowners.js +66 -0
- package/dist/init-codeowners.js.map +1 -1
- package/dist/job-draft-api.d.ts +52 -0
- package/dist/job-draft-api.js +110 -0
- package/dist/job-draft-api.js.map +1 -0
- package/dist/journeeze-api.d.ts +71 -0
- package/dist/journeeze-api.js +285 -0
- package/dist/journeeze-api.js.map +1 -0
- package/dist/journeeze-connect.d.ts +156 -0
- package/dist/journeeze-connect.js +357 -0
- package/dist/journeeze-connect.js.map +1 -0
- package/dist/journey-anchor-gate.d.ts +52 -0
- package/dist/journey-anchor-gate.js +101 -0
- package/dist/journey-anchor-gate.js.map +1 -0
- package/dist/journey-api.d.ts +43 -3
- package/dist/journey-api.js +226 -21
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli.js +162 -20
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-heal.d.ts +74 -0
- package/dist/journey-heal.js +180 -0
- package/dist/journey-heal.js.map +1 -0
- package/dist/journey-lint-sarif.d.ts +3 -2
- package/dist/journey-lint-sarif.js +9 -2
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-migrate-api.d.ts +54 -0
- package/dist/journey-migrate-api.js +64 -0
- package/dist/journey-migrate-api.js.map +1 -0
- package/dist/journey-network-checks.d.ts +5 -1
- package/dist/journey-network-checks.js +15 -4
- package/dist/journey-network-checks.js.map +1 -1
- package/dist/journey-prefix.js +2 -2
- package/dist/journey-prefix.js.map +1 -1
- package/dist/journey-proposal-store.d.ts +109 -0
- package/dist/journey-proposal-store.js +365 -0
- package/dist/journey-proposal-store.js.map +1 -0
- package/dist/journey-result-record.d.ts +42 -0
- package/dist/journey-result-record.js +33 -0
- package/dist/journey-result-record.js.map +1 -0
- package/dist/journey-review-api.js +16 -1
- package/dist/journey-review-api.js.map +1 -1
- package/dist/journey-review.d.ts +18 -1
- package/dist/journey-review.js +116 -5
- package/dist/journey-review.js.map +1 -1
- package/dist/journey-stale-api.d.ts +40 -0
- package/dist/journey-stale-api.js +57 -0
- package/dist/journey-stale-api.js.map +1 -0
- package/dist/launch-armed.js +5 -1
- package/dist/launch-armed.js.map +1 -1
- package/dist/locator-health-api.d.ts +101 -0
- package/dist/locator-health-api.js +203 -0
- package/dist/locator-health-api.js.map +1 -0
- package/dist/locator-health.d.ts +195 -0
- package/dist/locator-health.js +419 -0
- package/dist/locator-health.js.map +1 -0
- package/dist/login-cli.d.ts +2 -0
- package/dist/login-cli.js +56 -11
- package/dist/login-cli.js.map +1 -1
- package/dist/mcp-api.d.ts +5 -1
- package/dist/mcp-api.js +53 -8
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-cli-tools.d.ts +3 -1
- package/dist/mcp-cli-tools.js +80 -10
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/multi-run.d.ts +7 -6
- package/dist/multi-run.js +5 -4
- package/dist/multi-run.js.map +1 -1
- package/dist/persona-login.d.ts +60 -3
- package/dist/persona-login.js +341 -31
- package/dist/persona-login.js.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/project-config.d.ts +29 -0
- package/dist/project-config.js +76 -0
- package/dist/project-config.js.map +1 -0
- package/dist/report-api.d.ts +1 -0
- package/dist/report-api.js +1 -0
- package/dist/report-api.js.map +1 -1
- package/dist/resource-preflight.js +5 -3
- package/dist/resource-preflight.js.map +1 -1
- package/dist/run-metadata.d.ts +5 -2
- package/dist/run-metadata.js +10 -3
- package/dist/run-metadata.js.map +1 -1
- package/dist/run-tags-cli.d.ts +2 -0
- package/dist/run-tags-cli.js +8 -2
- package/dist/run-tags-cli.js.map +1 -1
- package/dist/self-heal-adapter.d.ts +43 -27
- package/dist/self-heal-adapter.js +196 -62
- package/dist/self-heal-adapter.js.map +1 -1
- package/dist/source-cli.js +5 -3
- package/dist/source-cli.js.map +1 -1
- package/dist/suite-explore-options.js +1 -0
- package/dist/suite-explore-options.js.map +1 -1
- package/dist/sweep-api.d.ts +23 -0
- package/dist/sweep-api.js +39 -8
- package/dist/sweep-api.js.map +1 -1
- package/dist/sweep-cli.js +3 -1
- package/dist/sweep-cli.js.map +1 -1
- package/dist/ux-usability.js +3 -2
- package/dist/ux-usability.js.map +1 -1
- package/package.json +17 -15
- package/skills/jevitate-ci-check/SKILL.md +25 -1
- package/skills/jevitate-getting-started/SKILL.md +9 -3
- package/skills/jevitate-mission-scope/SKILL.md +2 -2
- package/skills/jevitate-run-journey/SKILL.md +36 -5
- package/skills/jevitate-test-campaign/SKILL.md +13 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #453 change-aware self-heal: read a git range read-only and build a `ChangeScope`.
|
|
3
|
+
* The only IO is `git rev-parse` / `git merge-base` / `git diff`, always via an argv array, never a
|
|
4
|
+
* shell, and only resolved 40-hex SHAs ever reach `diff`. Raw diff text leaves only as `ChangeEvidence`.
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { extractChangeEvidence, isSkippedChangePath } from "@jevitate/runtime";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
11
|
+
const DEFAULT_MAX_FILES = 400;
|
|
12
|
+
const DEFAULT_MAX_BYTES = 4_000_000;
|
|
13
|
+
const MAX_NOTES = 20;
|
|
14
|
+
const MAX_NOTE_LENGTH = 2000;
|
|
15
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
16
|
+
// The whole range must be ref-safe characters; the separator, if any, is exactly `..` or `...`.
|
|
17
|
+
const RANGE_RE = /^([A-Za-z0-9._\/~^@{}-]{1,200})(?:(\.\.\.?)([A-Za-z0-9._\/~^@{}-]{1,200}))?$/;
|
|
18
|
+
/** The caller supplied a syntactically invalid range or note set. */
|
|
19
|
+
export class ChangesArgsError extends Error {
|
|
20
|
+
code = "E_CHANGES_ARGS";
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ChangesArgsError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Git could not resolve the range, or the diff is too large to surface. */
|
|
27
|
+
export class ChangesInputError extends Error {
|
|
28
|
+
code = "E_CHANGES_INPUT";
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ChangesInputError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The production git runner: argv only, no shell, optional locks and prompts disabled. */
|
|
35
|
+
export const execGitReadOnly = async (args, opts) => {
|
|
36
|
+
const result = await execFileAsync("git", args, {
|
|
37
|
+
cwd: opts.cwd,
|
|
38
|
+
env: { ...opts.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
|
|
39
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
40
|
+
});
|
|
41
|
+
return { stdout: result.stdout };
|
|
42
|
+
};
|
|
43
|
+
/** Parse `a..b`, `a...b` (symmetric) or a single `X` (meaning `X..HEAD`) into its resolved ends. */
|
|
44
|
+
export function parseChangeRange(s) {
|
|
45
|
+
if (typeof s !== "string" || !RANGE_RE.test(s))
|
|
46
|
+
throw new ChangesArgsError(`invalid change range: ${JSON.stringify(s)}`);
|
|
47
|
+
const triple = s.indexOf("...");
|
|
48
|
+
const double = triple >= 0 ? -1 : s.indexOf("..");
|
|
49
|
+
let from;
|
|
50
|
+
let to;
|
|
51
|
+
let symmetric;
|
|
52
|
+
if (triple >= 0) {
|
|
53
|
+
from = s.slice(0, triple);
|
|
54
|
+
to = s.slice(triple + 3);
|
|
55
|
+
symmetric = true;
|
|
56
|
+
}
|
|
57
|
+
else if (double >= 0) {
|
|
58
|
+
from = s.slice(0, double);
|
|
59
|
+
to = s.slice(double + 2);
|
|
60
|
+
symmetric = false;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
from = s;
|
|
64
|
+
to = "HEAD";
|
|
65
|
+
symmetric = false;
|
|
66
|
+
}
|
|
67
|
+
for (const side of [from, to]) {
|
|
68
|
+
if (side === "" || side.startsWith("-") || side.includes("..") || side.includes("@{-")) {
|
|
69
|
+
throw new ChangesArgsError(`unsafe change range: ${JSON.stringify(s)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { from, to, symmetric };
|
|
73
|
+
}
|
|
74
|
+
/** Validate human change notes: at most 20, each 1-2000 chars, no NUL. Returns a copy. */
|
|
75
|
+
export function validateChangeNotes(notes) {
|
|
76
|
+
if (notes.length > MAX_NOTES)
|
|
77
|
+
throw new ChangesArgsError(`at most ${MAX_NOTES} change notes are allowed`);
|
|
78
|
+
for (const note of notes) {
|
|
79
|
+
if (note.length < 1 || note.length > MAX_NOTE_LENGTH || note.includes("\u0000")) {
|
|
80
|
+
throw new ChangesArgsError(`each change note must be 1-${MAX_NOTE_LENGTH} characters and contain no NUL`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...notes];
|
|
84
|
+
}
|
|
85
|
+
/** Read a git range (and/or notes) and build the `ChangeScope` the healer consumes. */
|
|
86
|
+
export async function readChangeScope(o) {
|
|
87
|
+
const exec = o.exec ?? execGitReadOnly;
|
|
88
|
+
const notes = validateChangeNotes(o.notes);
|
|
89
|
+
const maxFiles = o.limits?.maxFiles ?? DEFAULT_MAX_FILES;
|
|
90
|
+
const maxBytes = o.limits?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
91
|
+
if (o.range === undefined) {
|
|
92
|
+
return { evidence: extractChangeEvidence("", notes), scanned: { files: 0, hunks: 0, skipped: [] } };
|
|
93
|
+
}
|
|
94
|
+
const { from, to, symmetric } = parseChangeRange(o.range);
|
|
95
|
+
const fromSha = await resolveCommit(exec, o.cwd, from);
|
|
96
|
+
const toSha = await resolveCommit(exec, o.cwd, to);
|
|
97
|
+
const baseSha = symmetric ? await mergeBase(exec, o.cwd, fromSha, toSha) : fromSha;
|
|
98
|
+
const headSha = toSha;
|
|
99
|
+
assertSha(baseSha);
|
|
100
|
+
assertSha(headSha);
|
|
101
|
+
const diffArgs = [
|
|
102
|
+
"-c",
|
|
103
|
+
"core.fsmonitor=",
|
|
104
|
+
"-c",
|
|
105
|
+
"diff.external=",
|
|
106
|
+
"diff",
|
|
107
|
+
"--no-ext-diff",
|
|
108
|
+
"--no-textconv",
|
|
109
|
+
"--no-color",
|
|
110
|
+
"-M",
|
|
111
|
+
"--unified=0",
|
|
112
|
+
`${baseSha}..${headSha}`,
|
|
113
|
+
"--",
|
|
114
|
+
];
|
|
115
|
+
let stdout;
|
|
116
|
+
try {
|
|
117
|
+
({ stdout } = await exec(diffArgs, { cwd: o.cwd, env: process.env }));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new ChangesInputError(`git diff failed: ${messageOf(error)}`);
|
|
121
|
+
}
|
|
122
|
+
if (Buffer.byteLength(stdout, "utf8") > maxBytes) {
|
|
123
|
+
throw new ChangesInputError(`change diff exceeds ${maxBytes} bytes`);
|
|
124
|
+
}
|
|
125
|
+
const files = countDiffSections(stdout);
|
|
126
|
+
if (files > maxFiles) {
|
|
127
|
+
throw new ChangesInputError(`change diff touches ${files} files (max ${maxFiles})`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
range: o.range,
|
|
131
|
+
baseSha,
|
|
132
|
+
headSha,
|
|
133
|
+
evidence: extractChangeEvidence(stdout, notes),
|
|
134
|
+
scanned: { files, hunks: countHunks(stdout), skipped: skippedPaths(stdout) },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function resolveCommit(exec, cwd, rev) {
|
|
138
|
+
const args = ["rev-parse", "--verify", "--quiet", "--end-of-options", `${rev}^{commit}`];
|
|
139
|
+
let stdout;
|
|
140
|
+
try {
|
|
141
|
+
({ stdout } = await exec(args, { cwd, env: process.env }));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new ChangesInputError(`cannot resolve change revision ${JSON.stringify(rev)}: ${messageOf(error)}`);
|
|
145
|
+
}
|
|
146
|
+
const sha = stdout.trim();
|
|
147
|
+
if (!SHA_RE.test(sha))
|
|
148
|
+
throw new ChangesInputError(`cannot resolve change revision ${JSON.stringify(rev)}`);
|
|
149
|
+
return sha;
|
|
150
|
+
}
|
|
151
|
+
async function mergeBase(exec, cwd, shaA, shaB) {
|
|
152
|
+
let stdout;
|
|
153
|
+
try {
|
|
154
|
+
({ stdout } = await exec(["merge-base", shaA, shaB], { cwd, env: process.env }));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new ChangesInputError(`cannot find merge base: ${messageOf(error)}`);
|
|
158
|
+
}
|
|
159
|
+
const sha = stdout.trim();
|
|
160
|
+
if (!SHA_RE.test(sha))
|
|
161
|
+
throw new ChangesInputError(`cannot find merge base for ${shaA}...${shaB}`);
|
|
162
|
+
return sha;
|
|
163
|
+
}
|
|
164
|
+
function assertSha(sha) {
|
|
165
|
+
if (!SHA_RE.test(sha))
|
|
166
|
+
throw new ChangesInputError(`resolved revision is not a commit SHA: ${JSON.stringify(sha)}`);
|
|
167
|
+
}
|
|
168
|
+
function countDiffSections(diff) {
|
|
169
|
+
let count = 0;
|
|
170
|
+
for (const line of diff.split(/\r?\n/))
|
|
171
|
+
if (line.startsWith("diff --git "))
|
|
172
|
+
count++;
|
|
173
|
+
return count;
|
|
174
|
+
}
|
|
175
|
+
function countHunks(diff) {
|
|
176
|
+
let count = 0;
|
|
177
|
+
for (const line of diff.split(/\r?\n/))
|
|
178
|
+
if (line.startsWith("@@ "))
|
|
179
|
+
count++;
|
|
180
|
+
return count;
|
|
181
|
+
}
|
|
182
|
+
function skippedPaths(diff) {
|
|
183
|
+
const paths = [];
|
|
184
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
185
|
+
const header = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
186
|
+
if (!header)
|
|
187
|
+
continue;
|
|
188
|
+
const newPath = unquotePath(header[2]);
|
|
189
|
+
if (isSkippedChangePath(newPath))
|
|
190
|
+
paths.push(newPath);
|
|
191
|
+
}
|
|
192
|
+
return paths;
|
|
193
|
+
}
|
|
194
|
+
function unquotePath(value) {
|
|
195
|
+
const trimmed = value.trim();
|
|
196
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
197
|
+
return trimmed.slice(1, -1);
|
|
198
|
+
return trimmed;
|
|
199
|
+
}
|
|
200
|
+
function messageOf(error) {
|
|
201
|
+
return error instanceof Error ? error.message : String(error);
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=change-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-context.js","sourceRoot":"","sources":["../src/change-context.ts"],"names":[],"mappings":"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"}
|
package/dist/check-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CheckResult, type RunCheckOptions } from "./check-types.js";
|
|
2
2
|
export { affectedBy } from "./check-plan.js";
|
|
3
3
|
export { BudgetMeter } from "./check-budget.js";
|
|
4
|
-
export { type BudgetReport, CheckAiSetupError, type CheckFinding, type CheckGateways, type CheckItemReport, CheckPreflightError, type CheckResult, type CheckRunners, type ItemKind, type RunCheckOptions } from "./check-types.js";
|
|
4
|
+
export { type BudgetReport, CheckAiSetupError, CheckArgsError, type CheckFinding, type CheckGateways, type CheckItemReport, CheckPreflightError, type CheckResult, type CheckRunners, type ItemKind, type RunCheckOptions } from "./check-types.js";
|
|
5
5
|
export declare function runCheck(opts: RunCheckOptions): Promise<CheckResult>;
|
package/dist/check-api.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { aggregateOf, formatUsageLine } from "@jevitate/ai-core";
|
|
4
5
|
import { consolidate, diffRuns, findingKey, renderJUnit, renderReportMarkdown, renderSarif } from "@jevitate/findings";
|
|
5
|
-
import {
|
|
6
|
+
import { approvalsReportVerified } from "./approval-provenance.js";
|
|
6
7
|
import { loadCatalog } from "./catalog-api.js";
|
|
7
8
|
import { runAdversarialCliMission, runCoverageMission, runExploration, runFeatureCliMission } from "./explore-api.js";
|
|
8
9
|
import { runJourneyProgrammatically } from "./journey-api.js";
|
|
@@ -10,14 +11,18 @@ import { runUsabilityMission } from "./ux-api.js";
|
|
|
10
11
|
import { runVerifyFix } from "./verify-fix-api.js";
|
|
11
12
|
import { currentEngineInfo } from "./engine.js";
|
|
12
13
|
import { loadRunFile, resolveBaseline, scanRuns, summarizeRun } from "./report-api.js";
|
|
13
|
-
import { CheckAiSetupError } from "./check-types.js";
|
|
14
|
+
import { CheckAiSetupError, CheckArgsError, CheckPreflightError } from "./check-types.js";
|
|
15
|
+
import { brittleStepGate, testIdAttributesOf, testIdAttributesOrDefault } from "./locator-health-api.js";
|
|
16
|
+
import { combineHealth, compareLocatorHealth, exceedsBrittleSteps, trendLine } from "./locator-health.js";
|
|
17
|
+
import { findProjectDir } from "./project-dir.js";
|
|
14
18
|
import { BudgetMeter } from "./check-budget.js";
|
|
15
|
-
import { errorMessage, plan, prepareTarget } from "./check-plan.js";
|
|
19
|
+
import { errorMessage, plan, prepareSelfHeal, prepareTarget } from "./check-plan.js";
|
|
20
|
+
import { makeEvidenceSelfHealer } from "./self-heal-adapter.js";
|
|
16
21
|
import { caseDetail, execute } from "./check-execute.js";
|
|
17
22
|
import { clock } from "@jevitate/domain";
|
|
18
23
|
export { affectedBy } from "./check-plan.js";
|
|
19
24
|
export { BudgetMeter } from "./check-budget.js";
|
|
20
|
-
export { CheckAiSetupError, CheckPreflightError } from "./check-types.js";
|
|
25
|
+
export { CheckAiSetupError, CheckArgsError, CheckPreflightError } from "./check-types.js";
|
|
21
26
|
const REAL_RUNNERS = {
|
|
22
27
|
journey: runJourneyProgrammatically,
|
|
23
28
|
goal: runExploration,
|
|
@@ -33,7 +38,8 @@ async function approvalDefects(opts, req) {
|
|
|
33
38
|
const catalogs = [];
|
|
34
39
|
for (const d of dirs)
|
|
35
40
|
catalogs.push(await loadCatalog(req.catalogDir, d));
|
|
36
|
-
|
|
41
|
+
// #469: a recorded pr-review approval counts only when the forge re-confirms it.
|
|
42
|
+
const report = await approvalsReportVerified(catalogs, req.allowedChannels, { catalogDir: req.catalogDir, ...(opts.approvalVerification ?? {}) });
|
|
37
43
|
const defects = (report.requirement?.violations ?? []).map((v) => {
|
|
38
44
|
const identity = { category: "approval", signal: `approval-${v.problem}`, control: `${v.kind} ${v.id}` };
|
|
39
45
|
const key = findingKey(identity);
|
|
@@ -41,6 +47,90 @@ async function approvalDefects(opts, req) {
|
|
|
41
47
|
});
|
|
42
48
|
return { defects };
|
|
43
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* #470: the project's test-id convention for a check's Journey items. With the opt-in gate a malformed
|
|
52
|
+
* project config refuses the check (it would gate on a guessed convention); without it the defaults
|
|
53
|
+
* apply, as a run's result.json does.
|
|
54
|
+
*/
|
|
55
|
+
function checkTestIdAttributes(opts) {
|
|
56
|
+
const projectDir = opts.projectDir === undefined ? findProjectDir() : opts.projectDir;
|
|
57
|
+
if (opts.maxBrittleSteps === undefined)
|
|
58
|
+
return testIdAttributesOrDefault(projectDir);
|
|
59
|
+
try {
|
|
60
|
+
return testIdAttributesOf(projectDir);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new CheckPreflightError(`--max-brittle-steps needs the project's testIdAttributes convention: ${errorMessage(e)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** #470: the gate's finding for one Journey item past `--max-brittle-steps` (hard: it fails the check). */
|
|
67
|
+
function brittleDefect(journeyId, health, maxBrittleSteps) {
|
|
68
|
+
const identity = { category: "locator-health", signal: "brittle-steps", control: `journey ${journeyId}` };
|
|
69
|
+
const key = findingKey(identity);
|
|
70
|
+
const title = `Journey '${journeyId}': ${health.brittle} brittle locator step(s), more than --max-brittle-steps ${maxBrittleSteps}${health.line === undefined ? "" : ` (${health.line})`}`;
|
|
71
|
+
return { key, keys: [key], identity, category: "locator-health", severity: "hard", title, reproduce: `jevitate locator-health --journey ${journeyId}`, fingerprints: [], modes: [], occurrences: 1, runCount: 0, evidence: [], intermittent: false };
|
|
72
|
+
}
|
|
73
|
+
/** #470: the locator health a baseline run's result.json recorded, per Journey (the latest run of each wins). */
|
|
74
|
+
function baselineLocatorHealths(runs) {
|
|
75
|
+
const out = new Map();
|
|
76
|
+
for (const run of runs) {
|
|
77
|
+
if (run.mode !== "journey")
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const json = JSON.parse(readFileSync(run.path, "utf8"));
|
|
81
|
+
const h = json.result?.locatorHealth;
|
|
82
|
+
if (typeof json.result?.journeyId === "string" && typeof h?.brittle === "number" && Array.isArray(h.steps))
|
|
83
|
+
out.set(json.result.journeyId, h);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// An unreadable baseline run has no locator health to compare against.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** #470: one trend over several Journeys (the counts summed). */
|
|
92
|
+
function sumTrends(trends) {
|
|
93
|
+
const sum = (f) => trends.reduce((n, t) => n + f(t), 0);
|
|
94
|
+
return {
|
|
95
|
+
improved: sum((t) => t.improved),
|
|
96
|
+
regressed: sum((t) => t.regressed),
|
|
97
|
+
unchanged: sum((t) => t.unchanged),
|
|
98
|
+
added: sum((t) => t.added),
|
|
99
|
+
removed: sum((t) => t.removed),
|
|
100
|
+
brittleDelta: sum((t) => t.brittleDelta),
|
|
101
|
+
changes: trends.flatMap((t) => t.changes),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const LOCATOR_WARNING_RULE = "jevitate/locator-health/brittle-locator";
|
|
105
|
+
/**
|
|
106
|
+
* #470: advisory locator fixes as SARIF WARNINGS (never errors, never gating) — one result per element
|
|
107
|
+
* to fix, located at the suite file with the route as its logical location.
|
|
108
|
+
*/
|
|
109
|
+
function withLocatorWarnings(sarif, suggestions, suiteUri) {
|
|
110
|
+
if (suggestions.length === 0)
|
|
111
|
+
return sarif;
|
|
112
|
+
const [run, ...rest] = sarif.runs;
|
|
113
|
+
if (run === undefined)
|
|
114
|
+
return sarif;
|
|
115
|
+
const rule = { id: LOCATOR_WARNING_RULE, name: LOCATOR_WARNING_RULE.replace(/[^A-Za-z0-9]+/g, "_"), shortDescription: { text: "locator-health: a step's target misses the project's test-id convention" }, defaultConfiguration: { level: "warning" } };
|
|
116
|
+
const results = suggestions.map((s) => {
|
|
117
|
+
const journeys = [...new Set(s.occurrences.flatMap((o) => (o.journeyId === undefined ? [] : [o.journeyId])))];
|
|
118
|
+
return {
|
|
119
|
+
ruleId: LOCATOR_WARNING_RULE,
|
|
120
|
+
level: "warning",
|
|
121
|
+
message: { text: s.fix },
|
|
122
|
+
locations: [
|
|
123
|
+
{
|
|
124
|
+
physicalLocation: { artifactLocation: { uri: suiteUri }, region: { startLine: 1 } },
|
|
125
|
+
...(s.route === undefined ? {} : { logicalLocations: [{ name: s.route, kind: "module" }] }),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
partialFingerprints: { jevitateFindingKey: `locator-health:${s.key}` },
|
|
129
|
+
properties: { category: "locator-health", severity: "advisory", gating: false, element: s.element, reasons: s.reasons, journeys, steps: s.occurrences.length },
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
return { ...sarif, runs: [{ ...run, tool: { ...run.tool, driver: { ...run.tool.driver, rules: [...run.tool.driver.rules, rule] } }, results: [...run.results, ...results] }, ...rest] };
|
|
133
|
+
}
|
|
44
134
|
export async function runCheck(opts) {
|
|
45
135
|
const nowIso = opts.nowIso ?? (() => clock.nowIso());
|
|
46
136
|
const startedAt = nowIso();
|
|
@@ -53,6 +143,18 @@ export async function runCheck(opts) {
|
|
|
53
143
|
for (const t of opts.suite.targets)
|
|
54
144
|
prepared.push(await prepareTarget(t, opts));
|
|
55
145
|
const items = plan(prepared, opts.changedRoutes, opts);
|
|
146
|
+
// #453: refuse a bad self-heal request and read the change scope ONCE, before the first browser opens.
|
|
147
|
+
const heal = await prepareSelfHeal(opts);
|
|
148
|
+
// #470: the opt-in locator gate and the convention it measures against, before anything runs.
|
|
149
|
+
if (opts.maxBrittleSteps !== undefined) {
|
|
150
|
+
try {
|
|
151
|
+
brittleStepGate(opts.maxBrittleSteps);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw new CheckArgsError(errorMessage(e));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const testIdAttributes = items.some((i) => i.kind === "journey" && i.skipped === undefined) ? checkTestIdAttributes(opts) : undefined;
|
|
56
158
|
let gw;
|
|
57
159
|
const gateways = () => {
|
|
58
160
|
if (opts.gateways === undefined)
|
|
@@ -67,7 +169,17 @@ export async function runCheck(opts) {
|
|
|
67
169
|
const meter = new BudgetMeter(opts.suite.budget, { ...(opts.now === undefined ? {} : { now: opts.now }), costKnownZero: opts.aiMode === "fake" });
|
|
68
170
|
const usage = async () => (gw === undefined ? undefined : (await gw).usage.snapshot());
|
|
69
171
|
let n = 0;
|
|
70
|
-
|
|
172
|
+
let healer;
|
|
173
|
+
const ctx = {
|
|
174
|
+
opts,
|
|
175
|
+
runners,
|
|
176
|
+
resultsDir,
|
|
177
|
+
gateways,
|
|
178
|
+
engine,
|
|
179
|
+
seq: () => String(++n),
|
|
180
|
+
...(testIdAttributes === undefined ? {} : { testIdAttributes }),
|
|
181
|
+
...(heal === undefined ? {} : { heal, healer: () => (healer ??= gateways().then((g) => makeEvidenceSelfHealer(g.gen, { usage: g.usage }))) }),
|
|
182
|
+
};
|
|
71
183
|
const now = opts.now ?? clock.now;
|
|
72
184
|
let exceeded;
|
|
73
185
|
const executed = [];
|
|
@@ -109,7 +221,17 @@ export async function runCheck(opts) {
|
|
|
109
221
|
}
|
|
110
222
|
// #437 --require-approvals: each violation is a hard `approval` finding on the `approvals` item.
|
|
111
223
|
const approvals = opts.requireApprovals === undefined ? undefined : await approvalDefects(opts, opts.requireApprovals);
|
|
112
|
-
|
|
224
|
+
// #470 --max-brittle-steps: a Journey item past the threshold is a hard `locator-health` finding on that item.
|
|
225
|
+
const brittleOf = new Map();
|
|
226
|
+
if (opts.maxBrittleSteps !== undefined) {
|
|
227
|
+
executed.forEach(({ item, ex }, idx) => {
|
|
228
|
+
const health = ex?.locatorHealth;
|
|
229
|
+
if (item.kind === "journey" && health !== undefined && exceedsBrittleSteps(health, opts.maxBrittleSteps)) {
|
|
230
|
+
brittleOf.set(idx, brittleDefect(item.name, { brittle: health.brittle, ...(ex?.locatorDetail === undefined ? {} : { line: ex.locatorDetail.line }) }, opts.maxBrittleSteps));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const defects = [...consolidate(runs), ...(approvals?.defects ?? []), ...brittleOf.values()];
|
|
113
235
|
let diff;
|
|
114
236
|
let baselineRuns;
|
|
115
237
|
if (opts.baseline !== undefined) {
|
|
@@ -123,6 +245,9 @@ export async function runCheck(opts) {
|
|
|
123
245
|
}
|
|
124
246
|
const entryOf = new Map(diff?.entries.map((e) => [e.key, e]) ?? []);
|
|
125
247
|
const isGating = (d) => {
|
|
248
|
+
// #453: a proposed Journey revision (`pending`) is never a defect, whatever `gateAdvisory` says: it makes the check exit 5.
|
|
249
|
+
if (d.severity === "pending")
|
|
250
|
+
return false;
|
|
126
251
|
if (d.severity !== "hard" && !opts.suite.gateAdvisory)
|
|
127
252
|
return false;
|
|
128
253
|
if (diff === undefined)
|
|
@@ -133,7 +258,7 @@ export async function runCheck(opts) {
|
|
|
133
258
|
};
|
|
134
259
|
const statusOf = (d) => d.keys.map((k) => entryOf.get(k)?.status).find((s) => s !== undefined);
|
|
135
260
|
const gating = defects.filter(isGating);
|
|
136
|
-
const itemReports = executed.map(({ item, ex, durationMs }) => {
|
|
261
|
+
const itemReports = executed.map(({ item, ex, durationMs }, idx) => {
|
|
137
262
|
const base = {
|
|
138
263
|
target: item.t.target.name,
|
|
139
264
|
kind: item.kind,
|
|
@@ -144,10 +269,18 @@ export async function runCheck(opts) {
|
|
|
144
269
|
if (ex === undefined)
|
|
145
270
|
return { ...base, status: "skipped", actions: 0, verdict: "skipped", gating: [] };
|
|
146
271
|
const run = ex.resultPath === undefined ? undefined : runOf.get(ex.resultPath);
|
|
147
|
-
const
|
|
148
|
-
const
|
|
272
|
+
const brittle = brittleOf.get(idx);
|
|
273
|
+
const own = [
|
|
274
|
+
...(run === undefined ? [] : gating.filter((d) => d.modes.some((m) => m.runs.some((r) => r.path === run.path)))),
|
|
275
|
+
...(brittle !== undefined && gating.includes(brittle) ? [brittle] : []),
|
|
276
|
+
];
|
|
277
|
+
// #453: a self-healed Journey that only proposed a revision is pending a person (never a pass).
|
|
278
|
+
const pending = ex.status === "ran" && ex.outcome === "healed-pending-review" && own.length === 0;
|
|
279
|
+
const verdict = ex.status === "error" ? "error" : own.length > 0 ? "failed" : pending ? "pending-review" : "passed";
|
|
149
280
|
return {
|
|
150
281
|
...base,
|
|
282
|
+
...(pending ? { proposal: { journeyId: item.name, ...(ex.proposal ?? {}) } } : {}),
|
|
283
|
+
...(ex.healAttempts === undefined ? {} : { healAttempts: ex.healAttempts }),
|
|
151
284
|
status: ex.status,
|
|
152
285
|
actions: ex.actions,
|
|
153
286
|
verdict,
|
|
@@ -157,19 +290,60 @@ export async function runCheck(opts) {
|
|
|
157
290
|
...(run === undefined ? {} : { runId: run.runId }),
|
|
158
291
|
...(ex.outcome === undefined ? {} : { outcome: ex.outcome }),
|
|
159
292
|
...(ex.goalOutcome === undefined ? {} : { goalOutcome: ex.goalOutcome }),
|
|
293
|
+
...(ex.journeyHash === undefined ? {} : { journeyHash: ex.journeyHash }),
|
|
294
|
+
// #469: a baseline only from a clean run — a completed replay with no gating finding.
|
|
295
|
+
...(ex.baseline === undefined || verdict !== "passed" ? {} : { baseline: ex.baseline }),
|
|
296
|
+
...(ex.locatorHealth === undefined ? {} : { locatorHealth: ex.locatorHealth }),
|
|
160
297
|
};
|
|
161
298
|
});
|
|
299
|
+
// #470: locator health over the Journey items (advisory unless the gate is set), and its trend against --baseline.
|
|
300
|
+
const details = executed.flatMap(({ item, ex }) => item.kind === "journey" && ex?.locatorDetail !== undefined ? [{ name: item.name, detail: ex.locatorDetail }] : []);
|
|
301
|
+
const combined = combineHealth(details.map((d) => d.detail));
|
|
302
|
+
let locatorTrend;
|
|
303
|
+
if (baselineRuns !== undefined && details.length > 0) {
|
|
304
|
+
const before = baselineLocatorHealths(baselineRuns);
|
|
305
|
+
const trends = executed.flatMap(({ item, ex }) => {
|
|
306
|
+
const b = item.kind === "journey" && ex?.locatorHealth !== undefined ? before.get(item.name) : undefined;
|
|
307
|
+
return b === undefined || ex?.locatorHealth === undefined ? [] : [compareLocatorHealth(ex.locatorHealth, b)];
|
|
308
|
+
});
|
|
309
|
+
if (trends.length > 0) {
|
|
310
|
+
const t = sumTrends(trends);
|
|
311
|
+
locatorTrend = { improved: t.improved, regressed: t.regressed, unchanged: t.unchanged, added: t.added, removed: t.removed, brittleDelta: t.brittleDelta, line: trendLine(t) };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const locatorHealth = details.length === 0 && opts.maxBrittleSteps === undefined
|
|
315
|
+
? undefined
|
|
316
|
+
: {
|
|
317
|
+
journeys: details.length,
|
|
318
|
+
stable: combined.stable,
|
|
319
|
+
brittle: combined.brittle,
|
|
320
|
+
...(testIdAttributes === undefined ? {} : { testIdAttributes }),
|
|
321
|
+
...(opts.maxBrittleSteps === undefined ? {} : { maxBrittleSteps: opts.maxBrittleSteps, exceeded: brittleOf.size > 0 }),
|
|
322
|
+
...(details.length === 0 ? {} : { line: combined.line }),
|
|
323
|
+
...(locatorTrend === undefined ? {} : { trend: locatorTrend }),
|
|
324
|
+
};
|
|
325
|
+
// Without the gate, each Journey item's fixes are JUnit/SARIF warnings (never failures).
|
|
326
|
+
const warningsOf = (i, idx) => {
|
|
327
|
+
if (opts.maxBrittleSteps !== undefined || i.kind !== "journey")
|
|
328
|
+
return [];
|
|
329
|
+
const ex = executed[idx]?.ex;
|
|
330
|
+
return (ex?.locatorDetail?.suggestions ?? []).map((s) => s.fix);
|
|
331
|
+
};
|
|
162
332
|
if (approvals !== undefined) {
|
|
163
333
|
const own = gating.filter((d) => approvals.defects.includes(d)).map((d) => d.key);
|
|
164
334
|
itemReports.push({ target: "approvals", kind: "approvals", name: "require-approvals", status: "ran", actions: 0, durationMs: 0, verdict: own.length > 0 ? "failed" : "passed", gating: own });
|
|
165
335
|
}
|
|
166
336
|
const errors = itemReports.filter((i) => i.verdict === "error").length;
|
|
167
|
-
const
|
|
337
|
+
const pendingItems = itemReports.filter((i) => i.verdict === "pending-review");
|
|
338
|
+
const proposals = pendingItems.flatMap((i) => (i.proposal === undefined ? [] : [i.proposal]));
|
|
339
|
+
const gatingFindings = gating.length;
|
|
340
|
+
// Precedence 1 > 2 > 5 > 0: a defect or exhausted heal, then an item error / budget overrun, then a proposal awaiting review.
|
|
341
|
+
const exitCode = gatingFindings > 0 ? 1 : errors > 0 || exceeded !== undefined ? 2 : pendingItems.length > 0 ? 5 : 0;
|
|
168
342
|
const junitPath = resolve(opts.junitPath ?? join(outDir, "junit.xml"));
|
|
169
343
|
const sarifPath = resolve(opts.sarifPath ?? join(outDir, "jevitate.sarif"));
|
|
170
344
|
const jsonPath = resolve(opts.jsonPath ?? join(outDir, "check.json"));
|
|
171
345
|
const reportPath = join(outDir, "report.md");
|
|
172
|
-
const cases = itemReports.map((i) => {
|
|
346
|
+
const cases = itemReports.map((i, idx) => {
|
|
173
347
|
const own = gating.filter((d) => i.gating.includes(d.key));
|
|
174
348
|
const first = own[0];
|
|
175
349
|
// #250: each gating finding's repro clip and screenshots, attached to the case CI shows.
|
|
@@ -184,6 +358,14 @@ export async function runCheck(opts) {
|
|
|
184
358
|
status: i.verdict,
|
|
185
359
|
...(i.resultPath === undefined ? {} : { resultPath: i.resultPath }),
|
|
186
360
|
...(attachments.length === 0 ? {} : { attachments }),
|
|
361
|
+
...(warningsOf(i, idx).length === 0 ? {} : { warnings: warningsOf(i, idx) }),
|
|
362
|
+
...(i.verdict === "pending-review"
|
|
363
|
+
? {
|
|
364
|
+
type: "healed-pending-review",
|
|
365
|
+
message: `proposed revision${i.proposal?.proposalId === undefined ? "" : ` ${i.proposal.proposalId}`} awaiting review`,
|
|
366
|
+
...(i.proposal?.path === undefined ? {} : { detail: i.proposal.path }),
|
|
367
|
+
}
|
|
368
|
+
: {}),
|
|
187
369
|
...(i.verdict === "failed" && first !== undefined
|
|
188
370
|
? { type: first.category, message: `${own.length} gating finding(s): ${first.title}`, detail: caseDetail(own) }
|
|
189
371
|
: {}),
|
|
@@ -214,7 +396,7 @@ export async function runCheck(opts) {
|
|
|
214
396
|
kind: "jevitate-check",
|
|
215
397
|
suite: opts.suite.name,
|
|
216
398
|
suitePath: opts.suite.path,
|
|
217
|
-
verdict: exitCode === 0 ? "pass" : "fail",
|
|
399
|
+
verdict: exitCode === 0 ? "pass" : exitCode === 5 ? "pending-review" : "fail",
|
|
218
400
|
exitCode,
|
|
219
401
|
engine,
|
|
220
402
|
...(opts.targetBuild === undefined ? {} : { targetBuild: opts.targetBuild }),
|
|
@@ -229,8 +411,11 @@ export async function runCheck(opts) {
|
|
|
229
411
|
failed: itemReports.filter((i) => i.verdict === "failed").length,
|
|
230
412
|
errors,
|
|
231
413
|
skipped: itemReports.filter((i) => i.verdict === "skipped").length,
|
|
232
|
-
gatingFindings
|
|
414
|
+
gatingFindings,
|
|
415
|
+
pendingReview: pendingItems.length,
|
|
233
416
|
},
|
|
417
|
+
...(locatorHealth === undefined ? {} : { locatorHealth }),
|
|
418
|
+
proposals,
|
|
234
419
|
...(diff === undefined || baselineRuns === undefined ? {} : { diff: { baseline: baselineRuns.map(summarizeRun), summary: diff.summary } }),
|
|
235
420
|
results: runs.map((r) => r.path),
|
|
236
421
|
junitPath,
|
|
@@ -239,16 +424,21 @@ export async function runCheck(opts) {
|
|
|
239
424
|
reportPath,
|
|
240
425
|
};
|
|
241
426
|
await writeFile(junitPath, renderJUnit(`jevitate check: ${opts.suite.name}`, cases, startedAt), "utf8");
|
|
242
|
-
const sarif = renderSarif({
|
|
427
|
+
const sarif = withLocatorWarnings(renderSarif({
|
|
243
428
|
toolVersion: engine.version,
|
|
244
429
|
engineCommit: engine.commit,
|
|
245
430
|
...(opts.targetBuild === undefined ? {} : { targetBuild: opts.targetBuild }),
|
|
246
431
|
suiteUri: opts.suiteUri ?? opts.suite.path,
|
|
247
432
|
automationId: `jevitate-check/${opts.suite.name}/`,
|
|
248
433
|
findings: defects.map((d) => ({ defect: d, gating: gating.includes(d), ...(statusOf(d) === undefined ? {} : { status: statusOf(d) }) })),
|
|
249
|
-
});
|
|
434
|
+
}), opts.maxBrittleSteps === undefined ? combined.suggestions : [], opts.suiteUri ?? opts.suite.path);
|
|
250
435
|
await writeFile(sarifPath, `${JSON.stringify(sarif, null, 2)}\n`, "utf8");
|
|
251
436
|
await writeFile(reportPath, renderReportMarkdown({ title: `jevitate check: ${opts.suite.name} — ${result.verdict}`, runs, defects, ...(diff === undefined ? {} : { diff }) }) +
|
|
437
|
+
(proposals.length === 0
|
|
438
|
+
? ""
|
|
439
|
+
: `\n## Proposed Journey revisions\n\nA self-heal re-ran these failed Journeys against the change and proposed a revision. Nothing passes until a person accepts it.\n\n${proposals
|
|
440
|
+
.map((p) => `- \`${p.journeyId}\`${p.proposalId === undefined ? "" : ` — proposal \`${p.proposalId}\``}${p.path === undefined ? "" : ` (${p.path})`}: \`jevitate journey review ${p.journeyId}\``)
|
|
441
|
+
.join("\n")}\n`) +
|
|
252
442
|
(suiteUsage === undefined ? "" : `\n## Model cost\n\n${formatUsageLine(suiteUsage)}\n`), "utf8");
|
|
253
443
|
await writeFile(jsonPath, `${JSON.stringify({ v: 1, ok: true, data: result }, null, 2)}\n`, "utf8");
|
|
254
444
|
return result;
|