@dev-loops/core 1.0.1 → 1.0.2-slim.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * gate-evidence-reconcile.mjs — deterministic self-heal for a stuck
3
+ * `gate-evidence` required status (issue #1935).
4
+ *
5
+ * The server-side `gate-evidence` check (`.github/workflows/gate-evidence.yml`)
6
+ * re-fires when a gate verdict is posted (ADR 0043). That native re-fire is
7
+ * racy: a verdict-post run can be CANCELLED by `cancel-in-progress` when a
8
+ * superseding event lands, or evaluate before the just-posted verdict is
9
+ * API-visible, leaving the required status stuck at `failure` even though a
10
+ * clean current-head `pre_approval_gate` verdict now exists. Nothing re-fires
11
+ * afterward, so the merge stays `UNSTABLE` until a manual `gh run rerun`
12
+ * (observed on PR #1934; ADR 0057).
13
+ *
14
+ * This pure decision separates the two cases the reconcile must never confuse:
15
+ * - evidence genuinely satisfied but the status is stuck non-green → re-fire
16
+ * the concrete run that posted the stale status (automating the manual
17
+ * rerun; the rerun re-evaluates LIVE evidence, which is now satisfied).
18
+ * - evidence genuinely NOT satisfied → do nothing. A head that truly lacks a
19
+ * clean current-head verdict MUST keep failing the check (fail-closed).
20
+ */
21
+
22
+ /** Required commit-status context posted by the gate-evidence workflow. */
23
+ export const GATE_EVIDENCE_STATUS_CONTEXT = "gate-evidence";
24
+
25
+ /**
26
+ * Extract the Actions run id from a gate-evidence commit-status `target_url`.
27
+ * The workflow points every posted status at its own run:
28
+ * https://github.com/<owner>/<repo>/actions/runs/<run_id>
29
+ * Returns the numeric run id as a string, or null when the URL is absent or
30
+ * not an Actions-run URL (an unexpected target_url must not be coerced).
31
+ *
32
+ * @param {string} [targetUrl]
33
+ * @returns {string|null}
34
+ */
35
+ export function parseRunIdFromTargetUrl(targetUrl) {
36
+ if (typeof targetUrl !== "string") return null;
37
+ const match = targetUrl.match(/\/actions\/runs\/(\d+)(?:[/?#]|$)/);
38
+ return match ? match[1] : null;
39
+ }
40
+
41
+ /**
42
+ * Decide whether a stuck `gate-evidence` status should be re-fired.
43
+ *
44
+ * @param {object} input
45
+ * @param {boolean} input.evidenceSatisfied detect-checkpoint-evidence reports
46
+ * `evidenceState === "satisfied"` for the current head (clean draft_gate +
47
+ * current-head pre_approval_gate verdicts present).
48
+ * @param {string} [input.statusState] the `gate-evidence` commit-status state
49
+ * on the current head: `success` | `failure` | `error` | `pending` | `none`
50
+ * (`none` = no gate-evidence status posted for this head yet).
51
+ * @param {string|null} [input.runId] the Actions run id that posted the stale
52
+ * status (from `parseRunIdFromTargetUrl`), or null when unknown.
53
+ * @returns {{ action: "refire"|"none", runId?: string, reason: string }}
54
+ */
55
+ export function resolveGateEvidenceStatusReconcile({ evidenceSatisfied, statusState, runId } = {}) {
56
+ // Fail-closed: never re-fire when the verdict evidence is genuinely not
57
+ // satisfied. This preserves the "verdict genuinely missing" case — the head
58
+ // keeps failing closed exactly as before (issue #1935 AC #3).
59
+ if (evidenceSatisfied !== true) {
60
+ return { action: "none", reason: "evidence-not-satisfied-fail-closed" };
61
+ }
62
+ // Already green — nothing to reconcile.
63
+ if (statusState === "success") {
64
+ return { action: "none", reason: "already-success" };
65
+ }
66
+ // Evidence IS satisfied for the current head, but the required status is not
67
+ // success (the push-before-verdict race: a cancelled/stale re-fire). Re-fire
68
+ // the concrete run that posted the stale status. Without a run id there is
69
+ // nothing to re-fire deterministically; leave it to the native path rather
70
+ // than forging a status.
71
+ if (!runId) {
72
+ return { action: "none", reason: "no-run-to-refire" };
73
+ }
74
+ return { action: "refire", runId, reason: "evidence-satisfied-status-stale" };
75
+ }