@codyswann/lisa 4.10.7 → 4.10.9
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/lisa-gates.mjs +82 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +2 -2
- package/dist/cli/doctor-declared-contexts.d.ts.map +1 -1
- package/dist/cli/doctor-declared-contexts.js +16 -0
- 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 +6 -0
- package/dist/cli/gate-report-drift-section.js.map +1 -1
- package/dist/cli/gate-report-joins.d.ts +0 -15
- package/dist/cli/gate-report-joins.d.ts.map +1 -1
- package/dist/cli/gate-report-joins.js +2 -1
- package/dist/cli/gate-report-joins.js.map +1 -1
- package/dist/cli/gate-report-registry.d.ts +8 -0
- package/dist/cli/gate-report-registry.d.ts.map +1 -1
- package/dist/cli/gate-report-registry.js.map +1 -1
- package/dist/cli/gate-report-types.d.ts +2 -1
- package/dist/cli/gate-report-types.d.ts.map +1 -1
- package/dist/cli/gate-report-types.js.map +1 -1
- package/dist/codex/scripts/block-no-verify.sh +1 -1
- package/dist/core/gate-context-owners.d.ts +122 -0
- package/dist/core/gate-context-owners.d.ts.map +1 -0
- package/dist/core/gate-context-owners.js +174 -0
- package/dist/core/gate-context-owners.js.map +1 -0
- package/dist/core/gate-declaration-drift.d.ts +56 -79
- package/dist/core/gate-declaration-drift.d.ts.map +1 -1
- package/dist/core/gate-declaration-drift.js +73 -115
- 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/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +10 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/declared-checks-inspection.d.ts +0 -22
- package/dist/health/declared-checks-inspection.d.ts.map +1 -1
- package/dist/health/declared-checks-inspection.js +44 -5
- package/dist/health/declared-checks-inspection.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +2 -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/hooks/block-no-verify.agy.sh +1 -1
- package/plugins/lisa/hooks/block-no-verify.sh +2 -2
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +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-copilot/hooks/block-no-verify.sh +2 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +2 -2
- 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/block-no-verify.agy.sh +1 -1
- package/plugins/src/base/hooks/block-no-verify.sh +2 -2
- package/scripts/lisa-github-rulesets.sh +138 -0
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +19 -14
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which gate produces a required status context, and under which caller chain.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `core/gate-declaration-drift` because it answers a different
|
|
5
|
+
* question. This module knows the shipped registry, the merge moment and the
|
|
6
|
+
* caller-chain rules, and produces one map from context string to owning gate.
|
|
7
|
+
* The comparator consumes that map as a `ReadonlyMap<string, ContextOwner>`
|
|
8
|
+
* and knows none of it. Nothing here reaches an enforcing surface or reasons
|
|
9
|
+
* about a verdict, so the two vocabularies the comparator's note warns about —
|
|
10
|
+
* the verdict table and the sentences explaining it — stay together over
|
|
11
|
+
* there, undisturbed by this seam.
|
|
12
|
+
*
|
|
13
|
+
* The one rule that lives here rather than in the comparator: a retired label
|
|
14
|
+
* some OTHER gate has since adopted as its current label is not retired. That
|
|
15
|
+
* string is posted again, by a different job, so requiring it is not a
|
|
16
|
+
* permanent wait — and telling an operator to delete it would be this check
|
|
17
|
+
* inventing a defect. The guard is why the map is built from the WHOLE
|
|
18
|
+
* registry rather than one entry's field.
|
|
19
|
+
* @module core/gate-context-owners
|
|
20
|
+
*/
|
|
21
|
+
/** The moment a branch ruleset guards. */
|
|
22
|
+
export const MERGE_MOMENT = "pull-request";
|
|
23
|
+
/**
|
|
24
|
+
* The declaration state one resolved level names.
|
|
25
|
+
*
|
|
26
|
+
* Anything that is not one of the three levels is `not-declared`, because an
|
|
27
|
+
* unknown level is a typo rather than a claim.
|
|
28
|
+
* @param level - The resolved level
|
|
29
|
+
* @returns The declaration state
|
|
30
|
+
*/
|
|
31
|
+
function asDeclaration(level) {
|
|
32
|
+
return level === "required" || level === "optional" || level === "off"
|
|
33
|
+
? level
|
|
34
|
+
: "not-declared";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Map every context a registry gate can produce to that gate.
|
|
38
|
+
*
|
|
39
|
+
* Built from the whole registry rather than from the declared gates, which is
|
|
40
|
+
* the difference between "protection requires a context whose gate nobody
|
|
41
|
+
* declared" and "protection requires a context Lisa knows nothing about". Those
|
|
42
|
+
* are different findings with different remedies, and a map built only from
|
|
43
|
+
* declarations could not tell them apart.
|
|
44
|
+
* @param options - Inputs
|
|
45
|
+
* @param options.registry - The shipped registry
|
|
46
|
+
* @param options.gates - The project's gates block
|
|
47
|
+
* @param options.workflowName - The workflow whose name prefixes a context
|
|
48
|
+
* @returns Context string to owning gate
|
|
49
|
+
*/
|
|
50
|
+
export function contextOwners(options) {
|
|
51
|
+
const { registry, gates, workflowName } = options;
|
|
52
|
+
const resolved = resolveMergeMoment(registry, gates);
|
|
53
|
+
const family = registry.momentFamily(MERGE_MOMENT);
|
|
54
|
+
const owners = Object.entries(registry.REGISTRY).map(([gateId, definition]) => {
|
|
55
|
+
const hit = resolved.get(gateId);
|
|
56
|
+
return [
|
|
57
|
+
`${prefixFor(registry, hit?.callerChain, workflowName)} / ${definition.label}`,
|
|
58
|
+
{
|
|
59
|
+
gateId,
|
|
60
|
+
declaration: asDeclaration(hit?.level),
|
|
61
|
+
legalAtMerge: definition.moments.includes(family),
|
|
62
|
+
retired: null,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
});
|
|
66
|
+
const awaited = [...resolved.values()].flatMap(hit => hit.mode === "await" && hit.awaits !== null
|
|
67
|
+
? [
|
|
68
|
+
[
|
|
69
|
+
hit.awaits,
|
|
70
|
+
{
|
|
71
|
+
gateId: hit.id,
|
|
72
|
+
declaration: asDeclaration(hit.level),
|
|
73
|
+
legalAtMerge: registry.REGISTRY[hit.id]?.moments.includes(family) === true,
|
|
74
|
+
retired: null,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
]
|
|
78
|
+
: []);
|
|
79
|
+
// Retired entries go FIRST so a later current-label or awaited entry always
|
|
80
|
+
// wins the key. The collision filter below already covers the reachable
|
|
81
|
+
// case; the ordering covers the one nobody thought of.
|
|
82
|
+
return new Map([
|
|
83
|
+
...retiredOwners(registry, resolved, workflowName, family),
|
|
84
|
+
...owners,
|
|
85
|
+
...awaited,
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The contexts the registry records as renamed away, keyed by their old name.
|
|
90
|
+
*
|
|
91
|
+
* Built from the whole registry — like the current-label map, and for the same
|
|
92
|
+
* reason. It is what separates "a required context nothing in Lisa produces"
|
|
93
|
+
* (which may be CodeRabbit, a Sonar status, a manual status, or a job the host
|
|
94
|
+
* repository's own CI defines, and is not a defect) from "a required context
|
|
95
|
+
* Lisa itself retired" (which no run can post, ever). Only the second is
|
|
96
|
+
* reportable, and only the second earns a removal remedy.
|
|
97
|
+
*
|
|
98
|
+
* A retired label some other gate has since adopted as its CURRENT label is
|
|
99
|
+
* skipped: that string is posted again, by a different job, so requiring it is
|
|
100
|
+
* not a permanent wait. Missing this would turn a legitimate requirement into
|
|
101
|
+
* a false "delete this" instruction.
|
|
102
|
+
* @param registry - The shipped registry
|
|
103
|
+
* @param resolved - The resolved merge moment, keyed by gate id
|
|
104
|
+
* @param workflowName - The caller chain that prefixes a run gate's context
|
|
105
|
+
* @param family - The merge moment's family
|
|
106
|
+
* @returns Retired context to owning gate
|
|
107
|
+
*/
|
|
108
|
+
function retiredOwners(registry, resolved, workflowName, family) {
|
|
109
|
+
const live = new Set(Object.values(registry.REGISTRY).map(definition => definition.label));
|
|
110
|
+
return Object.entries(registry.REGISTRY).flatMap(([gateId, definition]) => (definition.previousLabels ?? [])
|
|
111
|
+
.filter(label => !live.has(label))
|
|
112
|
+
.map((label) => [
|
|
113
|
+
`${workflowName} / ${label}`,
|
|
114
|
+
{
|
|
115
|
+
gateId,
|
|
116
|
+
declaration: asDeclaration(resolved.get(gateId)?.level),
|
|
117
|
+
legalAtMerge: definition.moments.includes(family),
|
|
118
|
+
retired: {
|
|
119
|
+
label,
|
|
120
|
+
replacement: `${workflowName} / ${definition.label}`,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
]));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The prefix one gate's context carries, honouring a per-declaration override.
|
|
127
|
+
*
|
|
128
|
+
* Three states, and the third is the one that matters. No override means the
|
|
129
|
+
* caller-wide name, unchanged — which is what keeps every existing declaration
|
|
130
|
+
* deriving exactly what it derived before this field existed. An override with
|
|
131
|
+
* a registry able to join it means that gate's own chain. An override with a
|
|
132
|
+
* registry too old to join it is refused outright, because the alternative is
|
|
133
|
+
* to derive the caller-wide name for a gate whose declaration has just said
|
|
134
|
+
* that name is wrong — a comparison that would report a clean match against a
|
|
135
|
+
* context nothing posts.
|
|
136
|
+
* @param registry - The shipped registry
|
|
137
|
+
* @param chain - The declared override, when there is one
|
|
138
|
+
* @param workflowName - The caller-wide chain
|
|
139
|
+
* @returns The prefix
|
|
140
|
+
* @throws {Error} When an override cannot be joined
|
|
141
|
+
*/
|
|
142
|
+
function prefixFor(registry, chain, workflowName) {
|
|
143
|
+
if (chain === null || chain === undefined)
|
|
144
|
+
return workflowName;
|
|
145
|
+
if (registry.callerPrefix === undefined) {
|
|
146
|
+
throw new Error(`a gate declares its own caller chain (${JSON.stringify(chain)}), but ` +
|
|
147
|
+
`the installed copy of Lisa's check registry is too old to derive a ` +
|
|
148
|
+
`context from one. Update Lisa, or remove the override; deriving the ` +
|
|
149
|
+
`default name here would report agreement about a check that never ` +
|
|
150
|
+
`reports.`);
|
|
151
|
+
}
|
|
152
|
+
return registry.callerPrefix(chain);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the merge moment, treating a refusal as "nothing resolved".
|
|
156
|
+
*
|
|
157
|
+
* A refusal is never swallowed by the caller: `readTemplateEnforcement` and the
|
|
158
|
+
* health check both report the declaration problems separately. What must not
|
|
159
|
+
* happen here is a throw that turns a comparison into no comparison at all.
|
|
160
|
+
* @param registry - The shipped registry
|
|
161
|
+
* @param gates - The project's gates block
|
|
162
|
+
* @returns Resolved gates keyed by id
|
|
163
|
+
*/
|
|
164
|
+
function resolveMergeMoment(registry, gates) {
|
|
165
|
+
try {
|
|
166
|
+
return new Map(registry
|
|
167
|
+
.resolveMoment({ gates, moment: MERGE_MOMENT, includeOff: true })
|
|
168
|
+
.map(entry => [entry.id, { ...entry }]));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return new Map();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=gate-context-owners.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-context-owners.js","sourceRoot":"","sources":["../../src/core/gate-context-owners.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,22 +26,34 @@
|
|
|
26
26
|
* "agreed and proved" from "agreed and unproved" would itself be a control
|
|
27
27
|
* reporting more than it measured, so the word this module uses is
|
|
28
28
|
* `matched` — the two surfaces agree — and never `enforced` or `proved`.
|
|
29
|
-
* 3. **
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
29
|
+
* 3. **A remedy may say "remove the context" only where Lisa renamed the job
|
|
30
|
+
* itself.** The original rule here was that no remedy could ever say it,
|
|
31
|
+
* and the reason was sound: third-party contexts are enforced by
|
|
32
|
+
* construction and declared by nobody, so a comparator free to propose a
|
|
33
|
+
* removal would eventually propose deleting one. That reason does not cover
|
|
34
|
+
* one case, and #3067 is what the gap costs. When the shipped registry
|
|
35
|
+
* carries a `previousLabels` entry, Lisa is not inferring that nothing owns
|
|
36
|
+
* a context — it is reading its OWN record that it renamed the job, which
|
|
37
|
+
* is proof that this exact string can never be posted again. A required
|
|
38
|
+
* context that never reports does not fail a pull request; GitHub waits for
|
|
39
|
+
* it forever, so leaving that unsaid red-walls the repository silently.
|
|
38
40
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* The narrowing is structural, not conventional. Exactly one verdict,
|
|
42
|
+
* `enforced-context-retired`, maps to the single removal remedy, and that
|
|
43
|
+
* verdict is reachable only from a `previousLabels` match. A context with
|
|
44
|
+
* no owner at all is still `enforced-not-lisa-owned` with remedy `none`, so
|
|
45
|
+
* the original guarantee holds unchanged for every third-party check.
|
|
46
|
+
*
|
|
47
|
+
* The ownership map this comparison reads — which gate produces a context
|
|
48
|
+
* string, under which caller chain, and whether the registry records that
|
|
49
|
+
* name as renamed away — is built in `core/gate-context-owners`. That is a
|
|
50
|
+
* different question from the one here: it knows the registry and the chain
|
|
51
|
+
* rules and nothing about verdicts, and this module knows the verdicts and
|
|
52
|
+
* reads the map through `ReadonlyMap<string, ContextOwner>` without knowing
|
|
53
|
+
* how it was derived. The seam is the one the signatures already drew.
|
|
54
|
+
* @module core/gate-declaration-drift
|
|
43
55
|
*/
|
|
44
|
-
|
|
56
|
+
import { type ContextOwner, type DeclarationState } from "./gate-context-owners.js";
|
|
45
57
|
/** Which enforcing surface a comparison was made against. */
|
|
46
58
|
export type DriftSurface = "ruleset-templates" | "live-ruleset";
|
|
47
59
|
/**
|
|
@@ -62,13 +74,34 @@ export type DriftVerdict =
|
|
|
62
74
|
/** Enforced, and a registry gate produces it, but nothing declares it. */
|
|
63
75
|
| "enforced-undeclared"
|
|
64
76
|
/** Enforced, and no registry gate produces it — a third-party check. */
|
|
65
|
-
| "enforced-not-lisa-owned"
|
|
77
|
+
| "enforced-not-lisa-owned"
|
|
78
|
+
/**
|
|
79
|
+
* Enforced, and Lisa's own registry records that it RENAMED the job posting
|
|
80
|
+
* it, so nothing will ever post this name again.
|
|
81
|
+
*
|
|
82
|
+
* Not a failure and not a gap — an absence. GitHub holds a required context
|
|
83
|
+
* that never reports at "Expected — Waiting for status to be reported"
|
|
84
|
+
* indefinitely: no red tick, no log, `mergeable: MERGEABLE` with
|
|
85
|
+
* `mergeStateStatus: BLOCKED`, and every pull request in the repository
|
|
86
|
+
* red-walled with nothing naming the cause.
|
|
87
|
+
*/
|
|
88
|
+
| "enforced-context-retired";
|
|
66
89
|
/**
|
|
67
90
|
* What to do about one verdict.
|
|
68
91
|
*
|
|
69
92
|
* Deliberately closed, and deliberately without a removal. See the module note.
|
|
70
93
|
*/
|
|
71
|
-
export type DriftRemedy = "none" | "declare-the-gate" | "enforce-the-context" | "resolve-the-contradiction" | "decide-which-surface-wins"
|
|
94
|
+
export type DriftRemedy = "none" | "declare-the-gate" | "enforce-the-context" | "resolve-the-contradiction" | "decide-which-surface-wins"
|
|
95
|
+
/**
|
|
96
|
+
* The one removal in the vocabulary, and reachable from exactly one verdict.
|
|
97
|
+
*
|
|
98
|
+
* See the module note: it is not "this context looks unowned, drop it". It
|
|
99
|
+
* is "Lisa's registry records that Lisa renamed this job, so this exact
|
|
100
|
+
* string cannot be posted by anything, ever". A context with no owner at all
|
|
101
|
+
* never reaches this remedy — it is `enforced-not-lisa-owned`, whose remedy
|
|
102
|
+
* is still `none`.
|
|
103
|
+
*/
|
|
104
|
+
| "stop-requiring-the-retired-context";
|
|
72
105
|
/** One required status context, and where the requirement was read from. */
|
|
73
106
|
export interface EnforcedContext {
|
|
74
107
|
/** The context string, exactly as the ruleset spells it. */
|
|
@@ -82,15 +115,6 @@ export interface EnforcedContext {
|
|
|
82
115
|
*/
|
|
83
116
|
readonly source: string;
|
|
84
117
|
}
|
|
85
|
-
/** The gate a context belongs to, and what the settings file says about it. */
|
|
86
|
-
export interface ContextOwner {
|
|
87
|
-
/** Registry gate id. */
|
|
88
|
-
readonly gateId: string;
|
|
89
|
-
/** The declaration at the merge moment. */
|
|
90
|
-
readonly declaration: DeclarationState;
|
|
91
|
-
/** Whether the registry permits declaring this gate at the merge moment. */
|
|
92
|
-
readonly legalAtMerge: boolean;
|
|
93
|
-
}
|
|
94
118
|
/** One context, its verdict, and the evidence behind it. */
|
|
95
119
|
export interface DeclarationDriftEntry {
|
|
96
120
|
/** The context string. */
|
|
@@ -122,64 +146,17 @@ export interface DeclarationDriftReport {
|
|
|
122
146
|
readonly contradictions: number;
|
|
123
147
|
/** Entries whose verdict is a gap the declaration surface could close. */
|
|
124
148
|
readonly gaps: number;
|
|
125
|
-
}
|
|
126
|
-
/** The slice of the shipped registry this comparison reads. */
|
|
127
|
-
export interface MergeContextRegistry {
|
|
128
|
-
readonly REGISTRY: Readonly<Record<string, {
|
|
129
|
-
readonly label: string;
|
|
130
|
-
readonly moments: readonly string[];
|
|
131
|
-
}>>;
|
|
132
|
-
readonly resolveMoment: (options: {
|
|
133
|
-
gates: Record<string, unknown>;
|
|
134
|
-
moment: string;
|
|
135
|
-
includeOff?: boolean;
|
|
136
|
-
}) => readonly {
|
|
137
|
-
readonly id: string;
|
|
138
|
-
readonly level: string;
|
|
139
|
-
readonly mode: string;
|
|
140
|
-
readonly awaits: string | null;
|
|
141
|
-
/**
|
|
142
|
-
* The chain this declaration overrode the caller-wide one with, raw.
|
|
143
|
-
*
|
|
144
|
-
* Optional because a consumer may hold an older copy of the shipped
|
|
145
|
-
* registry that predates the field. Absent reads as "no override", which
|
|
146
|
-
* is what every registry before it meant.
|
|
147
|
-
*/
|
|
148
|
-
readonly callerChain?: readonly string[] | string | null;
|
|
149
|
-
}[];
|
|
150
|
-
readonly momentFamily: (moment: string) => string;
|
|
151
149
|
/**
|
|
152
|
-
*
|
|
150
|
+
* Entries the enforcing surface requires and nothing can post.
|
|
153
151
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
152
|
+
* Counted separately from `contradictions` and `gaps` because it is neither.
|
|
153
|
+
* A contradiction has two surfaces saying opposite things and a gap has one
|
|
154
|
+
* surface silent; this has one surface waiting on a name that no longer
|
|
155
|
+
* exists. Folding it into either bucket would give an operator the wrong
|
|
156
|
+
* remedy, and folding it into `gaps` would let it warn when it must fail.
|
|
159
157
|
*/
|
|
160
|
-
readonly
|
|
158
|
+
readonly unpostable: number;
|
|
161
159
|
}
|
|
162
|
-
/** The moment a branch ruleset guards. */
|
|
163
|
-
export declare const MERGE_MOMENT = "pull-request";
|
|
164
|
-
/**
|
|
165
|
-
* Map every context a registry gate can produce to that gate.
|
|
166
|
-
*
|
|
167
|
-
* Built from the whole registry rather than from the declared gates, which is
|
|
168
|
-
* the difference between "protection requires a context whose gate nobody
|
|
169
|
-
* declared" and "protection requires a context Lisa knows nothing about". Those
|
|
170
|
-
* are different findings with different remedies, and a map built only from
|
|
171
|
-
* declarations could not tell them apart.
|
|
172
|
-
* @param options - Inputs
|
|
173
|
-
* @param options.registry - The shipped registry
|
|
174
|
-
* @param options.gates - The project's gates block
|
|
175
|
-
* @param options.workflowName - The workflow whose name prefixes a context
|
|
176
|
-
* @returns Context string to owning gate
|
|
177
|
-
*/
|
|
178
|
-
export declare function contextOwners(options: {
|
|
179
|
-
registry: MergeContextRegistry;
|
|
180
|
-
gates: Record<string, unknown>;
|
|
181
|
-
workflowName: string;
|
|
182
|
-
}): ReadonlyMap<string, ContextOwner>;
|
|
183
160
|
/**
|
|
184
161
|
* The contexts a `required` declaration promises protection will require.
|
|
185
162
|
* @param owners - Context to owning gate
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-declaration-drift.d.ts","sourceRoot":"","sources":["../../src/core/gate-declaration-drift.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gate-declaration-drift.d.ts","sourceRoot":"","sources":["../../src/core/gate-declaration-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAEtB,MAAM,0BAA0B,CAAC;AAElC,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,cAAc,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,YAAY;AACtB,kEAAkE;AAChE,SAAS;AACX,yEAAyE;GACvE,uBAAuB;AACzB,uDAAuD;GACrD,4BAA4B;AAC9B,yEAAyE;GACvE,uBAAuB;AACzB,0EAA0E;GACxE,qBAAqB;AACvB,wEAAwE;GACtE,yBAAyB;AAC3B;;;;;;;;;GASG;GACD,0BAA0B,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,kBAAkB,GAClB,qBAAqB,GACrB,2BAA2B,GAC3B,2BAA2B;AAC7B;;;;;;;;GAQG;GACD,oCAAoC,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,4DAA4D;AAC5D,MAAM,WAAW,qBAAqB;IACpC,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mBAAmB;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC9C,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACnD,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,kEAAkE;IAClE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAyDD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,GACxC,SAAS,MAAM,EAAE,CAcnB;AAsGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC1C,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CACtC,GAAG,sBAAsB,CAoEzB"}
|
|
@@ -26,15 +26,34 @@
|
|
|
26
26
|
* "agreed and proved" from "agreed and unproved" would itself be a control
|
|
27
27
|
* reporting more than it measured, so the word this module uses is
|
|
28
28
|
* `matched` — the two surfaces agree — and never `enforced` or `proved`.
|
|
29
|
-
* 3. **
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* 3. **A remedy may say "remove the context" only where Lisa renamed the job
|
|
30
|
+
* itself.** The original rule here was that no remedy could ever say it,
|
|
31
|
+
* and the reason was sound: third-party contexts are enforced by
|
|
32
|
+
* construction and declared by nobody, so a comparator free to propose a
|
|
33
|
+
* removal would eventually propose deleting one. That reason does not cover
|
|
34
|
+
* one case, and #3067 is what the gap costs. When the shipped registry
|
|
35
|
+
* carries a `previousLabels` entry, Lisa is not inferring that nothing owns
|
|
36
|
+
* a context — it is reading its OWN record that it renamed the job, which
|
|
37
|
+
* is proof that this exact string can never be posted again. A required
|
|
38
|
+
* context that never reports does not fail a pull request; GitHub waits for
|
|
39
|
+
* it forever, so leaving that unsaid red-walls the repository silently.
|
|
40
|
+
*
|
|
41
|
+
* The narrowing is structural, not conventional. Exactly one verdict,
|
|
42
|
+
* `enforced-context-retired`, maps to the single removal remedy, and that
|
|
43
|
+
* verdict is reachable only from a `previousLabels` match. A context with
|
|
44
|
+
* no owner at all is still `enforced-not-lisa-owned` with remedy `none`, so
|
|
45
|
+
* the original guarantee holds unchanged for every third-party check.
|
|
46
|
+
*
|
|
47
|
+
* The ownership map this comparison reads — which gate produces a context
|
|
48
|
+
* string, under which caller chain, and whether the registry records that
|
|
49
|
+
* name as renamed away — is built in `core/gate-context-owners`. That is a
|
|
50
|
+
* different question from the one here: it knows the registry and the chain
|
|
51
|
+
* rules and nothing about verdicts, and this module knows the verdicts and
|
|
52
|
+
* reads the map through `ReadonlyMap<string, ContextOwner>` without knowing
|
|
53
|
+
* how it was derived. The seam is the one the signatures already drew.
|
|
34
54
|
* @module core/gate-declaration-drift
|
|
35
55
|
*/
|
|
36
|
-
|
|
37
|
-
export const MERGE_MOMENT = "pull-request";
|
|
56
|
+
import { MERGE_MOMENT, } from "./gate-context-owners.js";
|
|
38
57
|
/** Every verdict, named once so no two mentions can drift apart. */
|
|
39
58
|
const MATCHED = "matched";
|
|
40
59
|
const DECLARED_NOT_ENFORCED = "declared-not-enforced";
|
|
@@ -42,6 +61,7 @@ const ENFORCED_DECLARED_OPTIONAL = "enforced-declared-optional";
|
|
|
42
61
|
const ENFORCED_DECLARED_OFF = "enforced-declared-off";
|
|
43
62
|
const ENFORCED_UNDECLARED = "enforced-undeclared";
|
|
44
63
|
const ENFORCED_NOT_LISA_OWNED = "enforced-not-lisa-owned";
|
|
64
|
+
const ENFORCED_CONTEXT_RETIRED = "enforced-context-retired";
|
|
45
65
|
/** Verdicts where the two surfaces state opposite things. */
|
|
46
66
|
const CONTRADICTIONS = new Set([
|
|
47
67
|
DECLARED_NOT_ENFORCED,
|
|
@@ -52,6 +72,10 @@ const GAPS = new Set([
|
|
|
52
72
|
ENFORCED_DECLARED_OPTIONAL,
|
|
53
73
|
ENFORCED_UNDECLARED,
|
|
54
74
|
]);
|
|
75
|
+
/** Verdicts where the surface requires a name nothing can post. */
|
|
76
|
+
const UNPOSTABLE = new Set([
|
|
77
|
+
ENFORCED_CONTEXT_RETIRED,
|
|
78
|
+
]);
|
|
55
79
|
/** The remedy each verdict calls for. */
|
|
56
80
|
const REMEDIES = {
|
|
57
81
|
[MATCHED]: "none",
|
|
@@ -60,6 +84,7 @@ const REMEDIES = {
|
|
|
60
84
|
[ENFORCED_DECLARED_OFF]: "resolve-the-contradiction",
|
|
61
85
|
[ENFORCED_UNDECLARED]: "declare-the-gate",
|
|
62
86
|
[ENFORCED_NOT_LISA_OWNED]: "none",
|
|
87
|
+
[ENFORCED_CONTEXT_RETIRED]: "stop-requiring-the-retired-context",
|
|
63
88
|
};
|
|
64
89
|
/** Every verdict, so a count of zero is still a stated zero. */
|
|
65
90
|
const VERDICTS = Object.keys(REMEDIES);
|
|
@@ -76,111 +101,6 @@ const SURFACE_NAMES = {
|
|
|
76
101
|
function sorted(values) {
|
|
77
102
|
return [...values].sort((left, right) => left.localeCompare(right));
|
|
78
103
|
}
|
|
79
|
-
/**
|
|
80
|
-
* The declaration state one resolved level names.
|
|
81
|
-
*
|
|
82
|
-
* Anything that is not one of the three levels is `not-declared`, because an
|
|
83
|
-
* unknown level is a typo rather than a claim.
|
|
84
|
-
* @param level - The resolved level
|
|
85
|
-
* @returns The declaration state
|
|
86
|
-
*/
|
|
87
|
-
function asDeclaration(level) {
|
|
88
|
-
return level === "required" || level === "optional" || level === "off"
|
|
89
|
-
? level
|
|
90
|
-
: "not-declared";
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Map every context a registry gate can produce to that gate.
|
|
94
|
-
*
|
|
95
|
-
* Built from the whole registry rather than from the declared gates, which is
|
|
96
|
-
* the difference between "protection requires a context whose gate nobody
|
|
97
|
-
* declared" and "protection requires a context Lisa knows nothing about". Those
|
|
98
|
-
* are different findings with different remedies, and a map built only from
|
|
99
|
-
* declarations could not tell them apart.
|
|
100
|
-
* @param options - Inputs
|
|
101
|
-
* @param options.registry - The shipped registry
|
|
102
|
-
* @param options.gates - The project's gates block
|
|
103
|
-
* @param options.workflowName - The workflow whose name prefixes a context
|
|
104
|
-
* @returns Context string to owning gate
|
|
105
|
-
*/
|
|
106
|
-
export function contextOwners(options) {
|
|
107
|
-
const { registry, gates, workflowName } = options;
|
|
108
|
-
const resolved = resolveMergeMoment(registry, gates);
|
|
109
|
-
const family = registry.momentFamily(MERGE_MOMENT);
|
|
110
|
-
const owners = Object.entries(registry.REGISTRY).map(([gateId, definition]) => {
|
|
111
|
-
const hit = resolved.get(gateId);
|
|
112
|
-
return [
|
|
113
|
-
`${prefixFor(registry, hit?.callerChain, workflowName)} / ${definition.label}`,
|
|
114
|
-
{
|
|
115
|
-
gateId,
|
|
116
|
-
declaration: asDeclaration(hit?.level),
|
|
117
|
-
legalAtMerge: definition.moments.includes(family),
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
});
|
|
121
|
-
const awaited = [...resolved.values()].flatMap(hit => hit.mode === "await" && hit.awaits !== null
|
|
122
|
-
? [
|
|
123
|
-
[
|
|
124
|
-
hit.awaits,
|
|
125
|
-
{
|
|
126
|
-
gateId: hit.id,
|
|
127
|
-
declaration: asDeclaration(hit.level),
|
|
128
|
-
legalAtMerge: registry.REGISTRY[hit.id]?.moments.includes(family) === true,
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
]
|
|
132
|
-
: []);
|
|
133
|
-
return new Map([...owners, ...awaited]);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The prefix one gate's context carries, honouring a per-declaration override.
|
|
137
|
-
*
|
|
138
|
-
* Three states, and the third is the one that matters. No override means the
|
|
139
|
-
* caller-wide name, unchanged — which is what keeps every existing declaration
|
|
140
|
-
* deriving exactly what it derived before this field existed. An override with
|
|
141
|
-
* a registry able to join it means that gate's own chain. An override with a
|
|
142
|
-
* registry too old to join it is refused outright, because the alternative is
|
|
143
|
-
* to derive the caller-wide name for a gate whose declaration has just said
|
|
144
|
-
* that name is wrong — a comparison that would report a clean match against a
|
|
145
|
-
* context nothing posts.
|
|
146
|
-
* @param registry - The shipped registry
|
|
147
|
-
* @param chain - The declared override, when there is one
|
|
148
|
-
* @param workflowName - The caller-wide chain
|
|
149
|
-
* @returns The prefix
|
|
150
|
-
* @throws {Error} When an override cannot be joined
|
|
151
|
-
*/
|
|
152
|
-
function prefixFor(registry, chain, workflowName) {
|
|
153
|
-
if (chain === null || chain === undefined)
|
|
154
|
-
return workflowName;
|
|
155
|
-
if (registry.callerPrefix === undefined) {
|
|
156
|
-
throw new Error(`a gate declares its own caller chain (${JSON.stringify(chain)}), but ` +
|
|
157
|
-
`the installed copy of Lisa's check registry is too old to derive a ` +
|
|
158
|
-
`context from one. Update Lisa, or remove the override; deriving the ` +
|
|
159
|
-
`default name here would report agreement about a check that never ` +
|
|
160
|
-
`reports.`);
|
|
161
|
-
}
|
|
162
|
-
return registry.callerPrefix(chain);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Resolve the merge moment, treating a refusal as "nothing resolved".
|
|
166
|
-
*
|
|
167
|
-
* A refusal is never swallowed by the caller: `readTemplateEnforcement` and the
|
|
168
|
-
* health check both report the declaration problems separately. What must not
|
|
169
|
-
* happen here is a throw that turns a comparison into no comparison at all.
|
|
170
|
-
* @param registry - The shipped registry
|
|
171
|
-
* @param gates - The project's gates block
|
|
172
|
-
* @returns Resolved gates keyed by id
|
|
173
|
-
*/
|
|
174
|
-
function resolveMergeMoment(registry, gates) {
|
|
175
|
-
try {
|
|
176
|
-
return new Map(registry
|
|
177
|
-
.resolveMoment({ gates, moment: MERGE_MOMENT, includeOff: true })
|
|
178
|
-
.map(entry => [entry.id, { ...entry }]));
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return new Map();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
104
|
/**
|
|
185
105
|
* The contexts a `required` declaration promises protection will require.
|
|
186
106
|
* @param owners - Context to owning gate
|
|
@@ -188,9 +108,27 @@ function resolveMergeMoment(registry, gates) {
|
|
|
188
108
|
*/
|
|
189
109
|
export function declaredRequiredContexts(owners) {
|
|
190
110
|
return sorted([...owners.entries()]
|
|
191
|
-
.
|
|
111
|
+
// A retired label is never a promise. Its gate's declaration promises
|
|
112
|
+
// the CURRENT context, so listing the old name here would report a
|
|
113
|
+
// `declared-not-enforced` gap against a string no declaration asks for
|
|
114
|
+
// and tell the operator to start requiring a context nothing posts —
|
|
115
|
+
// the permanent-pending trap, created by the check meant to find it.
|
|
116
|
+
.filter(([, owner]) => owner.declaration === "required" && retirementOf(owner) === null)
|
|
192
117
|
.map(([context]) => context));
|
|
193
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The rename one owner is the losing half of, normalized to null.
|
|
121
|
+
* @param owner - The gate producing a context, when one does
|
|
122
|
+
* @returns The rename, or null
|
|
123
|
+
*/
|
|
124
|
+
function retirementOf(owner) {
|
|
125
|
+
// `?? null` rather than `!== null`, because an owner built before this field
|
|
126
|
+
// existed carries `undefined` — and `undefined !== null` is true, which would
|
|
127
|
+
// classify EVERY context as retired and tell an operator to delete their
|
|
128
|
+
// whole required list. Callers include fixtures and any consumer holding an
|
|
129
|
+
// older shape, so the normalization lives here rather than at each read.
|
|
130
|
+
return owner?.retired ?? null;
|
|
131
|
+
}
|
|
194
132
|
/**
|
|
195
133
|
* The verdict for one enforced context.
|
|
196
134
|
* @param owner - The gate producing it, when one does
|
|
@@ -199,6 +137,12 @@ export function declaredRequiredContexts(owners) {
|
|
|
199
137
|
function verdictForEnforced(owner) {
|
|
200
138
|
if (owner === undefined)
|
|
201
139
|
return ENFORCED_NOT_LISA_OWNED;
|
|
140
|
+
// Ahead of every declaration branch, because the declaration cannot rescue
|
|
141
|
+
// it. The gate may be declared `required` and run on every pull request —
|
|
142
|
+
// it posts its CURRENT label, so this string still never reports. Reading
|
|
143
|
+
// the declaration first would classify the exact #3067 case as `matched`.
|
|
144
|
+
if (retirementOf(owner) !== null)
|
|
145
|
+
return ENFORCED_CONTEXT_RETIRED;
|
|
202
146
|
if (owner.declaration === "required")
|
|
203
147
|
return MATCHED;
|
|
204
148
|
if (owner.declaration === "optional")
|
|
@@ -214,13 +158,21 @@ function verdictForEnforced(owner) {
|
|
|
214
158
|
* @param options.surface - The enforcing surface
|
|
215
159
|
* @param options.gateId - The owning gate, when there is one
|
|
216
160
|
* @param options.sources - Where the requirement was read from
|
|
161
|
+
* @param options.rulesets - The rulesets requiring it, sorted
|
|
162
|
+
* @param options.retired - The rename it is the losing half of, or null
|
|
217
163
|
* @returns One operator-readable sentence
|
|
218
164
|
*/
|
|
219
165
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- one branch per verdict keeps the wording auditable
|
|
220
166
|
function detailFor(options) {
|
|
221
|
-
const { verdict, gateId, sources } = options;
|
|
167
|
+
const { verdict, gateId, sources, rulesets, retired } = options;
|
|
222
168
|
const surface = SURFACE_NAMES[options.surface];
|
|
223
169
|
const where = sources.length === 0 ? "" : ` (${sources.join(", ")})`;
|
|
170
|
+
if (verdict === ENFORCED_CONTEXT_RETIRED && retired !== null) {
|
|
171
|
+
const named = rulesets.length === 0
|
|
172
|
+
? " The ruleset holding it was not named by the reader."
|
|
173
|
+
: ` The requirement lives in ${rulesets.length === 1 ? "ruleset" : "rulesets"} ${rulesets.map(name => `"${name}"`).join(", ")}, which Lisa may not manage — reported here, never edited automatically.`;
|
|
174
|
+
return `NOTHING WILL EVER POST THIS. Lisa's registry records that the gate "${String(gateId)}" was renamed from "${retired.label}", and ${surface} still requires the old name${where}. This is not a failing check — GitHub holds a required context that never reports at "Expected — Waiting for status to be reported" indefinitely, so every pull request in this repository is blocked with no red tick and no log to open. The gate posts "${retired.replacement}" now.${named} Remove the old context first, then require the new one: requiring the new one before the job posts it creates the same permanent wait in the other direction.`;
|
|
175
|
+
}
|
|
224
176
|
if (verdict === MATCHED) {
|
|
225
177
|
return `Declared required, and ${surface} requires it${where}. The two surfaces agree; that is not evidence the check proves its property.`;
|
|
226
178
|
}
|
|
@@ -277,19 +229,22 @@ export function classifyDeclarationDrift(options) {
|
|
|
277
229
|
const verdict = verdictForEnforced(owner);
|
|
278
230
|
const found = grouped.get(context);
|
|
279
231
|
const sources = sorted(new Set(found?.sources ?? []));
|
|
232
|
+
const rulesets = sorted(new Set(found?.rulesets ?? []));
|
|
280
233
|
return {
|
|
281
234
|
context,
|
|
282
235
|
verdict,
|
|
283
236
|
remedy: REMEDIES[verdict],
|
|
284
237
|
gateId: owner?.gateId ?? null,
|
|
285
238
|
declaration: owner?.declaration ?? null,
|
|
286
|
-
rulesets
|
|
239
|
+
rulesets,
|
|
287
240
|
sources,
|
|
288
241
|
detail: detailFor({
|
|
289
242
|
verdict,
|
|
290
243
|
surface,
|
|
291
244
|
gateId: owner?.gateId ?? null,
|
|
292
245
|
sources,
|
|
246
|
+
rulesets,
|
|
247
|
+
retired: retirementOf(owner),
|
|
293
248
|
}),
|
|
294
249
|
};
|
|
295
250
|
});
|
|
@@ -310,6 +265,8 @@ export function classifyDeclarationDrift(options) {
|
|
|
310
265
|
surface,
|
|
311
266
|
gateId,
|
|
312
267
|
sources: [],
|
|
268
|
+
rulesets: [],
|
|
269
|
+
retired: null,
|
|
313
270
|
}),
|
|
314
271
|
};
|
|
315
272
|
});
|
|
@@ -324,6 +281,7 @@ export function classifyDeclarationDrift(options) {
|
|
|
324
281
|
contradictions: entries.filter(entry => CONTRADICTIONS.has(entry.verdict))
|
|
325
282
|
.length,
|
|
326
283
|
gaps: entries.filter(entry => GAPS.has(entry.verdict)).length,
|
|
284
|
+
unpostable: entries.filter(entry => UNPOSTABLE.has(entry.verdict)).length,
|
|
327
285
|
};
|
|
328
286
|
}
|
|
329
287
|
//# sourceMappingURL=gate-declaration-drift.js.map
|