@codyswann/lisa 3.2.0 → 3.4.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/cli/doctor-lisa-owned-artifacts.d.ts.map +1 -1
- package/dist/cli/doctor-lisa-owned-artifacts.js +41 -2
- package/dist/cli/doctor-lisa-owned-artifacts.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +90 -16
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +21 -2
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- package/plugins/lisa/rules/eager/design-source-of-truth.md +38 -0
- package/plugins/lisa/rules/eager/do-it-now.md +11 -0
- package/plugins/lisa/rules/eager/empirical-inquiry.md +1 -0
- package/plugins/lisa/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/lisa/rules/eager/integration-access-layer.md +5 -0
- package/plugins/lisa/rules/eager/learnings-ladder.md +18 -0
- package/plugins/lisa/rules/eager/local-ci-first.md +15 -0
- package/plugins/lisa/rules/eager/not-blocked-just-waiting.md +13 -0
- package/plugins/lisa/rules/eager/session-status-updates.md +19 -0
- package/plugins/lisa/rules/eager/wiki-knowledge-source.md +6 -11
- package/plugins/lisa/rules/reference/design-source-of-truth.md +195 -0
- package/plugins/lisa/rules/reference/do-it-now.md +37 -0
- package/plugins/lisa/rules/reference/empirical-inquiry.md +1 -0
- package/plugins/lisa/rules/reference/falsifiable-checks.md +24 -2
- package/plugins/lisa/rules/reference/integration-access-layer.md +8 -0
- package/plugins/lisa/rules/reference/learnings-ladder.md +41 -0
- package/plugins/lisa/rules/reference/local-ci-first.md +34 -0
- package/plugins/lisa/rules/reference/not-blocked-just-waiting.md +41 -0
- package/plugins/lisa/rules/reference/session-status-updates.md +60 -0
- package/plugins/lisa/rules/reference/wiki-knowledge-source.md +21 -5
- package/plugins/lisa/scripts/design-source-gate.mjs +530 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/lisa/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/lisa/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-source-gate.mjs +530 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/lisa-agy/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/lisa-agy/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- 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-copilot/rules/eager/design-source-of-truth.md +38 -0
- package/plugins/lisa-copilot/rules/eager/do-it-now.md +11 -0
- package/plugins/lisa-copilot/rules/eager/empirical-inquiry.md +1 -0
- package/plugins/lisa-copilot/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/lisa-copilot/rules/eager/integration-access-layer.md +5 -0
- package/plugins/lisa-copilot/rules/eager/learnings-ladder.md +18 -0
- package/plugins/lisa-copilot/rules/eager/local-ci-first.md +15 -0
- package/plugins/lisa-copilot/rules/eager/not-blocked-just-waiting.md +13 -0
- package/plugins/lisa-copilot/rules/eager/session-status-updates.md +19 -0
- package/plugins/lisa-copilot/rules/eager/wiki-knowledge-source.md +6 -11
- package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +195 -0
- package/plugins/lisa-copilot/rules/reference/do-it-now.md +37 -0
- package/plugins/lisa-copilot/rules/reference/empirical-inquiry.md +1 -0
- package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +24 -2
- package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +8 -0
- package/plugins/lisa-copilot/rules/reference/learnings-ladder.md +41 -0
- package/plugins/lisa-copilot/rules/reference/local-ci-first.md +34 -0
- package/plugins/lisa-copilot/rules/reference/not-blocked-just-waiting.md +41 -0
- package/plugins/lisa-copilot/rules/reference/session-status-updates.md +60 -0
- package/plugins/lisa-copilot/rules/reference/wiki-knowledge-source.md +21 -5
- package/plugins/lisa-copilot/scripts/design-source-gate.mjs +530 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/lisa-copilot/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +200 -0
- package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +43 -0
- package/plugins/lisa-cursor/rules/do-it-now-reference.mdc +42 -0
- package/plugins/lisa-cursor/rules/do-it-now.mdc +16 -0
- package/plugins/lisa-cursor/rules/empirical-inquiry-reference.mdc +1 -0
- package/plugins/lisa-cursor/rules/empirical-inquiry.mdc +1 -0
- package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +24 -2
- package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +2 -0
- package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +8 -0
- package/plugins/lisa-cursor/rules/integration-access-layer.mdc +5 -0
- package/plugins/lisa-cursor/rules/learnings-ladder-reference.mdc +46 -0
- package/plugins/lisa-cursor/rules/learnings-ladder.mdc +23 -0
- package/plugins/lisa-cursor/rules/local-ci-first-reference.mdc +39 -0
- package/plugins/lisa-cursor/rules/local-ci-first.mdc +20 -0
- package/plugins/lisa-cursor/rules/not-blocked-just-waiting-reference.mdc +46 -0
- package/plugins/lisa-cursor/rules/not-blocked-just-waiting.mdc +18 -0
- package/plugins/lisa-cursor/rules/session-status-updates-reference.mdc +65 -0
- package/plugins/lisa-cursor/rules/session-status-updates.mdc +24 -0
- package/plugins/lisa-cursor/rules/wiki-knowledge-source-reference.mdc +21 -5
- package/plugins/lisa-cursor/rules/wiki-knowledge-source.mdc +7 -12
- package/plugins/lisa-cursor/scripts/design-source-gate.mjs +530 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/lisa-cursor/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- 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/plugins/src/base/rules/eager/design-source-of-truth.md +38 -0
- package/plugins/src/base/rules/eager/do-it-now.md +11 -0
- package/plugins/src/base/rules/eager/empirical-inquiry.md +1 -0
- package/plugins/src/base/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/src/base/rules/eager/integration-access-layer.md +5 -0
- package/plugins/src/base/rules/eager/learnings-ladder.md +18 -0
- package/plugins/src/base/rules/eager/local-ci-first.md +15 -0
- package/plugins/src/base/rules/eager/not-blocked-just-waiting.md +13 -0
- package/plugins/src/base/rules/eager/session-status-updates.md +19 -0
- package/plugins/src/base/rules/eager/wiki-knowledge-source.md +6 -11
- package/plugins/src/base/rules/reference/design-source-of-truth.md +195 -0
- package/plugins/src/base/rules/reference/do-it-now.md +37 -0
- package/plugins/src/base/rules/reference/empirical-inquiry.md +1 -0
- package/plugins/src/base/rules/reference/falsifiable-checks.md +24 -2
- package/plugins/src/base/rules/reference/integration-access-layer.md +8 -0
- package/plugins/src/base/rules/reference/learnings-ladder.md +41 -0
- package/plugins/src/base/rules/reference/local-ci-first.md +34 -0
- package/plugins/src/base/rules/reference/not-blocked-just-waiting.md +41 -0
- package/plugins/src/base/rules/reference/session-status-updates.md +60 -0
- package/plugins/src/base/rules/reference/wiki-knowledge-source.md +21 -5
- package/plugins/src/base/scripts/design-source-gate.mjs +530 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-quality-review/SKILL.md +7 -0
- package/plugins/src/base/skills/lisa-review-local/SKILL.md +27 -0
- package/plugins/src/base/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +173 -8
- package/typescript/copy-overwrite/scripts/nightly-e2e-suites.schema.json +12 -0
|
@@ -86,7 +86,7 @@ import { pathToFileURL } from "node:url";
|
|
|
86
86
|
* rather than running a contract neither half agrees on. See §8 of
|
|
87
87
|
* `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
|
|
88
88
|
*/
|
|
89
|
-
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.
|
|
89
|
+
export const NIGHTLY_E2E_CONTRACT_VERSION = "1.3.0";
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* The conclusions that constitute a verdict about the code.
|
|
@@ -204,6 +204,24 @@ export const DEFAULT_BYPASS_REASON_PATTERN = REQUIRED_BYPASS_REASON_PATTERN;
|
|
|
204
204
|
/** Hard ceiling on how far out a bootstrap window may sit. */
|
|
205
205
|
export const BOOTSTRAP_ABSOLUTE_MAX_DAYS = 30;
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* How long a newly declared suite is forgiven for having no evidence yet.
|
|
209
|
+
*
|
|
210
|
+
* Bootstrap (§4) is one flag for the whole workflow, which made ADDING a suite
|
|
211
|
+
* a repository-wide wedge: the moment a fourth suite lands in the table of an
|
|
212
|
+
* armed repo its evidence is missing (row 9), and every pull request is blocked
|
|
213
|
+
* until that suite's first green nightly. The escapes were re-opening the
|
|
214
|
+
* GLOBAL window — un-arming the three suites that were working — or burning an
|
|
215
|
+
* audited bypass. Neither is a proportionate answer to adding a suite.
|
|
216
|
+
*
|
|
217
|
+
* Two weeks is a fortnight of nightlies: long enough to wire a suite up and
|
|
218
|
+
* burn its first failures down, short enough that forgetting the field is
|
|
219
|
+
* self-correcting. It is a DEFAULT, and `grace_days` may only shorten it —
|
|
220
|
+
* the ceiling it is checked against is `bootstrap_max_days`, the same
|
|
221
|
+
* forgiveness budget the global window spends from (§4.1).
|
|
222
|
+
*/
|
|
223
|
+
export const DEFAULT_SUITE_GRACE_DAYS = 14;
|
|
224
|
+
|
|
207
225
|
/** Hard ceiling on how stale a run may be and still speak for the branch. */
|
|
208
226
|
export const ABSOLUTE_MAX_FRESHNESS_HOURS = 720;
|
|
209
227
|
|
|
@@ -326,7 +344,15 @@ export class GateApiError extends Error {
|
|
|
326
344
|
|
|
327
345
|
/** Keys a suite entry may carry. Anything else is a typo, and typos fail. */
|
|
328
346
|
const SUITE_KEYS = Object.freeze(
|
|
329
|
-
new Set([
|
|
347
|
+
new Set([
|
|
348
|
+
"label",
|
|
349
|
+
"workflow",
|
|
350
|
+
"match",
|
|
351
|
+
"freshness_hours",
|
|
352
|
+
"required_sha",
|
|
353
|
+
"first_seen",
|
|
354
|
+
"grace_days",
|
|
355
|
+
])
|
|
330
356
|
);
|
|
331
357
|
|
|
332
358
|
/** Keys each match mode may carry. */
|
|
@@ -491,6 +517,39 @@ export function validateSuites(raw) {
|
|
|
491
517
|
);
|
|
492
518
|
}
|
|
493
519
|
}
|
|
520
|
+
// Rows 32-35 — the per-suite grace anchor and its length. Shape only; the
|
|
521
|
+
// WINDOW is resolved (and rejected) in `resolveSuiteGrace`, which is the
|
|
522
|
+
// one place the `bootstrap_max_days` ceiling is applied to it.
|
|
523
|
+
if (entry.first_seen !== undefined) {
|
|
524
|
+
if (
|
|
525
|
+
typeof entry.first_seen !== "string" ||
|
|
526
|
+
entry.first_seen.trim().length === 0
|
|
527
|
+
) {
|
|
528
|
+
throw new GateConfigError(
|
|
529
|
+
`${where}: \`first_seen\` must be an ISO-8601 UTC timestamp naming when this suite entered the table (e.g. "2026-08-10T00:00:00Z").`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (entry.grace_days !== undefined) {
|
|
534
|
+
// A knob with no anchor is a gate configured differently than its author
|
|
535
|
+
// believes — the same defect an ignored key is, so it fails the same way.
|
|
536
|
+
if (entry.first_seen === undefined) {
|
|
537
|
+
throw new GateConfigError(
|
|
538
|
+
`${where}: \`grace_days\` requires \`first_seen\`. A grace length with no anchor forgives nothing and reads as though it forgives everything.`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const days = entry.grace_days;
|
|
542
|
+
if (
|
|
543
|
+
typeof days !== "number" ||
|
|
544
|
+
!Number.isFinite(days) ||
|
|
545
|
+
days <= 0 ||
|
|
546
|
+
days > BOOTSTRAP_ABSOLUTE_MAX_DAYS
|
|
547
|
+
) {
|
|
548
|
+
throw new GateConfigError(
|
|
549
|
+
`${where}: \`grace_days\` must be a number in (0, ${BOOTSTRAP_ABSOLUTE_MAX_DAYS}]. A grace that outlives the bootstrap ceiling IS propswap's forever-bootstrap, whatever it is called — rejected rather than clamped, so widening it is a reviewable act.`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
494
553
|
if (entry.required_sha !== undefined) {
|
|
495
554
|
if (
|
|
496
555
|
typeof entry.required_sha !== "string" ||
|
|
@@ -777,6 +836,77 @@ export function resolveBootstrap(until, maxDays, now) {
|
|
|
777
836
|
});
|
|
778
837
|
}
|
|
779
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Resolves ONE suite's first-seen grace window (rows 32-35).
|
|
841
|
+
*
|
|
842
|
+
* The problem this exists for: bootstrap is workflow-global, so adding a suite
|
|
843
|
+
* to an armed repo blocks every pull request from the moment of the edit until
|
|
844
|
+
* that suite's first green nightly — and the only outs were re-opening the
|
|
845
|
+
* global window (which un-arms every suite that was already working) or an
|
|
846
|
+
* audited bypass. Neither is proportionate to the routine act of adding a
|
|
847
|
+
* suite, and both teach people that the gate is something to get around.
|
|
848
|
+
*
|
|
849
|
+
* What keeps this from becoming propswap's forever-bootstrap is the ANCHOR.
|
|
850
|
+
* The window is not a date somebody picks; it is `first_seen + grace_days`,
|
|
851
|
+
* and `first_seen` MAY NOT BE IN THE FUTURE. A future anchor would make this a
|
|
852
|
+
* hand-typed expiry under another name, extendable by one string edit forever —
|
|
853
|
+
* so it fails as misconfiguration, exactly as row 24 fails a bootstrap window
|
|
854
|
+
* beyond its cap. Rolling the anchor forward is still possible, and it is
|
|
855
|
+
* meant to be: it means writing "this suite is new" about a suite that is not,
|
|
856
|
+
* in a diff a reviewer reads.
|
|
857
|
+
*
|
|
858
|
+
* The ceiling is `bootstrap_max_days` — the SAME forgiveness budget the global
|
|
859
|
+
* window spends from, already clamped to `BOOTSTRAP_ABSOLUTE_MAX_DAYS` by
|
|
860
|
+
* `resolveSecurityLimits`. A grace that could outlive it would be a second,
|
|
861
|
+
* looser bootstrap wearing a per-suite hat.
|
|
862
|
+
*
|
|
863
|
+
* A window that lapsed long ago is INERT, never an error: cleaning the field
|
|
864
|
+
* up must stay optional, or the design buys a churn commit per suite per month
|
|
865
|
+
* and the first person to hit it deletes the anchor rather than the window.
|
|
866
|
+
*
|
|
867
|
+
* @param {object} suite - A validated suite entry
|
|
868
|
+
* @param {number} maxDays - Ceiling on how far out any forgiveness window may sit
|
|
869
|
+
* @param {Date} now - Evaluation instant
|
|
870
|
+
* @returns {{active: boolean, until: string|null, expiresInDays: number|null, firstSeen: string|null}} The window
|
|
871
|
+
* @throws {GateConfigError} When the anchor is unparseable, in the future, or the window exceeds the ceiling
|
|
872
|
+
*/
|
|
873
|
+
export function resolveSuiteGrace(suite, maxDays, now) {
|
|
874
|
+
const raw = suite?.first_seen;
|
|
875
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
876
|
+
return Object.freeze({
|
|
877
|
+
active: false,
|
|
878
|
+
until: null,
|
|
879
|
+
expiresInDays: null,
|
|
880
|
+
firstSeen: null,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const anchor = Date.parse(raw.trim());
|
|
884
|
+
if (Number.isNaN(anchor)) {
|
|
885
|
+
throw new GateConfigError(
|
|
886
|
+
`\`first_seen\` for suite ${JSON.stringify(suite.label)} is not an ISO-8601 timestamp: ${JSON.stringify(raw)}. Use e.g. "2026-08-10T00:00:00Z".`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (anchor > now.getTime()) {
|
|
890
|
+
throw new GateConfigError(
|
|
891
|
+
`\`first_seen\` for suite ${JSON.stringify(suite.label)} (${raw}) is in the future. A suite cannot have been first seen tomorrow, and an anchor that may sit in the future is a hand-typed expiry under another name — one string edit and the grace never ends.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const graceDays = suite.grace_days ?? DEFAULT_SUITE_GRACE_DAYS;
|
|
895
|
+
const untilMs = anchor + graceDays * 86_400_000;
|
|
896
|
+
const daysOut = (untilMs - now.getTime()) / 86_400_000;
|
|
897
|
+
if (daysOut > maxDays) {
|
|
898
|
+
throw new GateConfigError(
|
|
899
|
+
`The first-seen grace for suite ${JSON.stringify(suite.label)} runs ${Math.ceil(daysOut)} days out, beyond \`bootstrap_max_days\` (${maxDays}). Per-suite grace spends from the same forgiveness budget as the bootstrap window, so it fails as misconfiguration rather than being clamped — shorten \`grace_days\`, or raise the cap deliberately in the same review.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return Object.freeze({
|
|
903
|
+
active: untilMs > now.getTime(),
|
|
904
|
+
until: new Date(untilMs).toISOString(),
|
|
905
|
+
expiresInDays: Math.max(0, Math.ceil(daysOut)),
|
|
906
|
+
firstSeen: new Date(anchor).toISOString(),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
780
910
|
// ---------------------------------------------------------------------------
|
|
781
911
|
// 4. Bypass — maintainers only, no self-bypass, reason required, auto-expiring
|
|
782
912
|
// ---------------------------------------------------------------------------
|
|
@@ -924,13 +1054,21 @@ const BYPASS_REJECTIONS = Object.freeze({
|
|
|
924
1054
|
* carrying a stale label still reports `pass` rather than pretending the label
|
|
925
1055
|
* did something.
|
|
926
1056
|
*
|
|
1057
|
+
* A finding may carry its OWN window in `finding.grace` (rows 32-35), resolved
|
|
1058
|
+
* per suite from `first_seen`. It softens the same states the global window
|
|
1059
|
+
* softens and nothing more — `unknown` only, never `fail` — so grace forgives
|
|
1060
|
+
* absence of evidence and never evidence of failure, exactly as bootstrap does.
|
|
1061
|
+
* Two windows, one rule; a suite is forgiven when EITHER is open, which is what
|
|
1062
|
+
* lets a repo arm three suites and still add a fourth.
|
|
1063
|
+
*
|
|
927
1064
|
* @param {ReadonlyArray<object>} findings - Per-suite findings
|
|
928
1065
|
* @param {{bootstrap: object, bypass: object|null}} options - Window and bypass decision
|
|
929
1066
|
* @returns {{verdict: string, blocked: boolean, findings: ReadonlyArray<object>, bootstrap: object, bypass: object|null}} The verdict
|
|
930
1067
|
*/
|
|
931
1068
|
export function decide(findings, { bootstrap, bypass = null }) {
|
|
932
1069
|
const rendered = findings.map(finding =>
|
|
933
|
-
finding.state === SUITE_STATES.unknown &&
|
|
1070
|
+
finding.state === SUITE_STATES.unknown &&
|
|
1071
|
+
(bootstrap.active || finding.grace?.active === true)
|
|
934
1072
|
? { ...finding, state: "bootstrap" }
|
|
935
1073
|
: finding
|
|
936
1074
|
);
|
|
@@ -1027,7 +1165,14 @@ export function formatFinding(finding) {
|
|
|
1027
1165
|
? "not yet blocking"
|
|
1028
1166
|
: finding.state.toUpperCase();
|
|
1029
1167
|
const conclusion = finding.conclusion ? ` [${finding.conclusion}]` : "";
|
|
1030
|
-
|
|
1168
|
+
// The per-suite expiry rides on the LINE, not just in the trailing
|
|
1169
|
+
// paragraph: with one suite in grace and three armed, a reader has to be able
|
|
1170
|
+
// to tell which line is forgiven and until when. There is no quiet grace.
|
|
1171
|
+
const grace =
|
|
1172
|
+
finding.state === "bootstrap" && finding.grace?.active
|
|
1173
|
+
? ` — new suite (first seen ${finding.grace.firstSeen}); its grace expires ${finding.grace.until} (in ${finding.grace.expiresInDays} day(s)), after which this line blocks`
|
|
1174
|
+
: "";
|
|
1175
|
+
return `${marker} ${finding.label} — ${verdictWord}${conclusion}${when} — ${detail}${link}${grace}`;
|
|
1031
1176
|
}
|
|
1032
1177
|
|
|
1033
1178
|
/**
|
|
@@ -1056,6 +1201,19 @@ export function formatReport(verdict, context) {
|
|
|
1056
1201
|
);
|
|
1057
1202
|
}
|
|
1058
1203
|
|
|
1204
|
+
// Per-suite grace gets its own paragraph for the same reason bootstrap does:
|
|
1205
|
+
// a forgiveness nobody can see is a gate quietly measuring less than it reads
|
|
1206
|
+
// as measuring. Naming the suites keeps "which one is new?" off the reader.
|
|
1207
|
+
const inGrace = verdict.findings.filter(
|
|
1208
|
+
finding => finding.state === "bootstrap" && finding.grace?.active
|
|
1209
|
+
);
|
|
1210
|
+
if (inGrace.length > 0) {
|
|
1211
|
+
lines.push(
|
|
1212
|
+
`🌱 **New-suite grace active for ${inGrace.map(finding => `\`${finding.label}\``).join(", ")}.** A suite gets a bounded window from its \`first_seen\` anchor to produce its first verdict, so adding a suite cannot block every pull request until tomorrow's nightly. Every OTHER suite stays armed, evidence of FAILURE still blocks inside the window, and when the window lapses the line above blocks with no further action.`,
|
|
1213
|
+
""
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1059
1217
|
if (verdict.verdict === "bypassed") {
|
|
1060
1218
|
lines.push(
|
|
1061
1219
|
`⚠️ **Gate bypassed — audited.** Applied by \`${verdict.bypass.actor}\` at ${verdict.bypass.appliedAt}, expires ${verdict.bypass.expiresAt}. Ticket: \`${verdict.bypass.ticket}\`. Reason: ${verdict.bypass.detail}`,
|
|
@@ -1964,13 +2122,20 @@ export async function runGate(env, wait) {
|
|
|
1964
2122
|
settings.branch,
|
|
1965
2123
|
wait
|
|
1966
2124
|
);
|
|
1967
|
-
const findings = settings.suites.map((suite, index) =>
|
|
1968
|
-
assessSuite(suite, observations[index], {
|
|
2125
|
+
const findings = settings.suites.map((suite, index) => {
|
|
2126
|
+
const finding = assessSuite(suite, observations[index], {
|
|
1969
2127
|
branch: settings.branch,
|
|
1970
2128
|
freshnessHours: settings.freshnessHours,
|
|
1971
2129
|
now,
|
|
1972
|
-
})
|
|
1973
|
-
|
|
2130
|
+
});
|
|
2131
|
+
// Resolved for EVERY suite, so a misconfigured anchor fails the gate
|
|
2132
|
+
// whether or not its window is still open — a rule that only runs while it
|
|
2133
|
+
// would forgive something is a rule nobody notices breaking. The window is
|
|
2134
|
+
// attached only when the suite actually declared one, so an untouched
|
|
2135
|
+
// table produces byte-identical findings.
|
|
2136
|
+
const grace = resolveSuiteGrace(suite, settings.bootstrapMaxDays, now);
|
|
2137
|
+
return grace.firstSeen === null ? finding : { ...finding, grace };
|
|
2138
|
+
});
|
|
1974
2139
|
|
|
1975
2140
|
let bypass = null;
|
|
1976
2141
|
if (settings.pr.number && settings.pr.labels.includes(settings.bypassLabel)) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"type": "object",
|
|
12
12
|
"additionalProperties": false,
|
|
13
13
|
"required": ["label", "workflow", "match"],
|
|
14
|
+
"dependentRequired": { "grace_days": ["first_seen"] },
|
|
14
15
|
"properties": {
|
|
15
16
|
"label": {
|
|
16
17
|
"type": "string",
|
|
@@ -40,6 +41,17 @@
|
|
|
40
41
|
"type": "string",
|
|
41
42
|
"pattern": "^[0-9a-f]{40}$",
|
|
42
43
|
"description": "When present, the run's head_sha must equal this exact commit. Use only where the suite is expected to run on a pinned commit; freshness plus the branch filter is the normal staleness control."
|
|
44
|
+
},
|
|
45
|
+
"first_seen": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"format": "date-time",
|
|
48
|
+
"description": "ISO-8601 UTC timestamp naming when this suite entered the table — the ANCHOR of its per-suite grace window (truth-table rows 27-30). Set it when you ADD a suite to an already-armed repo: for `grace_days` (default 14) from this instant, MISSING evidence for THIS suite reports with its expiry but does not block, while every other suite stays armed. Evidence of FAILURE blocks inside the window too. It may NOT be in the future — a future anchor would be a hand-typed expiry under another name, extendable forever by one string edit — and a window running beyond `bootstrap_max_days` FAILS as misconfiguration rather than being clamped. A lapsed anchor is inert, so the field can be left in place."
|
|
49
|
+
},
|
|
50
|
+
"grace_days": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"exclusiveMinimum": 0,
|
|
53
|
+
"maximum": 30,
|
|
54
|
+
"description": "How many days after `first_seen` this suite's grace window lasts. Default 14. Requires `first_seen` — a grace length with no anchor forgives nothing and reads as though it forgives everything. Bounded by the same ceiling as the bootstrap window (`bootstrap_max_days`, hard ceiling 30): per-suite grace spends from the same forgiveness budget, and a grace that could outlive that ceiling is propswap's forever-bootstrap wearing a per-suite hat."
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
},
|