@intentius/chant 0.54.3 → 0.56.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.
@@ -46,6 +46,17 @@ interface ValidatorResult {
46
46
  * Exported for testability.
47
47
  */
48
48
  export declare function tryValidateExternal(yamlText: string): ValidatorResult;
49
+ /**
50
+ * Format the migration report as Markdown, mirroring the output format
51
+ * prescribed by the upstream gitlab-org/ci-cd/github-actions-to-gitlab-ci
52
+ * skill: overview, classification, diagnostic table, aggregated manual
53
+ * setup steps, suggested GitLab improvements, caveats.
54
+ *
55
+ * The same data backs the SARIF report (via formatSarif); this is the
56
+ * human-readable surface. Exported so downstream migrate consumers can render
57
+ * the same report without reimplementing it (#2066).
58
+ */
59
+ export declare function formatMarkdownSummary(provenance: Array<Record<string, unknown>>, diagnostics: Array<Record<string, unknown>>, sourceContent?: string): string;
49
60
  export declare function printMigrateResult(result: MigrateCliResult): void;
50
61
  export {};
51
62
  //# sourceMappingURL=migrate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C,4DAA4D;IAC5D,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsJpF;AAED,UAAU,eAAe;IACvB,GAAG,EAAE,OAAO,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAUrE;AA6OD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAajE"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C,4DAA4D;IAC5D,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsJpF;AAED,UAAU,eAAe;IACvB,GAAG,EAAE,OAAO,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAUrE;AAqBD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1C,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAkER;AAyID,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAajE"}
@@ -57,6 +57,28 @@ export interface ChangeSet {
57
57
  org: string;
58
58
  /** All proposed changes, in stable order. */
59
59
  entries: ChangeSetEntry[];
60
+ /**
61
+ * Live entries under management across the diffed collections (#2067).
62
+ * `diffCollection` returns each collection's live count so the diff layer
63
+ * can sum it here. Optional — when absent, guardrails that want a live
64
+ * denominator (see {@link removalDeltaCap}) fall back to the plan-relative
65
+ * one.
66
+ *
67
+ * Superseded by {@link ChangeSet.managedCounts}: a single pooled total lets
68
+ * non-deletable and cross-collection live entries dilute the delete
69
+ * fraction (wiping 3 of 4 teams reads as 12.5% when 20 team members pad
70
+ * the pool). Kept for compatibility — a change set carrying only this
71
+ * total keeps the 0.55.0 pooled behavior.
72
+ */
73
+ managedCount?: number;
74
+ /**
75
+ * Live managed-entry counts per resource type, keyed by
76
+ * `ChangeSetEntry.resourceType`. When present, {@link removalDeltaCap}
77
+ * evaluates the delete fraction per resource type instead of against the
78
+ * pooled `managedCount`, so live entries of one type cannot dilute a wipe
79
+ * of another. Supersedes `managedCount`.
80
+ */
81
+ managedCounts?: Readonly<Record<string, number>>;
60
82
  }
61
83
  /** Options controlling diff behaviour. */
62
84
  export interface DiffOptions {
@@ -105,8 +127,11 @@ export interface DiffCollectionParams<D, L> {
105
127
  * when `compareFields` reports differences, and ownership-gated deletes for
106
128
  * live-not-desired. This is the selective-by-omission + ownership-gated-delete
107
129
  * pattern shared by every keyed-collection diff.
130
+ *
131
+ * Returns the live entry count for this collection, so the diff layer can sum
132
+ * `ChangeSet.managedCount` across the collections it diffs (#2067).
108
133
  */
109
- export declare function diffCollection<D, L>(params: DiffCollectionParams<D, L>): void;
134
+ export declare function diffCollection<D, L>(params: DiffCollectionParams<D, L>): number;
110
135
  /** Count entries per change kind. */
111
136
  export declare function summarizeChangeSet(cs: ChangeSet): Record<ChangeKind, number>;
112
137
  /** Human-readable plan summary for dry-run output. Pure. */
@@ -125,18 +150,57 @@ export type GuardrailResult = {
125
150
  ok: false;
126
151
  diagnostics: GuardrailDiagnostic[];
127
152
  };
128
- /** A guardrail check over a (rename-resolved) change set. Returns null when it passes. */
129
- export type GuardrailCheck = (resolved: ChangeSet) => GuardrailDiagnostic | null;
153
+ /** Live context threaded to guardrail checks by `runGuardrailChecks` (#2067). */
154
+ export interface GuardrailContext {
155
+ /** Live entries under management this cycle (see `ChangeSet.managedCount`). */
156
+ managedCount?: number;
157
+ }
158
+ /**
159
+ * A guardrail check over a (rename-resolved) change set. Returns null when it
160
+ * passes. `ctx` carries live context when the caller has it (#2067); a check
161
+ * written against the single-argument shape keeps working unchanged.
162
+ */
163
+ export type GuardrailCheck = (resolved: ChangeSet, ctx?: GuardrailContext) => GuardrailDiagnostic | null;
130
164
  /** Config for `removalDeltaCap`. */
131
165
  export interface RemovalDeltaCapOptions {
132
- /** Max fraction of pre-existing entries that may be deleted. Must be in (0,1]. Default 0.25. */
166
+ /**
167
+ * Max fraction of pre-existing entries that may be deleted. Must be in
168
+ * (0,1]: `removalDeltaCap` throws an `Error` on any value outside that
169
+ * range (0, negatives, above 1, NaN) rather than silently disabling the
170
+ * cap. Default 0.25.
171
+ */
133
172
  maxFraction?: number;
173
+ /**
174
+ * Pooled live managed-entry denominator (#2067). When positive, deletes are
175
+ * divided by this count of live entries under management instead of the
176
+ * plan's non-create entries — a converged cycle deleting one stale entry
177
+ * out of many no longer reads as 1/1 = 100%. Defaults to
178
+ * `changeSet.managedCount`; when neither is present (or not positive) the
179
+ * cap keeps its plan-relative behavior.
180
+ *
181
+ * Superseded by {@link RemovalDeltaCapOptions.managedTotals} — the pooled
182
+ * total lets unrelated live entries dilute the delete fraction.
183
+ */
184
+ managedTotal?: number;
185
+ /**
186
+ * Live managed-entry counts per resource type (keys =
187
+ * `ChangeSetEntry.resourceType`), mirroring {@link ChangeSet.managedCounts}
188
+ * and winning over it when both are present. When per-type counts are
189
+ * available from either source, the cap is evaluated per resource type:
190
+ * each type's deletes divide by that type's live count when present and
191
+ * positive, else by that type's own plan non-creates. Supersedes
192
+ * `managedTotal`.
193
+ */
194
+ managedTotals?: Readonly<Record<string, number>>;
134
195
  }
135
196
  /**
136
197
  * Resolve rename aliases. A create entry carrying a `previously` key matching a
137
- * delete entry's key is collapsed into an update, removing the delete. Returns a
138
- * new ChangeSet with renames resolved. Provider-agnostic — works on any entry
139
- * whose `after.previously` is a string.
198
+ * delete entry's key OF THE SAME RESOURCE TYPE is collapsed into an update,
199
+ * removing the delete. Matching across resource types would let e.g. a team
200
+ * create with `previously: "old"` swallow an unrelated member delete keyed
201
+ * "old", hiding a real delete from guardrail counting. Returns a new ChangeSet
202
+ * with renames resolved. Provider-agnostic — works on any entry whose
203
+ * `after.previously` is a string.
140
204
  */
141
205
  export declare function resolveRenames(changeSet: ChangeSet): ChangeSet;
142
206
  /**
@@ -144,6 +208,24 @@ export declare function resolveRenames(changeSet: ChangeSet): ChangeSet;
144
208
  * (deletes + updates; creates excluded so a flood of new entries can't dilute
145
209
  * the delete fraction). Guards against a typo wiping the config in one apply.
146
210
  *
211
+ * With per-type live counts (`opts.managedTotals`, else
212
+ * `changeSet.managedCounts`) the cap is evaluated PER RESOURCE TYPE: for each
213
+ * type with deletes, that type's deletes divide by that type's live count when
214
+ * present and positive, else by that type's own plan non-creates. Any type
215
+ * over `maxFraction` blocks, and the diagnostic names the worst offender. The
216
+ * pooled denominator this replaces was unsafe — live entries of one type
217
+ * (team members, repos) diluted a wipe of another (teams, org secrets) below
218
+ * the cap.
219
+ *
220
+ * With only a pooled live denominator (`opts.managedTotal`, else
221
+ * `changeSet.managedCount`; #2067) deletes are measured against the pooled
222
+ * live entries under management — so a converged cycle's plan of exactly one
223
+ * stale delete does not trip at 1/1 = 100%. Without either, behavior is
224
+ * plan-relative as before.
225
+ *
226
+ * Throws on a `maxFraction` outside (0,1] (NaN included) — fail closed, never
227
+ * silently disable the cap.
228
+ *
147
229
  * CONTRACT: pass a RENAME-RESOLVED change set (see {@link resolveRenames}).
148
230
  */
149
231
  export declare function removalDeltaCap(changeSet: ChangeSet, opts?: RemovalDeltaCapOptions): GuardrailDiagnostic | null;
@@ -151,8 +233,10 @@ export declare function removalDeltaCap(changeSet: ChangeSet, opts?: RemovalDelt
151
233
  * Run a set of guardrail checks against a change set. Resolves renames ONCE,
152
234
  * then runs every check on the resolved set, aggregating any diagnostics. The
153
235
  * caller composes provider-specific checks (e.g. an admin floor) as closures.
236
+ * `ctx` (optional, #2067) threads live context — e.g. the managed-live count —
237
+ * to every check; single-argument checks ignore it.
154
238
  */
155
- export declare function runGuardrailChecks(changeSet: ChangeSet, checks: GuardrailCheck[]): GuardrailResult;
239
+ export declare function runGuardrailChecks(changeSet: ChangeSet, checks: GuardrailCheck[], ctx?: GuardrailContext): GuardrailResult;
156
240
  /** Controls how a cycle tracks its API usage against a shared request budget. */
157
241
  export interface RateBudget {
158
242
  /** Remaining request capacity for this run. */
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.54.3",
3
+ "version": "0.56.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -251,9 +251,10 @@ async function loadMigrationRules(targetLexicon: string): Promise<LintRule[]> {
251
251
  * setup steps, suggested GitLab improvements, caveats.
252
252
  *
253
253
  * The same data backs the SARIF report (via formatSarif); this is the
254
- * human-readable surface.
254
+ * human-readable surface. Exported so downstream migrate consumers can render
255
+ * the same report without reimplementing it (#2066).
255
256
  */
256
- function formatMarkdownSummary(
257
+ export function formatMarkdownSummary(
257
258
  provenance: Array<Record<string, unknown>>,
258
259
  diagnostics: Array<Record<string, unknown>>,
259
260
  sourceContent?: string,
@@ -102,6 +102,23 @@ describe("diffCollection", () => {
102
102
  ]);
103
103
  });
104
104
 
105
+ test("returns the live entry count for managedCount accumulation (#2067)", () => {
106
+ const out: ChangeSetEntry[] = [];
107
+ const count = diffCollection<D, L>({
108
+ resourceType: "thing",
109
+ desired: new Map([["a", { name: "a", v: 1 }]]),
110
+ live: new Map([
111
+ ["a", { name: "a", v: 1 }],
112
+ ["b", { name: "b", v: 2 }],
113
+ ["c", { name: "c", v: 3 }],
114
+ ]),
115
+ compareFields: () => [],
116
+ opts: noOpts,
117
+ out,
118
+ });
119
+ expect(count).toBe(3);
120
+ });
121
+
105
122
  test("honours createAfter / updateAfter mappers", () => {
106
123
  const out: ChangeSetEntry[] = [];
107
124
  diffCollection<D, L>({
@@ -171,6 +188,53 @@ describe("resolveRenames", () => {
171
188
  const cs: ChangeSet = { org: "acme", entries: [{ kind: "delete", resourceType: "team", key: "old" }] };
172
189
  expect(resolveRenames(cs)).toBe(cs);
173
190
  });
191
+
192
+ test("preserves managedCount and managedCounts on the resolved set", () => {
193
+ const cs: ChangeSet = {
194
+ org: "acme",
195
+ managedCount: 7,
196
+ managedCounts: { team: 4, member: 3 },
197
+ entries: [
198
+ { kind: "delete", resourceType: "team", key: "old", before: { slug: "old" } },
199
+ { kind: "create", resourceType: "team", key: "new", after: { previously: "old" } },
200
+ ],
201
+ };
202
+ const resolved = resolveRenames(cs);
203
+ expect(resolved.managedCount).toBe(7);
204
+ expect(resolved.managedCounts).toEqual({ team: 4, member: 3 });
205
+ });
206
+
207
+ test("only matches previously within the same resourceType", () => {
208
+ // A team create claiming previously "old" must NOT swallow a member
209
+ // delete that happens to share the key — that would hide a real delete
210
+ // from guardrail counting.
211
+ const cs: ChangeSet = {
212
+ org: "acme",
213
+ entries: [
214
+ { kind: "delete", resourceType: "member", key: "old", before: { login: "old" } },
215
+ { kind: "create", resourceType: "team", key: "new", after: { previously: "old" } },
216
+ ],
217
+ };
218
+ const resolved = resolveRenames(cs);
219
+ expect(resolved).toBe(cs); // untouched: no same-type match exists
220
+ expect(resolved.entries.filter((e) => e.kind === "delete")).toHaveLength(1);
221
+
222
+ // With a same-type delete alongside the cross-type collision, exactly
223
+ // the team delete collapses; the member delete survives.
224
+ const both: ChangeSet = {
225
+ org: "acme",
226
+ entries: [
227
+ { kind: "delete", resourceType: "member", key: "old", before: { login: "old" } },
228
+ { kind: "delete", resourceType: "team", key: "old", before: { slug: "old" } },
229
+ { kind: "create", resourceType: "team", key: "new", after: { previously: "old" } },
230
+ ],
231
+ };
232
+ const bothResolved = resolveRenames(both);
233
+ expect(bothResolved.entries.filter((e) => e.kind === "delete")).toEqual([
234
+ { kind: "delete", resourceType: "member", key: "old", before: { login: "old" } },
235
+ ]);
236
+ expect(bothResolved.entries.find((e) => e.kind === "update")!.before).toEqual({ slug: "old" });
237
+ });
174
238
  });
175
239
 
176
240
  describe("removalDeltaCap", () => {
@@ -199,6 +263,199 @@ describe("removalDeltaCap", () => {
199
263
  };
200
264
  expect(removalDeltaCap(cs)).toBeNull(); // 1/4 = 25%, not > 25%
201
265
  });
266
+
267
+ // Live-denominator mode (#2067) — the warden reference cases.
268
+
269
+ test("one stale delete of 10 live managed entries passes at the default cap", () => {
270
+ const cs: ChangeSet = {
271
+ org: "acme",
272
+ entries: [{ kind: "delete", resourceType: "x", key: "stale" }],
273
+ };
274
+ // Plan-relative this is 1/1 = 100% and trips; against the live count it
275
+ // is 1/10 = 10% and passes — the routine converged-cycle cleanup.
276
+ expect(removalDeltaCap(cs)).not.toBeNull();
277
+ expect(removalDeltaCap(cs, { managedTotal: 10 })).toBeNull();
278
+ });
279
+
280
+ test("4 deletes of 10 live managed entries blocks, with the live-mode message", () => {
281
+ const cs: ChangeSet = {
282
+ org: "acme",
283
+ entries: Array.from({ length: 4 }, (_, i) => ({
284
+ kind: "delete" as const,
285
+ resourceType: "x",
286
+ key: `k${i}`,
287
+ })),
288
+ };
289
+ const d = removalDeltaCap(cs, { managedTotal: 10 })!;
290
+ expect(d.guardrail).toBe("removalDeltaCap");
291
+ expect(d.message).toContain("4 of 10 live managed entries (40%)");
292
+ });
293
+
294
+ test("reads changeSet.managedCount when opts.managedTotal is absent", () => {
295
+ const cs: ChangeSet = {
296
+ org: "acme",
297
+ managedCount: 10,
298
+ entries: [{ kind: "delete", resourceType: "x", key: "stale" }],
299
+ };
300
+ expect(removalDeltaCap(cs)).toBeNull(); // 1/10 via the change set's own count
301
+ // An explicit managedTotal wins over the change set's count.
302
+ expect(removalDeltaCap({ ...cs, managedCount: 2 }, { managedTotal: 10 })).toBeNull();
303
+ });
304
+
305
+ test("without managed info the diagnostic deep-equals the plan-relative one", () => {
306
+ const cs: ChangeSet = {
307
+ org: "acme",
308
+ entries: [
309
+ { kind: "delete", resourceType: "x", key: "a" },
310
+ { kind: "delete", resourceType: "x", key: "b" },
311
+ ],
312
+ };
313
+ const expected = {
314
+ guardrail: "removalDeltaCap",
315
+ message:
316
+ "2 of 2 managed entries (100%) would be deleted, exceeding the 25% threshold. " +
317
+ "Check for typos in config or raise maxFraction to proceed.",
318
+ };
319
+ expect(removalDeltaCap(cs)).toEqual(expected);
320
+ // A zero live count means "no live info" — it never divides by zero and
321
+ // never loosens the cap; behavior stays exactly plan-relative.
322
+ expect(removalDeltaCap(cs, { managedTotal: 0 })).toEqual(expected);
323
+ });
324
+
325
+ test("a changeSet with only the legacy managedCount deep-equals the 0.55.0 behavior", () => {
326
+ const entries = Array.from({ length: 4 }, (_, i) => ({
327
+ kind: "delete" as const,
328
+ resourceType: "x",
329
+ key: `k${i}`,
330
+ }));
331
+ expect(removalDeltaCap({ org: "acme", managedCount: 10, entries })).toEqual({
332
+ guardrail: "removalDeltaCap",
333
+ message:
334
+ "4 of 10 live managed entries (40%) would be deleted, exceeding the 25% threshold. " +
335
+ "Check for typos in config or raise maxFraction to proceed.",
336
+ });
337
+ expect(removalDeltaCap({ org: "acme", managedCount: 20, entries })).toBeNull(); // 4/20
338
+ });
339
+
340
+ // Per-resource-type mode — the three-repo review cases. The pooled
341
+ // denominator let live entries of one type dilute a wipe of another.
342
+
343
+ test("3 of 4 team deletes blocks at 75% even with 20 live members in another type", () => {
344
+ const cs: ChangeSet = {
345
+ org: "acme",
346
+ managedCounts: { team: 4, member: 20 },
347
+ entries: Array.from({ length: 3 }, (_, i) => ({
348
+ kind: "delete" as const,
349
+ resourceType: "team",
350
+ key: `t${i}`,
351
+ })),
352
+ };
353
+ // Pooled (0.55.0) this read 3/24 = 12.5% and sailed under the default cap.
354
+ expect(removalDeltaCap({ ...cs, managedCounts: undefined, managedCount: 24 })).toBeNull();
355
+ const d = removalDeltaCap(cs)!;
356
+ expect(d.guardrail).toBe("removalDeltaCap");
357
+ expect(d.message).toContain("3 of 4 live team entries (75%) would be deleted");
358
+ });
359
+
360
+ test("a full org-secret wipe blocks at 100% regardless of live repo count", () => {
361
+ const cs: ChangeSet = {
362
+ org: "acme",
363
+ managedCounts: { "org-secret": 2, repo: 75 },
364
+ entries: [
365
+ { kind: "delete", resourceType: "org-secret", key: "s1" },
366
+ { kind: "delete", resourceType: "org-secret", key: "s2" },
367
+ ],
368
+ };
369
+ // Pooled this read 2/77 = 2.6% and passed.
370
+ expect(removalDeltaCap({ ...cs, managedCounts: undefined, managedCount: 77 })).toBeNull();
371
+ const d = removalDeltaCap(cs)!;
372
+ expect(d.message).toContain("2 of 2 live org-secret entries (100%) would be deleted");
373
+ });
374
+
375
+ test("a type with no live count falls back to its own plan non-creates", () => {
376
+ const cs: ChangeSet = {
377
+ org: "acme",
378
+ managedCounts: { team: 10 },
379
+ entries: [
380
+ { kind: "delete", resourceType: "team", key: "t0" }, // 1/10 live: fine
381
+ { kind: "delete", resourceType: "webhook", key: "w0" },
382
+ { kind: "delete", resourceType: "webhook", key: "w1" },
383
+ { kind: "update", resourceType: "webhook", key: "w2" },
384
+ ],
385
+ };
386
+ // webhook has no live count → plan-relative within the type: 2/3 = 67%.
387
+ const d = removalDeltaCap(cs)!;
388
+ expect(d.message).toContain("2 of 3 planned webhook entries (67%) would be deleted");
389
+ // team creates never pad the webhook denominator, and vice versa.
390
+ expect(d.message).not.toContain("team");
391
+ });
392
+
393
+ test("passes when every type stays under the cap, and zero live counts fall back", () => {
394
+ const cs: ChangeSet = {
395
+ org: "acme",
396
+ managedCounts: { team: 10, member: 20 },
397
+ entries: [
398
+ { kind: "delete", resourceType: "team", key: "t0" },
399
+ { kind: "delete", resourceType: "member", key: "m0" },
400
+ { kind: "delete", resourceType: "member", key: "m1" },
401
+ ],
402
+ };
403
+ expect(removalDeltaCap(cs)).toBeNull(); // 1/10 and 2/20
404
+
405
+ // A zero live count for a type means "no live info for it" — that type
406
+ // is measured plan-relative (1/1 here), never divided by zero.
407
+ const zero: ChangeSet = {
408
+ org: "acme",
409
+ managedCounts: { team: 0 },
410
+ entries: [{ kind: "delete", resourceType: "team", key: "t0" }],
411
+ };
412
+ expect(removalDeltaCap(zero)!.message).toContain("1 of 1 planned team entries (100%)");
413
+ });
414
+
415
+ test("names the worst offender first and lists other tripping types", () => {
416
+ const cs: ChangeSet = {
417
+ org: "acme",
418
+ managedCounts: { "org-secret": 2, team: 4 },
419
+ entries: [
420
+ { kind: "delete", resourceType: "team", key: "t0" },
421
+ { kind: "delete", resourceType: "team", key: "t1" },
422
+ { kind: "delete", resourceType: "team", key: "t2" },
423
+ { kind: "delete", resourceType: "org-secret", key: "s0" },
424
+ { kind: "delete", resourceType: "org-secret", key: "s1" },
425
+ ],
426
+ };
427
+ const d = removalDeltaCap(cs)!;
428
+ // 100% secrets beats 75% teams for the headline; teams are still listed.
429
+ expect(d.message).toMatch(/^2 of 2 live org-secret entries \(100%\) would be deleted/);
430
+ expect(d.message).toContain("Also over the cap: 3 of 4 live team entries (75%)");
431
+ });
432
+
433
+ test("opts.managedTotals wins over changeSet counts; per-type wins over pooled", () => {
434
+ const cs: ChangeSet = {
435
+ org: "acme",
436
+ managedCounts: { team: 2 },
437
+ managedCount: 100,
438
+ entries: [{ kind: "delete", resourceType: "team", key: "t0" }],
439
+ };
440
+ // Per-type (2) beats the diluting pooled 100: 1/2 = 50% trips.
441
+ expect(removalDeltaCap(cs)).not.toBeNull();
442
+ // Explicit opts.managedTotals overrides the change set's counts.
443
+ expect(removalDeltaCap(cs, { managedTotals: { team: 10 } })).toBeNull();
444
+ });
445
+
446
+ test("maxFraction outside (0,1] throws instead of silently disabling the cap", () => {
447
+ const cs: ChangeSet = {
448
+ org: "acme",
449
+ entries: [{ kind: "delete", resourceType: "x", key: "a" }],
450
+ };
451
+ for (const bad of [Number.NaN, 0, 1.5, -1]) {
452
+ expect(() => removalDeltaCap(cs, { maxFraction: bad })).toThrow(/maxFraction must be in \(0, 1\]/);
453
+ }
454
+ // The bounds themselves: 1 is allowed (nothing short of a full wipe trips)…
455
+ expect(removalDeltaCap(cs, { maxFraction: 1, managedTotal: 2 })).toBeNull();
456
+ // …and the default 0.25 stays valid explicitly.
457
+ expect(removalDeltaCap(cs, { maxFraction: 0.25, managedTotal: 10 })).toBeNull();
458
+ });
202
459
  });
203
460
 
204
461
  describe("runGuardrailChecks", () => {
@@ -221,6 +478,19 @@ describe("runGuardrailChecks", () => {
221
478
  const cs: ChangeSet = { org: "acme", entries: [{ kind: "create", resourceType: "x", key: "a" }] };
222
479
  expect(runGuardrailChecks(cs, [() => null])).toEqual({ ok: true });
223
480
  });
481
+
482
+ test("threads live context to checks that take it (#2067)", () => {
483
+ const cs: ChangeSet = {
484
+ org: "acme",
485
+ entries: [{ kind: "delete", resourceType: "x", key: "stale" }],
486
+ };
487
+ const check: GuardrailCheck = (resolved, ctx) =>
488
+ removalDeltaCap(resolved, { managedTotal: ctx?.managedCount });
489
+ // Without ctx the check falls back to plan-relative and trips (1/1).
490
+ expect(runGuardrailChecks(cs, [check]).ok).toBe(false);
491
+ // With the live count threaded through, 1/10 passes.
492
+ expect(runGuardrailChecks(cs, [check], { managedCount: 10 })).toEqual({ ok: true });
493
+ });
224
494
  });
225
495
 
226
496
  // ---------------------------------------------------------------------------
package/src/reconcile.ts CHANGED
@@ -66,6 +66,28 @@ export interface ChangeSet {
66
66
  org: string;
67
67
  /** All proposed changes, in stable order. */
68
68
  entries: ChangeSetEntry[];
69
+ /**
70
+ * Live entries under management across the diffed collections (#2067).
71
+ * `diffCollection` returns each collection's live count so the diff layer
72
+ * can sum it here. Optional — when absent, guardrails that want a live
73
+ * denominator (see {@link removalDeltaCap}) fall back to the plan-relative
74
+ * one.
75
+ *
76
+ * Superseded by {@link ChangeSet.managedCounts}: a single pooled total lets
77
+ * non-deletable and cross-collection live entries dilute the delete
78
+ * fraction (wiping 3 of 4 teams reads as 12.5% when 20 team members pad
79
+ * the pool). Kept for compatibility — a change set carrying only this
80
+ * total keeps the 0.55.0 pooled behavior.
81
+ */
82
+ managedCount?: number;
83
+ /**
84
+ * Live managed-entry counts per resource type, keyed by
85
+ * `ChangeSetEntry.resourceType`. When present, {@link removalDeltaCap}
86
+ * evaluates the delete fraction per resource type instead of against the
87
+ * pooled `managedCount`, so live entries of one type cannot dilute a wipe
88
+ * of another. Supersedes `managedCount`.
89
+ */
90
+ managedCounts?: Readonly<Record<string, number>>;
69
91
  }
70
92
 
71
93
  /** Options controlling diff behaviour. */
@@ -147,8 +169,11 @@ export interface DiffCollectionParams<D, L> {
147
169
  * when `compareFields` reports differences, and ownership-gated deletes for
148
170
  * live-not-desired. This is the selective-by-omission + ownership-gated-delete
149
171
  * pattern shared by every keyed-collection diff.
172
+ *
173
+ * Returns the live entry count for this collection, so the diff layer can sum
174
+ * `ChangeSet.managedCount` across the collections it diffs (#2067).
150
175
  */
151
- export function diffCollection<D, L>(params: DiffCollectionParams<D, L>): void {
176
+ export function diffCollection<D, L>(params: DiffCollectionParams<D, L>): number {
152
177
  const {
153
178
  resourceType,
154
179
  keyPrefix = "",
@@ -193,6 +218,8 @@ export function diffCollection<D, L>(params: DiffCollectionParams<D, L>): void {
193
218
  out.push({ kind: "delete", resourceType, key: entryKey, before: l });
194
219
  }
195
220
  }
221
+
222
+ return live.size;
196
223
  }
197
224
 
198
225
  // ---------------------------------------------------------------------------
@@ -263,25 +290,69 @@ export interface GuardrailDiagnostic {
263
290
  /** Aggregated guardrail result. */
264
291
  export type GuardrailResult = { ok: true } | { ok: false; diagnostics: GuardrailDiagnostic[] };
265
292
 
266
- /** A guardrail check over a (rename-resolved) change set. Returns null when it passes. */
267
- export type GuardrailCheck = (resolved: ChangeSet) => GuardrailDiagnostic | null;
293
+ /** Live context threaded to guardrail checks by `runGuardrailChecks` (#2067). */
294
+ export interface GuardrailContext {
295
+ /** Live entries under management this cycle (see `ChangeSet.managedCount`). */
296
+ managedCount?: number;
297
+ }
298
+
299
+ /**
300
+ * A guardrail check over a (rename-resolved) change set. Returns null when it
301
+ * passes. `ctx` carries live context when the caller has it (#2067); a check
302
+ * written against the single-argument shape keeps working unchanged.
303
+ */
304
+ export type GuardrailCheck = (resolved: ChangeSet, ctx?: GuardrailContext) => GuardrailDiagnostic | null;
268
305
 
269
306
  /** Config for `removalDeltaCap`. */
270
307
  export interface RemovalDeltaCapOptions {
271
- /** Max fraction of pre-existing entries that may be deleted. Must be in (0,1]. Default 0.25. */
308
+ /**
309
+ * Max fraction of pre-existing entries that may be deleted. Must be in
310
+ * (0,1]: `removalDeltaCap` throws an `Error` on any value outside that
311
+ * range (0, negatives, above 1, NaN) rather than silently disabling the
312
+ * cap. Default 0.25.
313
+ */
272
314
  maxFraction?: number;
315
+ /**
316
+ * Pooled live managed-entry denominator (#2067). When positive, deletes are
317
+ * divided by this count of live entries under management instead of the
318
+ * plan's non-create entries — a converged cycle deleting one stale entry
319
+ * out of many no longer reads as 1/1 = 100%. Defaults to
320
+ * `changeSet.managedCount`; when neither is present (or not positive) the
321
+ * cap keeps its plan-relative behavior.
322
+ *
323
+ * Superseded by {@link RemovalDeltaCapOptions.managedTotals} — the pooled
324
+ * total lets unrelated live entries dilute the delete fraction.
325
+ */
326
+ managedTotal?: number;
327
+ /**
328
+ * Live managed-entry counts per resource type (keys =
329
+ * `ChangeSetEntry.resourceType`), mirroring {@link ChangeSet.managedCounts}
330
+ * and winning over it when both are present. When per-type counts are
331
+ * available from either source, the cap is evaluated per resource type:
332
+ * each type's deletes divide by that type's live count when present and
333
+ * positive, else by that type's own plan non-creates. Supersedes
334
+ * `managedTotal`.
335
+ */
336
+ managedTotals?: Readonly<Record<string, number>>;
273
337
  }
274
338
 
275
339
  /**
276
340
  * Resolve rename aliases. A create entry carrying a `previously` key matching a
277
- * delete entry's key is collapsed into an update, removing the delete. Returns a
278
- * new ChangeSet with renames resolved. Provider-agnostic — works on any entry
279
- * whose `after.previously` is a string.
341
+ * delete entry's key OF THE SAME RESOURCE TYPE is collapsed into an update,
342
+ * removing the delete. Matching across resource types would let e.g. a team
343
+ * create with `previously: "old"` swallow an unrelated member delete keyed
344
+ * "old", hiding a real delete from guardrail counting. Returns a new ChangeSet
345
+ * with renames resolved. Provider-agnostic — works on any entry whose
346
+ * `after.previously` is a string.
280
347
  */
281
348
  export function resolveRenames(changeSet: ChangeSet): ChangeSet {
349
+ // Keys and resource types are both free-form strings, so scope the lookup
350
+ // with an unambiguous composite key ("\u0000" cannot appear in either).
351
+ const typedKey = (resourceType: string, key: string): string => `${resourceType}\u0000${key}`;
352
+
282
353
  const deleteEntries = new Map<string, ChangeSetEntry>();
283
354
  for (const e of changeSet.entries) {
284
- if (e.kind === "delete") deleteEntries.set(e.key, e);
355
+ if (e.kind === "delete") deleteEntries.set(typedKey(e.resourceType, e.key), e);
285
356
  }
286
357
 
287
358
  const resolvedDeletes = new Set<string>();
@@ -295,11 +366,11 @@ export function resolveRenames(changeSet: ChangeSet): ChangeSet {
295
366
  const previously = after["previously"];
296
367
  if (typeof previously !== "string") continue;
297
368
 
298
- const deleted = deleteEntries.get(previously);
369
+ const deleted = deleteEntries.get(typedKey(e.resourceType, previously));
299
370
  if (!deleted) continue;
300
371
 
301
- resolvedDeletes.add(previously);
302
- resolvedCreates.add(e.key);
372
+ resolvedDeletes.add(typedKey(e.resourceType, previously));
373
+ resolvedCreates.add(typedKey(e.resourceType, e.key));
303
374
  syntheticUpdates.push({
304
375
  kind: "update",
305
376
  resourceType: e.resourceType,
@@ -314,11 +385,11 @@ export function resolveRenames(changeSet: ChangeSet): ChangeSet {
314
385
 
315
386
  const filteredEntries = changeSet.entries.filter(
316
387
  (e) =>
317
- !(e.kind === "delete" && resolvedDeletes.has(e.key)) &&
318
- !(e.kind === "create" && resolvedCreates.has(e.key)),
388
+ !(e.kind === "delete" && resolvedDeletes.has(typedKey(e.resourceType, e.key))) &&
389
+ !(e.kind === "create" && resolvedCreates.has(typedKey(e.resourceType, e.key))),
319
390
  );
320
391
 
321
- return { org: changeSet.org, entries: [...filteredEntries, ...syntheticUpdates] };
392
+ return { ...changeSet, entries: [...filteredEntries, ...syntheticUpdates] };
322
393
  }
323
394
 
324
395
  /**
@@ -326,6 +397,24 @@ export function resolveRenames(changeSet: ChangeSet): ChangeSet {
326
397
  * (deletes + updates; creates excluded so a flood of new entries can't dilute
327
398
  * the delete fraction). Guards against a typo wiping the config in one apply.
328
399
  *
400
+ * With per-type live counts (`opts.managedTotals`, else
401
+ * `changeSet.managedCounts`) the cap is evaluated PER RESOURCE TYPE: for each
402
+ * type with deletes, that type's deletes divide by that type's live count when
403
+ * present and positive, else by that type's own plan non-creates. Any type
404
+ * over `maxFraction` blocks, and the diagnostic names the worst offender. The
405
+ * pooled denominator this replaces was unsafe — live entries of one type
406
+ * (team members, repos) diluted a wipe of another (teams, org secrets) below
407
+ * the cap.
408
+ *
409
+ * With only a pooled live denominator (`opts.managedTotal`, else
410
+ * `changeSet.managedCount`; #2067) deletes are measured against the pooled
411
+ * live entries under management — so a converged cycle's plan of exactly one
412
+ * stale delete does not trip at 1/1 = 100%. Without either, behavior is
413
+ * plan-relative as before.
414
+ *
415
+ * Throws on a `maxFraction` outside (0,1] (NaN included) — fail closed, never
416
+ * silently disable the cap.
417
+ *
329
418
  * CONTRACT: pass a RENAME-RESOLVED change set (see {@link resolveRenames}).
330
419
  */
331
420
  export function removalDeltaCap(
@@ -333,9 +422,36 @@ export function removalDeltaCap(
333
422
  opts: RemovalDeltaCapOptions = {},
334
423
  ): GuardrailDiagnostic | null {
335
424
  const maxFraction = opts.maxFraction ?? 0.25;
425
+ if (!(maxFraction > 0 && maxFraction <= 1)) {
426
+ // The negated form catches NaN too. A bad cap must never pass silently:
427
+ // maxFraction: 5 would otherwise wave every wipe through.
428
+ throw new Error(
429
+ `removalDeltaCap: maxFraction must be in (0, 1], got ${String(maxFraction)}`,
430
+ );
431
+ }
432
+ const managedTotals = opts.managedTotals ?? changeSet.managedCounts;
433
+ if (managedTotals !== undefined) {
434
+ return removalDeltaCapPerType(changeSet, managedTotals, maxFraction);
435
+ }
436
+
437
+ const deletes = changeSet.entries.filter((e) => e.kind === "delete").length;
438
+ const managedTotal = opts.managedTotal ?? changeSet.managedCount;
439
+ if (managedTotal !== undefined && managedTotal > 0) {
440
+ const fraction = deletes / managedTotal;
441
+ if (fraction > maxFraction) {
442
+ return {
443
+ guardrail: "removalDeltaCap",
444
+ message:
445
+ `${deletes} of ${managedTotal} live managed entries (${Math.round(fraction * 100)}%) would be deleted, ` +
446
+ `exceeding the ${Math.round(maxFraction * 100)}% threshold. ` +
447
+ `Check for typos in config or raise maxFraction to proceed.`,
448
+ };
449
+ }
450
+ return null;
451
+ }
452
+
336
453
  const total = changeSet.entries.filter((e) => e.kind !== "create").length;
337
454
  if (total === 0) return null;
338
- const deletes = changeSet.entries.filter((e) => e.kind === "delete").length;
339
455
  const fraction = deletes / total;
340
456
  if (fraction > maxFraction) {
341
457
  return {
@@ -349,16 +465,89 @@ export function removalDeltaCap(
349
465
  return null;
350
466
  }
351
467
 
468
+ /** One resource type's delete fraction, for the per-type cap evaluation. */
469
+ interface TypeFraction {
470
+ resourceType: string;
471
+ deletes: number;
472
+ total: number;
473
+ fraction: number;
474
+ /** True when `total` is a live count; false for the plan-relative fallback. */
475
+ live: boolean;
476
+ }
477
+
478
+ /**
479
+ * Per-resource-type evaluation for {@link removalDeltaCap}. Each type with
480
+ * deletes gets its own denominator: the type's live count from `managedTotals`
481
+ * when present and positive, else that type's plan non-creates (the same
482
+ * plan-relative rule as the typeless cap, scoped to the type). Blocks when ANY
483
+ * type exceeds `maxFraction`; the message leads with the worst offender and
484
+ * lists the rest.
485
+ */
486
+ function removalDeltaCapPerType(
487
+ changeSet: ChangeSet,
488
+ managedTotals: Readonly<Record<string, number>>,
489
+ maxFraction: number,
490
+ ): GuardrailDiagnostic | null {
491
+ const deletesByType = new Map<string, number>();
492
+ const nonCreatesByType = new Map<string, number>();
493
+ for (const e of changeSet.entries) {
494
+ if (e.kind === "create") continue;
495
+ nonCreatesByType.set(e.resourceType, (nonCreatesByType.get(e.resourceType) ?? 0) + 1);
496
+ if (e.kind === "delete") {
497
+ deletesByType.set(e.resourceType, (deletesByType.get(e.resourceType) ?? 0) + 1);
498
+ }
499
+ }
500
+
501
+ const tripping: TypeFraction[] = [];
502
+ for (const [resourceType, deletes] of deletesByType) {
503
+ const liveCount = managedTotals[resourceType];
504
+ // A zero (or negative) live count means "no live info for this type",
505
+ // never a looser or divide-by-zero cap — same rule as the pooled total.
506
+ const live = liveCount !== undefined && liveCount > 0;
507
+ const total = live ? (liveCount as number) : nonCreatesByType.get(resourceType)!;
508
+ const fraction = deletes / total;
509
+ if (fraction > maxFraction) tripping.push({ resourceType, deletes, total, fraction, live });
510
+ }
511
+
512
+ if (tripping.length === 0) return null;
513
+
514
+ // Worst offender first; deterministic tie-break by delete count then name.
515
+ tripping.sort(
516
+ (a, b) =>
517
+ b.fraction - a.fraction ||
518
+ b.deletes - a.deletes ||
519
+ a.resourceType.localeCompare(b.resourceType),
520
+ );
521
+ const [worst, ...rest] = tripping as [TypeFraction, ...TypeFraction[]];
522
+ const pct = (f: number): number => Math.round(f * 100);
523
+ const describe = (t: TypeFraction): string =>
524
+ `${t.deletes} of ${t.total} ${t.live ? "live" : "planned"} ${t.resourceType} entries (${pct(t.fraction)}%)`;
525
+
526
+ return {
527
+ guardrail: "removalDeltaCap",
528
+ message:
529
+ `${describe(worst)} would be deleted, exceeding the ${pct(maxFraction)}% threshold. ` +
530
+ (rest.length > 0 ? `Also over the cap: ${rest.map(describe).join(", ")}. ` : "") +
531
+ `Check for typos in config or raise maxFraction to proceed.`,
532
+ };
533
+ }
534
+
352
535
  /**
353
536
  * Run a set of guardrail checks against a change set. Resolves renames ONCE,
354
537
  * then runs every check on the resolved set, aggregating any diagnostics. The
355
538
  * caller composes provider-specific checks (e.g. an admin floor) as closures.
539
+ * `ctx` (optional, #2067) threads live context — e.g. the managed-live count —
540
+ * to every check; single-argument checks ignore it.
356
541
  */
357
- export function runGuardrailChecks(changeSet: ChangeSet, checks: GuardrailCheck[]): GuardrailResult {
542
+ export function runGuardrailChecks(
543
+ changeSet: ChangeSet,
544
+ checks: GuardrailCheck[],
545
+ ctx?: GuardrailContext,
546
+ ): GuardrailResult {
358
547
  const resolved = resolveRenames(changeSet);
359
548
  const diagnostics: GuardrailDiagnostic[] = [];
360
549
  for (const check of checks) {
361
- const d = check(resolved);
550
+ const d = check(resolved, ctx);
362
551
  if (d) diagnostics.push(d);
363
552
  }
364
553
  return diagnostics.length > 0 ? { ok: false, diagnostics } : { ok: true };