@codyswann/lisa 4.32.2 → 4.33.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/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +94 -6
- package/all/copy-overwrite/scripts/lisa-gates.mjs +259 -5
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +20 -39
- package/all/merge/.claude/settings.json +1 -3
- package/cdk/merge/.claude/settings.json +1 -1
- package/dist/cli/doctor-declared-contexts.d.ts +1 -1
- package/dist/cli/doctor-declared-contexts.d.ts.map +1 -1
- package/dist/cli/doctor-declared-contexts.js +48 -7
- package/dist/cli/doctor-declared-contexts.js.map +1 -1
- package/dist/cli/gate-report-drift-section.d.ts.map +1 -1
- package/dist/cli/gate-report-drift-section.js +2 -0
- package/dist/cli/gate-report-drift-section.js.map +1 -1
- package/dist/configs/vitest/base.d.ts +81 -0
- package/dist/configs/vitest/base.d.ts.map +1 -1
- package/dist/configs/vitest/base.js +106 -0
- package/dist/configs/vitest/base.js.map +1 -1
- package/dist/configs/vitest/cdk.d.ts +2 -2
- package/dist/configs/vitest/cdk.d.ts.map +1 -1
- package/dist/configs/vitest/cdk.js +4 -2
- package/dist/configs/vitest/cdk.js.map +1 -1
- package/dist/configs/vitest/harper-fabric.d.ts +2 -2
- package/dist/configs/vitest/harper-fabric.d.ts.map +1 -1
- package/dist/configs/vitest/harper-fabric.js +4 -2
- package/dist/configs/vitest/harper-fabric.js.map +1 -1
- package/dist/configs/vitest/nestjs.d.ts +2 -2
- package/dist/configs/vitest/nestjs.d.ts.map +1 -1
- package/dist/configs/vitest/nestjs.js +4 -2
- package/dist/configs/vitest/nestjs.js.map +1 -1
- package/dist/configs/vitest/phaser.d.ts +2 -2
- package/dist/configs/vitest/phaser.d.ts.map +1 -1
- package/dist/configs/vitest/phaser.js +4 -2
- package/dist/configs/vitest/phaser.js.map +1 -1
- package/dist/configs/vitest/typescript.d.ts +2 -2
- package/dist/configs/vitest/typescript.d.ts.map +1 -1
- package/dist/configs/vitest/typescript.js +4 -2
- package/dist/configs/vitest/typescript.js.map +1 -1
- package/dist/core/gate-context-owners.d.ts +18 -0
- package/dist/core/gate-context-owners.d.ts.map +1 -1
- package/dist/core/gate-context-owners.js +7 -0
- package/dist/core/gate-context-owners.js.map +1 -1
- package/dist/core/gate-declaration-drift.d.ts +37 -0
- package/dist/core/gate-declaration-drift.d.ts.map +1 -1
- package/dist/core/gate-declaration-drift.js +100 -26
- package/dist/core/gate-declaration-drift.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +47 -29
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/declared-checks-inspection.d.ts.map +1 -1
- package/dist/health/declared-checks-inspection.js +23 -7
- package/dist/health/declared-checks-inspection.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/prune-retired-claude-deny-rules.d.ts +39 -0
- package/dist/migrations/prune-retired-claude-deny-rules.d.ts.map +1 -0
- package/dist/migrations/prune-retired-claude-deny-rules.js +152 -0
- package/dist/migrations/prune-retired-claude-deny-rules.js.map +1 -0
- package/expo/merge/.claude/settings.json +1 -1
- package/harper-fabric/merge/.claude/settings.json +1 -1
- package/nestjs/merge/.claude/settings.json +1 -1
- package/package.json +4 -4
- package/phaser/merge/.claude/settings.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-github-add-journey/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-ticket-triage/SKILL.md +8 -0
- package/plugins/lisa/hooks/inject-resolved-config.mjs +12 -4
- package/plugins/lisa/hooks/inject-resolved-config.sh +39 -8
- package/plugins/lisa/rules/eager/control-reachability.md +39 -0
- package/plugins/lisa/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/lisa/rules/eager/work-item-definition-of-ready.md +1 -1
- package/plugins/lisa/rules/reference/control-reachability.md +122 -0
- package/plugins/lisa/rules/reference/falsifiable-checks.md +2 -0
- package/plugins/lisa/skills/lisa-github-add-journey/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/lisa/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/lisa/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/lisa/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-ticket-triage/SKILL.md +8 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-add-journey/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/lisa-agy/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/lisa-agy/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/lisa-agy/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-ticket-triage/SKILL.md +8 -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/hooks/inject-resolved-config.mjs +12 -4
- package/plugins/lisa-copilot/hooks/inject-resolved-config.sh +39 -8
- package/plugins/lisa-copilot/rules/eager/control-reachability.md +39 -0
- package/plugins/lisa-copilot/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/lisa-copilot/rules/eager/work-item-definition-of-ready.md +1 -1
- package/plugins/lisa-copilot/rules/reference/control-reachability.md +122 -0
- package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-github-add-journey/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/lisa-copilot/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/lisa-copilot/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/lisa-copilot/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-ticket-triage/SKILL.md +8 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/inject-resolved-config.mjs +12 -4
- package/plugins/lisa-cursor/hooks/inject-resolved-config.sh +39 -8
- package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +127 -0
- package/plugins/lisa-cursor/rules/control-reachability.mdc +44 -0
- package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +2 -0
- package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +2 -0
- package/plugins/lisa-cursor/rules/work-item-definition-of-ready.mdc +1 -1
- package/plugins/lisa-cursor/skills/lisa-github-add-journey/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/lisa-cursor/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/lisa-cursor/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/lisa-cursor/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-ticket-triage/SKILL.md +8 -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/hooks/inject-resolved-config.mjs +12 -4
- package/plugins/src/base/hooks/inject-resolved-config.sh +39 -8
- package/plugins/src/base/rules/eager/control-reachability.md +39 -0
- package/plugins/src/base/rules/eager/falsifiable-checks.md +2 -0
- package/plugins/src/base/rules/eager/work-item-definition-of-ready.md +1 -1
- package/plugins/src/base/rules/reference/control-reachability.md +122 -0
- package/plugins/src/base/rules/reference/falsifiable-checks.md +2 -0
- package/plugins/src/base/skills/lisa-github-add-journey/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +28 -1
- package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-jira-add-journey/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +28 -1
- package/plugins/src/base/skills/lisa-linear-add-journey/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +27 -0
- package/plugins/src/base/skills/lisa-reproduce-bug/SKILL.md +8 -0
- package/plugins/src/base/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-ticket-triage/SKILL.md +8 -0
- package/rails/merge/.claude/settings.json +1 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/merge/.claude/settings.json +1 -1
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
* measurement when the suite that produced it finished.
|
|
30
30
|
* @module lib/gate-failure-diagnosis
|
|
31
31
|
*/
|
|
32
|
+
import { availableParallelism, loadavg } from "node:os";
|
|
33
|
+
|
|
34
|
+
/** Default load-average source for {@link machineLoad}. */
|
|
35
|
+
const osLoadavg = () => loadavg();
|
|
36
|
+
|
|
37
|
+
/** Default core-count source for {@link machineLoad}. */
|
|
38
|
+
const osCores = () => availableParallelism();
|
|
32
39
|
|
|
33
40
|
/** What a failed gate's output was recognised as. */
|
|
34
41
|
export const DIAGNOSIS = Object.freeze({
|
|
@@ -445,9 +452,11 @@ function wasKilled(code) {
|
|
|
445
452
|
* sitting on top of. Nothing the output says is a verdict once the run was
|
|
446
453
|
* terminated.
|
|
447
454
|
* @param {number|null} code The exit code, or null when none was obtained.
|
|
455
|
+
* @param {LoadReading|null} [load] The machine's load at diagnosis time, from
|
|
456
|
+
* {@link machineLoad}. Omitted or null when it could not be read.
|
|
448
457
|
* @returns {Diagnosis} The verdict.
|
|
449
458
|
*/
|
|
450
|
-
function killedVerdict(code) {
|
|
459
|
+
function killedVerdict(code, load) {
|
|
451
460
|
const named = SIGNAL_EXITS[code];
|
|
452
461
|
const cause =
|
|
453
462
|
named === undefined
|
|
@@ -460,10 +469,85 @@ function killedVerdict(code) {
|
|
|
460
469
|
`reached no verdict, and its output is a truncated transcript rather ` +
|
|
461
470
|
`than a result. Re-run it on a quieter machine before reading anything ` +
|
|
462
471
|
`into what it printed`,
|
|
463
|
-
evidence:
|
|
472
|
+
evidence: loadEvidence(load ?? null),
|
|
464
473
|
};
|
|
465
474
|
}
|
|
466
475
|
|
|
476
|
+
/**
|
|
477
|
+
* The machine's run-queue pressure, or `null` when it cannot be read.
|
|
478
|
+
* @typedef {object} LoadReading
|
|
479
|
+
* @property {number} load1 One-minute load average.
|
|
480
|
+
* @property {number} cores Logical cores available to this process.
|
|
481
|
+
* @property {number} ratio `load1 / cores` — runnable work per core.
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Read the machine's current run-queue pressure.
|
|
486
|
+
*
|
|
487
|
+
* Injected rather than called inline so a test can state a load instead of
|
|
488
|
+
* inheriting whatever the test machine happened to be doing, which would make
|
|
489
|
+
* the assertion a coin flip on a busy box — the very condition this reading
|
|
490
|
+
* exists to describe.
|
|
491
|
+
* @param {() => number[]} [readLoadavg] Load-average source.
|
|
492
|
+
* @param {() => number} [readCores] Core-count source.
|
|
493
|
+
* @returns {LoadReading|null} The reading, or null when either source fails.
|
|
494
|
+
*/
|
|
495
|
+
export function machineLoad(readLoadavg = osLoadavg, readCores = osCores) {
|
|
496
|
+
try {
|
|
497
|
+
const load1 = readLoadavg()[0];
|
|
498
|
+
const cores = readCores();
|
|
499
|
+
if (!Number.isFinite(load1) || !Number.isFinite(cores) || cores <= 0) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return { load1, cores, ratio: load1 / cores };
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Turn a load reading into the one line an operator needs beside a kill.
|
|
510
|
+
*
|
|
511
|
+
* A kill that says only "the machine was busy" is still prose. What decides
|
|
512
|
+
* whether to re-run or to investigate is the NUMBER, and the number cuts both
|
|
513
|
+
* ways on purpose: an oversubscribed box says re-run, and a quiet one says the
|
|
514
|
+
* saturation story does not hold here, go and find the real killer. Reporting
|
|
515
|
+
* only the first case would make this a rubber stamp for "not my change".
|
|
516
|
+
*
|
|
517
|
+
* The reading is taken when the failure is diagnosed, which is after the kill,
|
|
518
|
+
* so it lags — load is falling by then. The line says so rather than implying a
|
|
519
|
+
* precision it does not have.
|
|
520
|
+
* @param {LoadReading|null} load The reading, or null when unavailable.
|
|
521
|
+
* @returns {string[]} Zero or one evidence line.
|
|
522
|
+
*/
|
|
523
|
+
function loadEvidence(load) {
|
|
524
|
+
if (load === null) return [];
|
|
525
|
+
const ratio = load.ratio.toFixed(1);
|
|
526
|
+
const where = `load1 ${load.load1.toFixed(1)} across ${load.cores} core(s), ${ratio}x per core`;
|
|
527
|
+
return load.ratio >= SATURATED_RATIO
|
|
528
|
+
? [
|
|
529
|
+
`machine at diagnosis (after the kill, so this is a floor): ${where}. ` +
|
|
530
|
+
`A box at this level terminates processes to survive; read this as ` +
|
|
531
|
+
`saturation, not as a defect in the change.`,
|
|
532
|
+
]
|
|
533
|
+
: [
|
|
534
|
+
`machine at diagnosis (after the kill, so this is a floor): ${where}. ` +
|
|
535
|
+
`That is not saturation, so contention does not explain this kill — ` +
|
|
536
|
+
`look for a timeout, an OOM, or an operator interrupt.`,
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Runnable work per core above which the box is treated as saturated.
|
|
542
|
+
*
|
|
543
|
+
* Two, not one. A load average equal to the core count is a machine that is
|
|
544
|
+
* fully used, which is what a test suite is supposed to do; twice that is a
|
|
545
|
+
* machine where every runnable thread is waiting behind another one. The
|
|
546
|
+
* sighting behind this issue was 21x, and the state after the fleet had already
|
|
547
|
+
* been asked to throttle was 8.3x, so the boundary is not close to either arm.
|
|
548
|
+
*/
|
|
549
|
+
const SATURATED_RATIO = 2;
|
|
550
|
+
|
|
467
551
|
/**
|
|
468
552
|
* The verdict for a failure whose transcript never reached this module.
|
|
469
553
|
*
|
|
@@ -530,10 +614,11 @@ function emptyTranscriptVerdict() {
|
|
|
530
614
|
* when the runner could not capture it.
|
|
531
615
|
* @param {number|null|undefined} code The command's exit code, or null when the
|
|
532
616
|
* runner could not obtain one.
|
|
617
|
+
* @param {LoadReading|null} load The machine's load at diagnosis time.
|
|
533
618
|
* @returns {object} What the failure was, before it is attributed.
|
|
534
619
|
*/
|
|
535
|
-
function classify(output, code) {
|
|
536
|
-
if (wasKilled(code)) return killedVerdict(code ?? null);
|
|
620
|
+
function classify(output, code, load) {
|
|
621
|
+
if (wasKilled(code)) return killedVerdict(code ?? null, load);
|
|
537
622
|
|
|
538
623
|
if (typeof output !== "string") return unavailableVerdict();
|
|
539
624
|
|
|
@@ -597,9 +682,12 @@ function classify(output, code) {
|
|
|
597
682
|
* @param {number|null|undefined} [code] The command's exit code. Omitted by a
|
|
598
683
|
* caller that has none; a caller that HAS one must pass it, because a kill is
|
|
599
684
|
* only legible in the exit code and never in the output.
|
|
685
|
+
* @param {LoadReading|null} [load] The machine's load, for a kill's evidence
|
|
686
|
+
* line. Defaults to reading this machine; pass `null` to suppress the line,
|
|
687
|
+
* or a fixed reading to make the output deterministic.
|
|
600
688
|
* @returns {Diagnosis} What the failure was, and whose it was.
|
|
601
689
|
*/
|
|
602
|
-
export function diagnoseFailure(output, code) {
|
|
603
|
-
const verdict = classify(output, code);
|
|
690
|
+
export function diagnoseFailure(output, code, load = machineLoad()) {
|
|
691
|
+
const verdict = classify(output, code, load);
|
|
604
692
|
return { ...verdict, proves: ATTRIBUTION[verdict.kind] ?? null };
|
|
605
693
|
}
|
|
@@ -69,11 +69,30 @@
|
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
import { createHash } from "node:crypto";
|
|
72
|
-
import {
|
|
73
|
-
|
|
72
|
+
import {
|
|
73
|
+
appendFileSync,
|
|
74
|
+
existsSync,
|
|
75
|
+
readdirSync,
|
|
76
|
+
readFileSync,
|
|
77
|
+
statSync,
|
|
78
|
+
} from "node:fs";
|
|
79
|
+
import { createRequire } from "node:module";
|
|
80
|
+
import { dirname, join, parse } from "node:path";
|
|
81
|
+
import { fileURLToPath } from "node:url";
|
|
74
82
|
|
|
75
83
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
76
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The npm manifest filename, named once.
|
|
87
|
+
*
|
|
88
|
+
* Three call sites read one: two resolve a project's own scripts, and the
|
|
89
|
+
* third walks up from a resolved package entry to find the manifest that owns
|
|
90
|
+
* it. A literal repeated across all three is a typo away from a lookup that
|
|
91
|
+
* silently answers "no manifest" — which every one of those readers is written
|
|
92
|
+
* to treat as an ordinary absence.
|
|
93
|
+
*/
|
|
94
|
+
const PACKAGE_MANIFEST = "package.json";
|
|
95
|
+
|
|
77
96
|
/** Enforcement levels a moment may carry. */
|
|
78
97
|
export const LEVELS = ["required", "optional", "off"];
|
|
79
98
|
|
|
@@ -3798,7 +3817,7 @@ export function readGates(cwd = process.cwd()) {
|
|
|
3798
3817
|
* @returns {Record<string, string>|null} The scripts, or null when unknown.
|
|
3799
3818
|
*/
|
|
3800
3819
|
export function projectScripts(cwd = process.cwd()) {
|
|
3801
|
-
const path = join(cwd,
|
|
3820
|
+
const path = join(cwd, PACKAGE_MANIFEST);
|
|
3802
3821
|
if (!existsSync(path)) return null;
|
|
3803
3822
|
try {
|
|
3804
3823
|
const scripts = JSON.parse(readFileSync(path, "utf8"))?.scripts;
|
|
@@ -3852,7 +3871,7 @@ function aliasFor({ declared, registryTask, shippedAs, scripts }) {
|
|
|
3852
3871
|
* @returns {Record<string, string>} The scripts block.
|
|
3853
3872
|
*/
|
|
3854
3873
|
export function readScripts(cwd = process.cwd()) {
|
|
3855
|
-
const path = join(cwd,
|
|
3874
|
+
const path = join(cwd, PACKAGE_MANIFEST);
|
|
3856
3875
|
if (!existsSync(path)) return {};
|
|
3857
3876
|
let manifest;
|
|
3858
3877
|
try {
|
|
@@ -5994,6 +6013,217 @@ function readPostedNames(source) {
|
|
|
5994
6013
|
return [...names];
|
|
5995
6014
|
}
|
|
5996
6015
|
|
|
6016
|
+
/* ─── Which Lisa produced this report ───────────────────────────────────── */
|
|
6017
|
+
|
|
6018
|
+
/**
|
|
6019
|
+
* The package whose version answers "which Lisa is installed here".
|
|
6020
|
+
*
|
|
6021
|
+
* Named once. `registryVersion()` resolves it and then checks the manifest it
|
|
6022
|
+
* lands on carries this exact name, and the two must not be able to disagree —
|
|
6023
|
+
* a resolution checked against a different literal is a version attached to
|
|
6024
|
+
* whatever package happened to answer.
|
|
6025
|
+
*/
|
|
6026
|
+
const LISA_PACKAGE = "@codyswann/lisa";
|
|
6027
|
+
|
|
6028
|
+
/**
|
|
6029
|
+
* What an unestablished identity field renders as.
|
|
6030
|
+
*
|
|
6031
|
+
* A word rather than an empty string, and never a fallback value. An operator
|
|
6032
|
+
* comparing two gate reports has to be able to tell "this ran a Lisa whose
|
|
6033
|
+
* version could not be resolved" from "this report has no version field at
|
|
6034
|
+
* all", and a blank renders as the second. Guessing renders as neither: the
|
|
6035
|
+
* host application's version in this slot is the specific failure
|
|
6036
|
+
* `registryVersion()` returns null to avoid.
|
|
6037
|
+
*/
|
|
6038
|
+
export const IDENTITY_UNKNOWN = "unknown";
|
|
6039
|
+
|
|
6040
|
+
/**
|
|
6041
|
+
* The shipped enforcement artifacts whose COPY decides a verdict.
|
|
6042
|
+
*
|
|
6043
|
+
* A workflow ref answers "which wrapper ran". It does not answer "which copy
|
|
6044
|
+
* of the checked-in script did that wrapper execute", and when the enforcement
|
|
6045
|
+
* is a file living in the consuming repository those are independent facts —
|
|
6046
|
+
* the second one is the one that produces the verdict. Four live copies of one
|
|
6047
|
+
* enforcement script have been observed at once (a repository's default
|
|
6048
|
+
* branch, a pull request's head, an installed release, and upstream), two of
|
|
6049
|
+
* them sharing a version string, so the identity emitted here is a digest of
|
|
6050
|
+
* the bytes rather than a declared version.
|
|
6051
|
+
*/
|
|
6052
|
+
const IDENTITY_ARTIFACTS = Object.freeze([
|
|
6053
|
+
"lisa-gates.mjs",
|
|
6054
|
+
"lisa-run-gates.mjs",
|
|
6055
|
+
]);
|
|
6056
|
+
|
|
6057
|
+
/** The directory the executing copy of this registry lives in. */
|
|
6058
|
+
const SCRIPTS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
6059
|
+
|
|
6060
|
+
/**
|
|
6061
|
+
* The `@codyswann/lisa` version that owns the resolver behind this run.
|
|
6062
|
+
*
|
|
6063
|
+
* Resolve the package entry from this file, then walk to the owning manifest.
|
|
6064
|
+
* That works in both shipped layouts: inside the installed package and after
|
|
6065
|
+
* Lisa emits this registry into a host project's `scripts/` directory. Reading
|
|
6066
|
+
* a fixed relative path from the emitted copy escapes the package, while
|
|
6067
|
+
* reading `../package.json` confidently reports the host application's version.
|
|
6068
|
+
*
|
|
6069
|
+
* The package-name check stays load-bearing. It makes an unexpected resolution
|
|
6070
|
+
* return null rather than attaching another package's version to this registry.
|
|
6071
|
+
* @returns {string|null} The version, or null when it cannot be established.
|
|
6072
|
+
*/
|
|
6073
|
+
export function registryVersion() {
|
|
6074
|
+
try {
|
|
6075
|
+
const entry = createRequire(import.meta.url).resolve(LISA_PACKAGE);
|
|
6076
|
+
const root = parse(entry).root;
|
|
6077
|
+
let directory = dirname(entry);
|
|
6078
|
+
while (true) {
|
|
6079
|
+
const manifest = join(directory, PACKAGE_MANIFEST);
|
|
6080
|
+
if (existsSync(manifest)) {
|
|
6081
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
6082
|
+
if (parsed.name === LISA_PACKAGE) return parsed.version ?? null;
|
|
6083
|
+
}
|
|
6084
|
+
if (directory === root) return null;
|
|
6085
|
+
directory = dirname(directory);
|
|
6086
|
+
}
|
|
6087
|
+
} catch {
|
|
6088
|
+
return null;
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
/**
|
|
6093
|
+
* A content digest of one file, or null when its bytes cannot be read.
|
|
6094
|
+
*
|
|
6095
|
+
* Twelve hex characters, not sixty-four. The question this answers is "did
|
|
6096
|
+
* these two surfaces run the same bytes", which is a comparison between two
|
|
6097
|
+
* printed lines an operator reads side by side; a full digest is not more
|
|
6098
|
+
* comparable and is materially less readable. Null rather than a placeholder
|
|
6099
|
+
* string, so a caller can tell "absent" from a digest that happens to look odd.
|
|
6100
|
+
* @param {string} file Path to the artifact.
|
|
6101
|
+
* @returns {string|null} `sha256:<12 hex>`, or null when unreadable.
|
|
6102
|
+
*/
|
|
6103
|
+
export function artifactDigest(file) {
|
|
6104
|
+
try {
|
|
6105
|
+
const hex = createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
6106
|
+
return `sha256:${hex.slice(0, 12)}`;
|
|
6107
|
+
} catch {
|
|
6108
|
+
return null;
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6112
|
+
/**
|
|
6113
|
+
* WHICH Lisa is running: the surface, the version, the ref, and the bytes.
|
|
6114
|
+
*
|
|
6115
|
+
* ONE COMPUTATION, SEVERAL RENDERINGS. A gate report from CI and a gate report
|
|
6116
|
+
* from a pre-push hook are claims about different code at different versions —
|
|
6117
|
+
* a consumer pins `@codyswann/lisa` for the local hook and calls the reusable
|
|
6118
|
+
* workflow at a floating ref for CI — and neither report used to say which. So
|
|
6119
|
+
* an observation about gate behaviour could not be dated, and a true warning
|
|
6120
|
+
* went stale with nothing in either repository able to notice.
|
|
6121
|
+
*
|
|
6122
|
+
* Every renderer below reads this object rather than resolving a version of
|
|
6123
|
+
* its own, because a second resolution path is a second answer that can
|
|
6124
|
+
* disagree with the first.
|
|
6125
|
+
* @param {object} [options] Inputs.
|
|
6126
|
+
* @param {Record<string,string|undefined>} [options.env] The environment.
|
|
6127
|
+
* @param {string} [options.directory] Where the enforcement scripts live.
|
|
6128
|
+
* @returns {object} The identity record.
|
|
6129
|
+
*/
|
|
6130
|
+
export function lisaIdentity({
|
|
6131
|
+
env = process.env,
|
|
6132
|
+
directory = SCRIPTS_DIRECTORY,
|
|
6133
|
+
} = {}) {
|
|
6134
|
+
return {
|
|
6135
|
+
surface: env.GITHUB_ACTIONS === "true" ? "ci" : "local",
|
|
6136
|
+
registry_version: registryVersion(),
|
|
6137
|
+
workflow_ref: env.GITHUB_WORKFLOW_REF ?? null,
|
|
6138
|
+
workflow_sha: env.GITHUB_WORKFLOW_SHA ?? null,
|
|
6139
|
+
artifacts: Object.fromEntries(
|
|
6140
|
+
IDENTITY_ARTIFACTS.map(name => [
|
|
6141
|
+
name,
|
|
6142
|
+
artifactDigest(join(directory, name)),
|
|
6143
|
+
])
|
|
6144
|
+
),
|
|
6145
|
+
};
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
/**
|
|
6149
|
+
* The workflow half of an identity, rendered for a reader.
|
|
6150
|
+
* @param {object} identity As returned by `lisaIdentity`.
|
|
6151
|
+
* @returns {string} `<ref>@<sha>`, or `unknown` when nothing was established.
|
|
6152
|
+
*/
|
|
6153
|
+
function identityWorkflow(identity) {
|
|
6154
|
+
if (!identity.workflow_ref) return IDENTITY_UNKNOWN;
|
|
6155
|
+
return `${identity.workflow_ref}@${identity.workflow_sha ?? IDENTITY_UNKNOWN}`;
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
/**
|
|
6159
|
+
* One operator-readable line naming which Lisa produced a report.
|
|
6160
|
+
*
|
|
6161
|
+
* Cheap enough to appear in ORDINARY output, which is the property that
|
|
6162
|
+
* matters. A stamp only an adjudicator sees helps a reader who is already
|
|
6163
|
+
* suspicious; the observations that go stale unchallenged are the ones nobody
|
|
6164
|
+
* had a reason to check.
|
|
6165
|
+
* @param {object} identity As returned by `lisaIdentity`.
|
|
6166
|
+
* @returns {string} The line, with `unknown` wherever nothing was established.
|
|
6167
|
+
*/
|
|
6168
|
+
export function formatIdentityLine(identity) {
|
|
6169
|
+
return [
|
|
6170
|
+
`🔖 Lisa identity — surface=${identity.surface}`,
|
|
6171
|
+
`package=${LISA_PACKAGE}@${identity.registry_version ?? IDENTITY_UNKNOWN}`,
|
|
6172
|
+
`workflow=${identityWorkflow(identity)}`,
|
|
6173
|
+
...Object.entries(identity.artifacts ?? {}).map(
|
|
6174
|
+
([name, sha]) => `${name}=${sha ?? IDENTITY_UNKNOWN}`
|
|
6175
|
+
),
|
|
6176
|
+
].join(" · ");
|
|
6177
|
+
}
|
|
6178
|
+
|
|
6179
|
+
/**
|
|
6180
|
+
* The same identity as a run-summary block.
|
|
6181
|
+
* @param {object} identity As returned by `lisaIdentity`.
|
|
6182
|
+
* @returns {string} Markdown for `$GITHUB_STEP_SUMMARY`.
|
|
6183
|
+
*/
|
|
6184
|
+
export function formatIdentityMarkdown(identity) {
|
|
6185
|
+
const version = identity.registry_version ?? IDENTITY_UNKNOWN;
|
|
6186
|
+
const rows = [
|
|
6187
|
+
["surface", identity.surface],
|
|
6188
|
+
["package", `${LISA_PACKAGE}@${version}`],
|
|
6189
|
+
["workflow ref", identity.workflow_ref ?? IDENTITY_UNKNOWN],
|
|
6190
|
+
["workflow sha", identity.workflow_sha ?? IDENTITY_UNKNOWN],
|
|
6191
|
+
...Object.entries(identity.artifacts ?? {}).map(([name, sha]) => [
|
|
6192
|
+
name,
|
|
6193
|
+
sha ?? IDENTITY_UNKNOWN,
|
|
6194
|
+
]),
|
|
6195
|
+
];
|
|
6196
|
+
return [
|
|
6197
|
+
"### 🔖 Which Lisa ran this",
|
|
6198
|
+
"",
|
|
6199
|
+
"| field | value |",
|
|
6200
|
+
"| --- | --- |",
|
|
6201
|
+
...rows.map(([field, value]) => `| ${field} | \`${value}\` |`),
|
|
6202
|
+
"",
|
|
6203
|
+
"Every gate report on this run was produced by the Lisa above. A report " +
|
|
6204
|
+
"from the other surface — the local pre-push gate — prints the same " +
|
|
6205
|
+
"fields, so the two can be compared without either repository: equal " +
|
|
6206
|
+
"digests mean the same enforcement bytes ran, and a differing " +
|
|
6207
|
+
"`workflow sha` dates any claim made from this run.",
|
|
6208
|
+
].join("\n");
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
/**
|
|
6212
|
+
* Append a block to the run summary, or report that there was nowhere to.
|
|
6213
|
+
* @param {string} markdown The block to append.
|
|
6214
|
+
* @returns {boolean} Whether it was written.
|
|
6215
|
+
*/
|
|
6216
|
+
function appendRunSummary(markdown) {
|
|
6217
|
+
const target = process.env.GITHUB_STEP_SUMMARY;
|
|
6218
|
+
if (!target) return false;
|
|
6219
|
+
try {
|
|
6220
|
+
appendFileSync(target, `${markdown}\n`);
|
|
6221
|
+
return true;
|
|
6222
|
+
} catch {
|
|
6223
|
+
return false;
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
|
|
5997
6227
|
/**
|
|
5998
6228
|
* CLI entry point.
|
|
5999
6229
|
*/
|
|
@@ -6003,6 +6233,30 @@ function main() {
|
|
|
6003
6233
|
const hit = rest.find(arg => arg.startsWith(`--${name}=`));
|
|
6004
6234
|
return hit ? hit.slice(name.length + 3) : null;
|
|
6005
6235
|
};
|
|
6236
|
+
// FIRST, AND BEFORE ANY GATE IS RESOLVED. This command answers "which Lisa
|
|
6237
|
+
// is about to run", so it must be answerable on a project whose declarations
|
|
6238
|
+
// are the thing under suspicion — including one whose config this registry
|
|
6239
|
+
// would refuse. It reads no gates and cannot fail on them.
|
|
6240
|
+
if (command === "identity") {
|
|
6241
|
+
const identity = lisaIdentity();
|
|
6242
|
+
if (rest.includes("--json")) {
|
|
6243
|
+
console.log(JSON.stringify(identity, null, 2));
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6246
|
+
const line = formatIdentityLine(identity);
|
|
6247
|
+
if (flag("format") === "github") {
|
|
6248
|
+
// The annotation AND the run summary, not one of them: an annotation is
|
|
6249
|
+
// what a reader sees beside a failing gate, and the summary is what
|
|
6250
|
+
// survives into a link somebody pastes into a ticket a week later. The
|
|
6251
|
+
// stale claim this exists to refute is always read later.
|
|
6252
|
+
console.log(`::notice title=Lisa identity::${line}`);
|
|
6253
|
+
appendRunSummary(formatIdentityMarkdown(identity));
|
|
6254
|
+
return;
|
|
6255
|
+
}
|
|
6256
|
+
console.log(line);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6006
6260
|
const { runner, gates, policy, config } = readGates();
|
|
6007
6261
|
// Read once, here, so every subcommand answers about the same project. The
|
|
6008
6262
|
// resolver is handed the result rather than reading the disk itself, which
|
|
@@ -6399,7 +6653,7 @@ function main() {
|
|
|
6399
6653
|
}
|
|
6400
6654
|
|
|
6401
6655
|
throw new Error(
|
|
6402
|
-
"usage: lisa-gates.mjs validate|list|legs|quality-plan|reuse-plan|needs|contexts|retired-contexts|verify-contexts|skip-jobs|audit-config|inventory|unconfigured|seed"
|
|
6656
|
+
"usage: lisa-gates.mjs validate|list|legs|quality-plan|reuse-plan|needs|contexts|retired-contexts|verify-contexts|skip-jobs|audit-config|inventory|unconfigured|identity|seed"
|
|
6403
6657
|
);
|
|
6404
6658
|
}
|
|
6405
6659
|
|
|
@@ -49,9 +49,8 @@ import {
|
|
|
49
49
|
rmSync,
|
|
50
50
|
writeFileSync,
|
|
51
51
|
} from "node:fs";
|
|
52
|
-
import { createRequire } from "node:module";
|
|
53
52
|
import { tmpdir } from "node:os";
|
|
54
|
-
import { dirname, join,
|
|
53
|
+
import { dirname, join, relative } from "node:path";
|
|
55
54
|
import { fileURLToPath } from "node:url";
|
|
56
55
|
|
|
57
56
|
import { DIAGNOSIS, diagnoseFailure } from "./lib/gate-failure-diagnosis.mjs";
|
|
@@ -62,6 +61,8 @@ import {
|
|
|
62
61
|
configurationProblems,
|
|
63
62
|
declarationsAt,
|
|
64
63
|
digest,
|
|
64
|
+
formatIdentityLine,
|
|
65
|
+
lisaIdentity,
|
|
65
66
|
momentsExecutedBy,
|
|
66
67
|
planDigest,
|
|
67
68
|
projectScripts,
|
|
@@ -1196,40 +1197,6 @@ const EVIDENCE_STATUS_FOR = Object.freeze({
|
|
|
1196
1197
|
[STATE.NOT_RUN]: "unknown",
|
|
1197
1198
|
});
|
|
1198
1199
|
|
|
1199
|
-
/**
|
|
1200
|
-
* The `@codyswann/lisa` version that owns the resolver behind this run.
|
|
1201
|
-
*
|
|
1202
|
-
* Resolve the package entry from this file, then walk to the owning manifest.
|
|
1203
|
-
* That works in both shipped layouts: inside the installed package and after
|
|
1204
|
-
* Lisa emits this runner into a host project's `scripts/` directory. Reading a
|
|
1205
|
-
* fixed relative path from the emitted copy escapes the package, while reading
|
|
1206
|
-
* `../package.json` confidently reports the host application's version.
|
|
1207
|
-
*
|
|
1208
|
-
* The package-name check stays load-bearing. It makes an unexpected resolution
|
|
1209
|
-
* return null rather than attaching another package's version to this registry.
|
|
1210
|
-
* @returns {string|null} The version, or null when it cannot be established.
|
|
1211
|
-
*/
|
|
1212
|
-
function registryVersion() {
|
|
1213
|
-
try {
|
|
1214
|
-
const entry = createRequire(import.meta.url).resolve("@codyswann/lisa");
|
|
1215
|
-
let directory = dirname(entry);
|
|
1216
|
-
const root = parse(directory).root;
|
|
1217
|
-
while (true) {
|
|
1218
|
-
const manifest = join(directory, "package.json");
|
|
1219
|
-
if (existsSync(manifest)) {
|
|
1220
|
-
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
1221
|
-
if (parsed.name === "@codyswann/lisa") {
|
|
1222
|
-
return parsed.version ?? null;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (directory === root) return null;
|
|
1226
|
-
directory = dirname(directory);
|
|
1227
|
-
}
|
|
1228
|
-
} catch {
|
|
1229
|
-
return null;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
1200
|
/**
|
|
1234
1201
|
* One revision from git, or null when git cannot answer.
|
|
1235
1202
|
*
|
|
@@ -1316,9 +1283,15 @@ function evidenceContract({ moment, gates, runner, scripts = null }) {
|
|
|
1316
1283
|
moment,
|
|
1317
1284
|
runner: runner ?? null,
|
|
1318
1285
|
gates_digest: planDigest({ gates, moment, runner, scripts }),
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1286
|
+
// SPREAD, not three hand-copied fields. The printed header and this
|
|
1287
|
+
// document have to be the same claim: a reader comparing a pasted gate
|
|
1288
|
+
// line against a stored envelope and finding them disagree learns nothing
|
|
1289
|
+
// except that Lisa has two answers. `surface` and `artifacts` ride along,
|
|
1290
|
+
// and they are the half that says WHICH COPY of the enforcement script
|
|
1291
|
+
// produced the verdict — a fact a workflow ref cannot carry, because the
|
|
1292
|
+
// wrapper's identity and the enforcement's identity are independent when
|
|
1293
|
+
// the enforcement is a file living in the consuming repository.
|
|
1294
|
+
...lisaIdentity(),
|
|
1322
1295
|
inputs_digest: inputsDigest(),
|
|
1323
1296
|
};
|
|
1324
1297
|
}
|
|
@@ -1619,6 +1592,14 @@ function main() {
|
|
|
1619
1592
|
return EXIT.RUNNER_FAILED;
|
|
1620
1593
|
}
|
|
1621
1594
|
|
|
1595
|
+
// PRINTED BEFORE ANY GATE RUNS, so it is present whether they pass, fail,
|
|
1596
|
+
// are optional, or never run at all. Every early return below is a report an
|
|
1597
|
+
// operator may quote later, and a quoted report that cannot be dated is the
|
|
1598
|
+
// defect this line exists to close: the local hook runs the version this
|
|
1599
|
+
// project PINNED, while CI runs a floating ref, so two reports about "the
|
|
1600
|
+
// gate" are routinely claims about different code.
|
|
1601
|
+
console.log(formatIdentityLine(lisaIdentity()));
|
|
1602
|
+
|
|
1622
1603
|
/**
|
|
1623
1604
|
* Record this run, and refuse to report a verdict we could not record.
|
|
1624
1605
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* operator to delete a required check; those warn and say what to declare.
|
|
16
16
|
* @module cli/doctor-declared-contexts
|
|
17
17
|
*/
|
|
18
|
-
import type
|
|
18
|
+
import { type DeclarationDriftReport } from "../core/gate-declaration-drift.js";
|
|
19
19
|
import type { DoctorCheck } from "./doctor.js";
|
|
20
20
|
/**
|
|
21
21
|
* Turn a completed comparison into one operator-readable line.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-declared-contexts.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-declared-contexts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"doctor-declared-contexts.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-declared-contexts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,mCAAmC,CAAC;AAM3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,sBAAsB,GAC7B,WAAW,CAyDb;AAqFD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAiCtB"}
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report where the settings file and the ruleset that enforces it disagree.
|
|
3
|
+
*
|
|
4
|
+
* The machine payload under `--json` carries the whole comparison, three states
|
|
5
|
+
* and all. This is the line an operator reads without asking for JSON, and it
|
|
6
|
+
* is deliberately the OFFLINE half: doctor is documented as making no network
|
|
7
|
+
* call, and the template is what a repository's protection is provisioned
|
|
8
|
+
* from, so a disagreement with it is drift the moment anyone provisions —
|
|
9
|
+
* whether or not the live ruleset has caught up. The live half runs in
|
|
10
|
+
* `lisa health`, which already reaches GitHub.
|
|
11
|
+
*
|
|
12
|
+
* A `warn` here is not a softer `fail`. Two of the verdicts are contradictions
|
|
13
|
+
* — the two surfaces state opposite things and one of them is wrong — and
|
|
14
|
+
* those fail. The other two are silences, and a silence must not send an
|
|
15
|
+
* operator to delete a required check; those warn and say what to declare.
|
|
16
|
+
* @module cli/doctor-declared-contexts
|
|
17
|
+
*/
|
|
18
|
+
import { isContradiction, isGap, } from "../core/gate-declaration-drift.js";
|
|
1
19
|
import { readConfig } from "./gate-report-config.js";
|
|
2
20
|
import { readFacadeFacts } from "./gate-report-facade.js";
|
|
3
21
|
import { buildDeclarationDrift } from "./gate-report-joins.js";
|
|
@@ -41,8 +59,9 @@ export function declaredContextsCheck(report) {
|
|
|
41
59
|
detail: `${named(retired.map(entry => entry.context))} can never report: Lisa renamed the job that used to post ${retired.length === 1 ? "it" : "them"}. ${retired[0]?.detail ?? ""} Run \`lisa health\` to sweep the LIVE rulesets, including any this repository hand-made, for the same name.`,
|
|
42
60
|
};
|
|
43
61
|
}
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
// Asked of the comparator rather than re-listed here — see the note on the
|
|
63
|
+
// same call in `health/declared-checks-inspection`.
|
|
64
|
+
const contradictions = report.entries.filter(entry => isContradiction(entry.verdict));
|
|
46
65
|
if (contradictions.length > 0) {
|
|
47
66
|
return {
|
|
48
67
|
name: NAME,
|
|
@@ -50,9 +69,7 @@ export function declaredContextsCheck(report) {
|
|
|
50
69
|
detail: `The settings file and the ruleset template say opposite things about ${named(contradictions.map(entry => entry.context))}. ${contradictions[0]?.detail ?? ""} Fix whichever is wrong; nothing here proposes removing a check.`,
|
|
51
70
|
};
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
const optional = report.entries.filter(entry => entry.verdict === "enforced-declared-optional");
|
|
55
|
-
if (undeclared.length === 0 && optional.length === 0) {
|
|
72
|
+
if (!report.entries.some(entry => isGap(entry.verdict))) {
|
|
56
73
|
return {
|
|
57
74
|
name: NAME,
|
|
58
75
|
status: "ok",
|
|
@@ -69,14 +86,38 @@ export function declaredContextsCheck(report) {
|
|
|
69
86
|
// whose gate IS declared — which is untrue and sends the operator looking
|
|
70
87
|
// for a declaration that is already there.
|
|
71
88
|
detail: [
|
|
72
|
-
undeclaredClause(undeclared),
|
|
73
|
-
optionalClause(optional),
|
|
89
|
+
undeclaredClause(entriesWith(report, "enforced-undeclared")),
|
|
90
|
+
optionalClause(entriesWith(report, "enforced-declared-optional")),
|
|
91
|
+
awaitedClause(entriesWith(report, "enforced-awaited-elsewhere")),
|
|
74
92
|
"Do NOT remove the required check to make this line go away.",
|
|
75
93
|
]
|
|
76
94
|
.filter(clause => clause.length > 0)
|
|
77
95
|
.join(" "),
|
|
78
96
|
};
|
|
79
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The entries carrying one verdict.
|
|
100
|
+
* @param report - The comparison
|
|
101
|
+
* @param verdict - The verdict to collect
|
|
102
|
+
* @returns The matching entries
|
|
103
|
+
*/
|
|
104
|
+
function entriesWith(report, verdict) {
|
|
105
|
+
return report.entries.filter(entry => entry.verdict === verdict);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The clause for contexts a gate's declaration awaits a different signal for.
|
|
109
|
+
* @param entries - The awaited-elsewhere entries
|
|
110
|
+
* @returns One sentence, or an empty string
|
|
111
|
+
*/
|
|
112
|
+
function awaitedClause(entries) {
|
|
113
|
+
if (entries.length === 0)
|
|
114
|
+
return "";
|
|
115
|
+
const gates = gateIdsOf(entries);
|
|
116
|
+
const awaited = named([
|
|
117
|
+
...new Set(entries.flatMap(entry => entry.awaitedInstead === null ? [] : [entry.awaitedInstead])),
|
|
118
|
+
]);
|
|
119
|
+
return `The template also requires ${named(entries.map(entry => entry.context))} while the settings file declares ${gates} with an \`await:\` at pull-request, which promises ${awaited} as the merge condition instead. An awaited gate posts no job of its own, so unless a hand-written job posts these exact names they will never report and every pull request waits on them.`;
|
|
120
|
+
}
|
|
80
121
|
/**
|
|
81
122
|
* The clause for contexts whose gate the settings file never mentions.
|
|
82
123
|
* @param entries - The undeclared entries
|