@codyswann/lisa 4.10.7 → 4.10.8

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.
Files changed (87) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +82 -1
  2. package/dist/cli/doctor-declared-contexts.d.ts.map +1 -1
  3. package/dist/cli/doctor-declared-contexts.js +16 -0
  4. package/dist/cli/doctor-declared-contexts.js.map +1 -1
  5. package/dist/cli/gate-report-drift-section.d.ts.map +1 -1
  6. package/dist/cli/gate-report-drift-section.js +6 -0
  7. package/dist/cli/gate-report-drift-section.js.map +1 -1
  8. package/dist/cli/gate-report-joins.d.ts +0 -15
  9. package/dist/cli/gate-report-joins.d.ts.map +1 -1
  10. package/dist/cli/gate-report-joins.js +2 -1
  11. package/dist/cli/gate-report-joins.js.map +1 -1
  12. package/dist/cli/gate-report-registry.d.ts +8 -0
  13. package/dist/cli/gate-report-registry.d.ts.map +1 -1
  14. package/dist/cli/gate-report-registry.js.map +1 -1
  15. package/dist/cli/gate-report-types.d.ts +2 -1
  16. package/dist/cli/gate-report-types.d.ts.map +1 -1
  17. package/dist/cli/gate-report-types.js.map +1 -1
  18. package/dist/core/gate-context-owners.d.ts +122 -0
  19. package/dist/core/gate-context-owners.d.ts.map +1 -0
  20. package/dist/core/gate-context-owners.js +174 -0
  21. package/dist/core/gate-context-owners.js.map +1 -0
  22. package/dist/core/gate-declaration-drift.d.ts +56 -79
  23. package/dist/core/gate-declaration-drift.d.ts.map +1 -1
  24. package/dist/core/gate-declaration-drift.js +73 -115
  25. package/dist/core/gate-declaration-drift.js.map +1 -1
  26. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.js +4 -0
  28. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  29. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  30. package/dist/core/upstream-evidence-manifest.js +6 -2
  31. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  32. package/dist/health/declared-checks-inspection.d.ts +0 -22
  33. package/dist/health/declared-checks-inspection.d.ts.map +1 -1
  34. package/dist/health/declared-checks-inspection.js +44 -5
  35. package/dist/health/declared-checks-inspection.js.map +1 -1
  36. package/package.json +1 -1
  37. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-agy/plugin.json +1 -1
  40. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-agy/plugin.json +1 -1
  50. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  70. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-agy/plugin.json +1 -1
  75. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  80. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  85. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  87. package/scripts/lisa-github-rulesets.sh +138 -0
@@ -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. **No remedy in this module can say "remove the context".** Third-party
30
- * contexts are enforced by construction and declared by nobody; a
31
- * comparator whose vocabulary contained a removal would eventually propose
32
- * deleting one. The `DriftRemedy` union simply has no such member, so the
33
- * guarantee holds for every caller rather than for the careful ones.
34
- * @module core/gate-declaration-drift
35
- */
36
- /**
37
- * What the settings file says about one gate at one moment.
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
- * `off` and `not-declared` are deliberately separate. Collapsing them is what
40
- * let a declaration govern nothing: the CI façade read both as
41
- * `configured=false` and ran its built-in fallback, so `off` could not turn a
42
- * job off.
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
- export type DeclarationState = "required" | "optional" | "off" | "not-declared";
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
- * Join one declaration's own chain into the prefix its context carries.
150
+ * Entries the enforcing surface requires and nothing can post.
153
151
  *
154
- * Optional for the same reason, and its absence is NOT a licence to join the
155
- * chain here: a second implementation of the joining rule is how a rename
156
- * lands in one derivation and not the other. Where it is absent, an override
157
- * cannot be honoured, and the comparison says so rather than deriving the
158
- * caller-wide name as though nothing had been declared.
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 callerPrefix?: (chain: readonly string[] | string) => string;
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":"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"}
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. **No remedy in this module can say "remove the context".** Third-party
30
- * contexts are enforced by construction and declared by nobody; a
31
- * comparator whose vocabulary contained a removal would eventually propose
32
- * deleting one. The `DriftRemedy` union simply has no such member, so the
33
- * guarantee holds for every caller rather than for the careful ones.
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
- /** The moment a branch ruleset guards. */
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
- .filter(([, owner]) => owner.declaration === "required")
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: sorted(new Set(found?.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