@codyswann/lisa 4.74.3 → 4.74.5
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/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +3 -0
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +71 -1
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +24 -4
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +3 -3
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +14 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +2 -2
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +6 -6
- package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +8 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +227 -41
|
@@ -1239,6 +1239,7 @@ export function assessSuite(suite, observation, context) {
|
|
|
1239
1239
|
label: suite.label,
|
|
1240
1240
|
workflow: suite.workflow,
|
|
1241
1241
|
...(observation.selection ? { selection: observation.selection } : {}),
|
|
1242
|
+
...(observation.discovery ? { discovery: observation.discovery } : {}),
|
|
1242
1243
|
};
|
|
1243
1244
|
const blank = { conclusion: null, url: null, createdAt: null, event: null };
|
|
1244
1245
|
// An observation with no `scope` at all is the same fact as an artifacts list
|
|
@@ -2204,7 +2205,11 @@ export function formatReport(verdict, context) {
|
|
|
2204
2205
|
...verdict.findings.flatMap(finding => {
|
|
2205
2206
|
const line = `- ${formatFinding(finding)}`;
|
|
2206
2207
|
const selection = formatSelection(finding.selection);
|
|
2207
|
-
return
|
|
2208
|
+
return [
|
|
2209
|
+
line,
|
|
2210
|
+
...(selection === null ? [] : [` - ${selection}`]),
|
|
2211
|
+
...(finding.discovery ?? []).map(entry => ` - Discovery: ${entry}`),
|
|
2212
|
+
];
|
|
2208
2213
|
})
|
|
2209
2214
|
);
|
|
2210
2215
|
lines.push("");
|
|
@@ -3094,17 +3099,16 @@ export async function apiGet(api, path, wait = sleep) {
|
|
|
3094
3099
|
*
|
|
3095
3100
|
* @param {object} api - API coordinates
|
|
3096
3101
|
* @param {string} file - Workflow file name
|
|
3097
|
-
* @param {string} branch - Branch to read
|
|
3098
|
-
* @param {string} event - Run event
|
|
3102
|
+
* @param {string|null} branch - Branch to read; null for client-filtered discovery
|
|
3103
|
+
* @param {string|null} event - Run event; null to omit status/event filters
|
|
3099
3104
|
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
3100
3105
|
* @returns {Promise<{runs: ReadonlyArray<object>, missing: boolean}>} The candidates, or a 404 marker
|
|
3101
3106
|
*/
|
|
3102
3107
|
export async function fetchRunCandidates(api, file, branch, event, wait) {
|
|
3103
3108
|
const query = new URLSearchParams({
|
|
3104
|
-
branch,
|
|
3105
|
-
status: "completed",
|
|
3106
|
-
event,
|
|
3107
|
-
per_page: String(RUN_CANDIDATE_PAGE_SIZE),
|
|
3109
|
+
...(branch ? { branch } : {}),
|
|
3110
|
+
...(event ? { status: "completed", event } : {}),
|
|
3111
|
+
per_page: String(event ? RUN_CANDIDATE_PAGE_SIZE : 100),
|
|
3108
3112
|
});
|
|
3109
3113
|
const result = await apiGet(
|
|
3110
3114
|
api,
|
|
@@ -3112,12 +3116,191 @@ export async function fetchRunCandidates(api, file, branch, event, wait) {
|
|
|
3112
3116
|
wait
|
|
3113
3117
|
);
|
|
3114
3118
|
if (result === null) return { runs: Object.freeze([]), missing: true };
|
|
3119
|
+
if (!Array.isArray(result.body.workflow_runs)) {
|
|
3120
|
+
throw staleRunList(file, ["the run list was not an array"]);
|
|
3121
|
+
}
|
|
3115
3122
|
return {
|
|
3116
|
-
runs: Object.freeze(result.body.workflow_runs
|
|
3123
|
+
runs: Object.freeze(result.body.workflow_runs),
|
|
3117
3124
|
missing: false,
|
|
3118
3125
|
};
|
|
3119
3126
|
}
|
|
3120
3127
|
|
|
3128
|
+
/**
|
|
3129
|
+
* Refuses untrustworthy discovery separately from a suite's actual verdict.
|
|
3130
|
+
* The API-failure path cannot offer a waiver for a suite it never observed.
|
|
3131
|
+
* @param {string} file - Workflow whose discovery could not be established
|
|
3132
|
+
* @param {ReadonlyArray<string>} diagnostics - Raw list and detail observations
|
|
3133
|
+
* @returns {GateApiError} A distinct platform-read refusal
|
|
3134
|
+
*/
|
|
3135
|
+
function staleRunList(file, diagnostics) {
|
|
3136
|
+
const error = new GateApiError(
|
|
3137
|
+
`[list_stale] GitHub's run history for ${file} could not establish a current, trustworthy completed run. The platform returned old or conflicting run information; this is not evidence that the nightly suite failed. Re-run this check to retry the platform read.\n\nDiscovery observations:\n${diagnostics.join("\n")}`
|
|
3138
|
+
);
|
|
3139
|
+
error.reason = "list_stale";
|
|
3140
|
+
return error;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
/**
|
|
3144
|
+
* Checks the fields the server-side filters were supposed to establish.
|
|
3145
|
+
* Branchless discovery has to apply the same scope locally, and direct reads
|
|
3146
|
+
* have to prove that scope independently rather than inherit the list's claim.
|
|
3147
|
+
* @param {object} run - A list entry or single-run payload
|
|
3148
|
+
* @param {string} file - The declared workflow file
|
|
3149
|
+
* @param {string} branch - The declared branch
|
|
3150
|
+
* @returns {boolean} Whether this run is eligible for this workflow and branch
|
|
3151
|
+
*/
|
|
3152
|
+
function eligibleDiscoveredRun(run, file, branch) {
|
|
3153
|
+
return (
|
|
3154
|
+
run !== null &&
|
|
3155
|
+
typeof run === "object" &&
|
|
3156
|
+
Number.isSafeInteger(run.id) &&
|
|
3157
|
+
run.id > 0 &&
|
|
3158
|
+
typeof run.created_at === "string" &&
|
|
3159
|
+
Number.isFinite(Date.parse(run.created_at)) &&
|
|
3160
|
+
run.status === "completed" &&
|
|
3161
|
+
run.head_branch === branch &&
|
|
3162
|
+
COUNTED_EVENTS.includes(run.event) &&
|
|
3163
|
+
typeof run.path === "string" &&
|
|
3164
|
+
run.path.split("@")[0] === `.github/workflows/${file}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* Discovers IDs while retaining the newest eligible observation across every
|
|
3170
|
+
* snapshot. Old heads get bounded alternative reads without status/event, then
|
|
3171
|
+
* without branch. A later stale snapshot cannot erase a run already seen.
|
|
3172
|
+
* @param {object} api - API coordinates and bounded retry settings
|
|
3173
|
+
* @param {string} file - Workflow file
|
|
3174
|
+
* @param {string} branch - Branch to inspect
|
|
3175
|
+
* @param {number} freshnessHours - This suite's existing freshness limit
|
|
3176
|
+
* @param {Date} now - Evaluation instant
|
|
3177
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable bounded backoff
|
|
3178
|
+
* @returns {Promise<object>} Candidates plus raw discovery diagnostics
|
|
3179
|
+
*/
|
|
3180
|
+
async function discoverRunCandidates(
|
|
3181
|
+
api,
|
|
3182
|
+
file,
|
|
3183
|
+
branch,
|
|
3184
|
+
freshnessHours,
|
|
3185
|
+
now,
|
|
3186
|
+
wait = sleep
|
|
3187
|
+
) {
|
|
3188
|
+
const diagnostics = [];
|
|
3189
|
+
const byId = new Map();
|
|
3190
|
+
const ingest = (result, strategy) => {
|
|
3191
|
+
const raw = result.runs.map(
|
|
3192
|
+
run => `${run?.id ?? "?"}@${run?.created_at ?? "?"}`
|
|
3193
|
+
);
|
|
3194
|
+
const line = `${file} ${strategy}: ${result.missing ? "404" : raw.join(", ") || "empty"}`;
|
|
3195
|
+
diagnostics.push(line);
|
|
3196
|
+
process.stderr.write(`[nightly discovery] ${line}\n`);
|
|
3197
|
+
for (const run of result.runs) {
|
|
3198
|
+
if (!eligibleDiscoveredRun(run, file, branch)) continue;
|
|
3199
|
+
const previous = byId.get(run.id);
|
|
3200
|
+
// An ID's creation time and workflow identity do not change. Disagreement
|
|
3201
|
+
// is an unreadable platform observation, never grounds to overwrite it.
|
|
3202
|
+
if (
|
|
3203
|
+
previous &&
|
|
3204
|
+
(previous.created_at !== run.created_at ||
|
|
3205
|
+
previous.workflow_id !== run.workflow_id ||
|
|
3206
|
+
previous.head_sha !== run.head_sha ||
|
|
3207
|
+
previous.event !== run.event)
|
|
3208
|
+
) {
|
|
3209
|
+
throw staleRunList(file, [
|
|
3210
|
+
...diagnostics,
|
|
3211
|
+
`run ${run.id} changed identity between list reads`,
|
|
3212
|
+
]);
|
|
3213
|
+
}
|
|
3214
|
+
byId.set(run.id, run);
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
const sorted = () =>
|
|
3218
|
+
[...byId.values()].sort(
|
|
3219
|
+
(left, right) =>
|
|
3220
|
+
Date.parse(right.created_at) - Date.parse(left.created_at) ||
|
|
3221
|
+
right.id - left.id
|
|
3222
|
+
);
|
|
3223
|
+
const primary = await Promise.all(
|
|
3224
|
+
COUNTED_EVENTS.map(event =>
|
|
3225
|
+
fetchRunCandidates(api, file, branch, event, wait)
|
|
3226
|
+
)
|
|
3227
|
+
);
|
|
3228
|
+
primary.forEach((result, index) =>
|
|
3229
|
+
ingest(result, `filtered ${COUNTED_EVENTS[index]}`)
|
|
3230
|
+
);
|
|
3231
|
+
if (primary.every(result => result.missing))
|
|
3232
|
+
return { missing: true, candidates: [], diagnostics };
|
|
3233
|
+
let candidates = sorted();
|
|
3234
|
+
// One initial filtered snapshot plus at most maxAttempts-1 alternatives.
|
|
3235
|
+
// This shares the existing clamped retry budget instead of adding an
|
|
3236
|
+
// independently configurable source of unbounded merge-time requests.
|
|
3237
|
+
for (
|
|
3238
|
+
let attempt = 1;
|
|
3239
|
+
!isFresh(candidates[0], freshnessHours, now) && attempt < api.maxAttempts;
|
|
3240
|
+
attempt += 1
|
|
3241
|
+
) {
|
|
3242
|
+
await wait(retryDelayMs({ headers: null }, attempt, api.retryMaxSeconds));
|
|
3243
|
+
const alternateBranch = attempt === 1 ? branch : null;
|
|
3244
|
+
const result = await fetchRunCandidates(
|
|
3245
|
+
api,
|
|
3246
|
+
file,
|
|
3247
|
+
alternateBranch,
|
|
3248
|
+
null,
|
|
3249
|
+
wait
|
|
3250
|
+
);
|
|
3251
|
+
ingest(
|
|
3252
|
+
result,
|
|
3253
|
+
alternateBranch
|
|
3254
|
+
? "unfiltered events, branch"
|
|
3255
|
+
: "unfiltered events, all branches"
|
|
3256
|
+
);
|
|
3257
|
+
candidates = sorted();
|
|
3258
|
+
}
|
|
3259
|
+
if (
|
|
3260
|
+
!isFresh(candidates[0], freshnessHours, now) &&
|
|
3261
|
+
(candidates.length > 0 || primary.some(result => result.runs.length > 0))
|
|
3262
|
+
) {
|
|
3263
|
+
throw staleRunList(file, diagnostics);
|
|
3264
|
+
}
|
|
3265
|
+
return { missing: false, candidates, diagnostics };
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* Reads a discovered ID independently before any verdict or cancellation walk.
|
|
3270
|
+
* A single-run 404 or conflict cannot prove that a newer run tested nothing.
|
|
3271
|
+
* @param {object} api - API coordinates
|
|
3272
|
+
* @param {object} candidate - The list's discovery claim
|
|
3273
|
+
* @param {object} discovery - Workflow, branch, diagnostics and detail cache
|
|
3274
|
+
* @param {(ms: number) => Promise<void>} [wait] - Injectable retry sleep
|
|
3275
|
+
* @returns {Promise<object>} Verified single-run payload
|
|
3276
|
+
*/
|
|
3277
|
+
async function verifyDiscoveredRun(api, candidate, discovery, wait) {
|
|
3278
|
+
if (discovery.verified.has(candidate.id))
|
|
3279
|
+
return discovery.verified.get(candidate.id);
|
|
3280
|
+
const result = await apiGet(
|
|
3281
|
+
api,
|
|
3282
|
+
`/repos/${api.repo}/actions/runs/${candidate.id}`,
|
|
3283
|
+
wait
|
|
3284
|
+
);
|
|
3285
|
+
const run = result?.body;
|
|
3286
|
+
const matches =
|
|
3287
|
+
eligibleDiscoveredRun(run, discovery.file, discovery.branch) &&
|
|
3288
|
+
run.id === candidate.id &&
|
|
3289
|
+
run.created_at === candidate.created_at &&
|
|
3290
|
+
Number.isSafeInteger(run.workflow_id) &&
|
|
3291
|
+
run.workflow_id === candidate.workflow_id &&
|
|
3292
|
+
run.event === candidate.event &&
|
|
3293
|
+
typeof run.head_sha === "string" &&
|
|
3294
|
+
/^[a-f0-9]{40}$/i.test(run.head_sha) &&
|
|
3295
|
+
run.head_sha === candidate.head_sha;
|
|
3296
|
+
const line = `${discovery.file} detail ${candidate.id}: ${matches ? "verified" : "missing or conflicting"} ${run?.id ?? "?"}@${run?.created_at ?? "?"}`;
|
|
3297
|
+
discovery.diagnostics.push(line);
|
|
3298
|
+
process.stderr.write(`[nightly discovery] ${line}\n`);
|
|
3299
|
+
if (!matches) throw staleRunList(discovery.file, discovery.diagnostics);
|
|
3300
|
+
discovery.verified.set(candidate.id, run);
|
|
3301
|
+
return run;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3121
3304
|
/**
|
|
3122
3305
|
* Every job of a run, paginated to exhaustion.
|
|
3123
3306
|
*
|
|
@@ -3424,8 +3607,8 @@ export async function fetchRunArtifacts(api, runId, wait) {
|
|
|
3424
3607
|
* worse one.
|
|
3425
3608
|
*
|
|
3426
3609
|
* When nothing conclusive is found inside the window it falls back to the NEWEST
|
|
3427
|
-
* candidate
|
|
3428
|
-
*
|
|
3610
|
+
* verified candidate, so `indecisive_conclusion` and the rest still fire and
|
|
3611
|
+
* the gate still blocks. Old discovery heads are refused before this walk.
|
|
3429
3612
|
* That fallback is what makes selection unable to invent a pass: it may only
|
|
3430
3613
|
* ever promote an older fresh, conclusive run over a run that tested nothing.
|
|
3431
3614
|
*
|
|
@@ -3434,14 +3617,23 @@ export async function fetchRunArtifacts(api, runId, wait) {
|
|
|
3434
3617
|
* @param {number} freshnessHours - This arm's freshness window
|
|
3435
3618
|
* @param {Date} now - Evaluation instant
|
|
3436
3619
|
* @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
|
|
3620
|
+
* @param {object} discovery - Workflow scope, diagnostics and verified detail cache
|
|
3437
3621
|
* @returns {Promise<{run: object, jobs: ReadonlyArray<object>, jobsComplete: boolean, selection: object}>} The scored run, its jobs, whether that job list was read to its end, and the audit
|
|
3438
3622
|
*/
|
|
3439
|
-
async function selectScoredRun(
|
|
3623
|
+
async function selectScoredRun(
|
|
3624
|
+
api,
|
|
3625
|
+
candidates,
|
|
3626
|
+
freshnessHours,
|
|
3627
|
+
now,
|
|
3628
|
+
wait,
|
|
3629
|
+
discovery
|
|
3630
|
+
) {
|
|
3440
3631
|
const jobsById = new Map();
|
|
3441
3632
|
const walkedPast = [];
|
|
3442
3633
|
let chosen = null;
|
|
3443
|
-
for (const
|
|
3444
|
-
if (!isFresh(
|
|
3634
|
+
for (const listed of candidates) {
|
|
3635
|
+
if (!isFresh(listed, freshnessHours, now)) break;
|
|
3636
|
+
const candidate = await verifyDiscoveredRun(api, listed, discovery, wait);
|
|
3445
3637
|
const read = await fetchAllJobs(api, candidate.id, wait);
|
|
3446
3638
|
const { jobs, complete } = read;
|
|
3447
3639
|
jobsById.set(candidate.id, read);
|
|
@@ -3457,7 +3649,8 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
|
|
|
3457
3649
|
totalJobs: jobs.length,
|
|
3458
3650
|
});
|
|
3459
3651
|
}
|
|
3460
|
-
const run =
|
|
3652
|
+
const run =
|
|
3653
|
+
chosen ?? (await verifyDiscoveredRun(api, candidates[0], discovery, wait));
|
|
3461
3654
|
const read = jobsById.get(run.id) ?? (await fetchAllJobs(api, run.id, wait));
|
|
3462
3655
|
const { jobs, complete: jobsComplete } = read;
|
|
3463
3656
|
// A run cannot be both scored and skipped. When the walk falls back onto a
|
|
@@ -3469,8 +3662,8 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
|
|
|
3469
3662
|
// Asked only of the one shape that is ambiguous: a run that reached no
|
|
3470
3663
|
// verdict of its own yet scored some of its jobs. A decisive run has already
|
|
3471
3664
|
// said what happened, and a run with nothing decisive tested nothing — both
|
|
3472
|
-
// are named without this, so neither pays for
|
|
3473
|
-
//
|
|
3665
|
+
// are named without this, so neither pays for an annotation read. Independent
|
|
3666
|
+
// run-detail verification still applies on the common path.
|
|
3474
3667
|
const timedOut =
|
|
3475
3668
|
jobsComplete &&
|
|
3476
3669
|
!DECISIVE_CONCLUSIONS.has(run.conclusion ?? null) &&
|
|
@@ -3509,31 +3702,18 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
|
|
|
3509
3702
|
export async function observe(api, suites, branch, context, wait) {
|
|
3510
3703
|
return await Promise.all(
|
|
3511
3704
|
suites.map(async suite => {
|
|
3512
|
-
const
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3705
|
+
const freshnessHours = suite.freshness_hours ?? context.freshnessHours;
|
|
3706
|
+
const { missing, candidates, diagnostics } = await discoverRunCandidates(
|
|
3707
|
+
api,
|
|
3708
|
+
suite.workflow,
|
|
3709
|
+
branch,
|
|
3710
|
+
freshnessHours,
|
|
3711
|
+
context.now,
|
|
3712
|
+
wait
|
|
3516
3713
|
);
|
|
3517
|
-
if (
|
|
3714
|
+
if (missing) {
|
|
3518
3715
|
return { workflowMissing: true, run: null, jobs: [] };
|
|
3519
3716
|
}
|
|
3520
|
-
// Newest FIRST by created_at, so a fresh dispatch supersedes an older
|
|
3521
|
-
// failed schedule. ISO-8601 UTC compares correctly as strings.
|
|
3522
|
-
const candidates = perEvent
|
|
3523
|
-
.flatMap(result => result.runs)
|
|
3524
|
-
.filter(
|
|
3525
|
-
candidate => candidate && typeof candidate.created_at === "string"
|
|
3526
|
-
)
|
|
3527
|
-
// Equal timestamps compare 0 rather than falling through to -1, which
|
|
3528
|
-
// would make the sort unstable and let two runs created in the same
|
|
3529
|
-
// second swap places between reads of the same history.
|
|
3530
|
-
.sort((left, right) =>
|
|
3531
|
-
left.created_at === right.created_at
|
|
3532
|
-
? 0
|
|
3533
|
-
: left.created_at < right.created_at
|
|
3534
|
-
? 1
|
|
3535
|
-
: -1
|
|
3536
|
-
);
|
|
3537
3717
|
// Jobs are read for EVERY match mode, `run` included. A run-scoped suite
|
|
3538
3718
|
// needs them to prove the run was complete (row 26) — a `success` run
|
|
3539
3719
|
// that skipped half its jobs is not evidence about the half it skipped.
|
|
@@ -3543,9 +3723,10 @@ export async function observe(api, suites, branch, context, wait) {
|
|
|
3543
3723
|
const { run, jobs, jobsComplete, selection } = await selectScoredRun(
|
|
3544
3724
|
api,
|
|
3545
3725
|
candidates,
|
|
3546
|
-
|
|
3726
|
+
freshnessHours,
|
|
3547
3727
|
context.now,
|
|
3548
|
-
wait
|
|
3728
|
+
wait,
|
|
3729
|
+
{ file: suite.workflow, branch, diagnostics, verified: new Map() }
|
|
3549
3730
|
);
|
|
3550
3731
|
// Artifacts are read for every suite, not only the ones declaring
|
|
3551
3732
|
// `min_flows`: a run that recorded ITSELF as filtered disqualifies with no
|
|
@@ -3559,6 +3740,7 @@ export async function observe(api, suites, branch, context, wait) {
|
|
|
3559
3740
|
jobs,
|
|
3560
3741
|
jobsComplete,
|
|
3561
3742
|
selection,
|
|
3743
|
+
discovery: Object.freeze([...diagnostics]),
|
|
3562
3744
|
scope: readSuiteScope(artifacts),
|
|
3563
3745
|
};
|
|
3564
3746
|
})
|
|
@@ -4749,7 +4931,11 @@ async function main(argv) {
|
|
|
4749
4931
|
const failure = {
|
|
4750
4932
|
verdict: "fail",
|
|
4751
4933
|
blocked: true,
|
|
4752
|
-
error: {
|
|
4934
|
+
error: {
|
|
4935
|
+
kind,
|
|
4936
|
+
message,
|
|
4937
|
+
...(error.reason ? { reason: error.reason } : {}),
|
|
4938
|
+
},
|
|
4753
4939
|
findings: [],
|
|
4754
4940
|
};
|
|
4755
4941
|
// A downstream job reading `verdict`/`blocked` must see the gate's OWN
|