@intentius/chant 0.44.14 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +21 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +275 -5
- package/src/cli/commands/build.ts +86 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +22 -2
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +49 -3
- package/src/codegen/validate.ts +36 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +159 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -72,8 +72,8 @@ export interface RemovedEntry {
|
|
|
72
72
|
entry: SurfaceEntry;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
/** Structural differences between two versions of one entry. */
|
|
76
|
+
export interface EntryChanges {
|
|
77
77
|
/** New resourceType (breaking). */
|
|
78
78
|
resourceTypeChanged?: { before: string; after: string };
|
|
79
79
|
/** kind changed from resource to property or vice-versa (breaking). */
|
|
@@ -96,14 +96,40 @@ export interface ChangedEntry {
|
|
|
96
96
|
taggableChanged?: { before?: boolean; after?: boolean };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export interface ChangedEntry extends EntryChanges {
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An entry whose TS export name changed while its kind and resourceType
|
|
105
|
+
* stayed the same (#1460). Paired from one removal and one addition.
|
|
106
|
+
*/
|
|
107
|
+
export interface RenamedEntry {
|
|
108
|
+
/** TS export name in the baseline. */
|
|
109
|
+
from: string;
|
|
110
|
+
/** TS export name in the fresh surface. */
|
|
111
|
+
to: string;
|
|
112
|
+
/** The entry as it now appears. */
|
|
113
|
+
entry: SurfaceEntry;
|
|
114
|
+
/** Structural differences between the two, when any. */
|
|
115
|
+
changes?: EntryChanges;
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
export interface SurfaceDelta {
|
|
100
119
|
added: AddedEntry[];
|
|
101
120
|
changed: ChangedEntry[];
|
|
102
121
|
removed: RemovedEntry[];
|
|
122
|
+
/**
|
|
123
|
+
* Removals and additions paired by kind + resourceType. A rename still
|
|
124
|
+
* breaks the old import, so it counts as breaking; it is reported apart
|
|
125
|
+
* from removals so a reader can tell the two kinds of break apart.
|
|
126
|
+
*/
|
|
127
|
+
renamed: RenamedEntry[];
|
|
103
128
|
/**
|
|
104
129
|
* Rolled-up severity. "none" means no surface changes at all.
|
|
105
130
|
* "additive" means only new resources / new optional props / new attrs.
|
|
106
|
-
* "breaking" means any removal or change that could break
|
|
131
|
+
* "breaking" means any removal, rename, or change that could break
|
|
132
|
+
* existing consumers.
|
|
107
133
|
*/
|
|
108
134
|
severity: ChangeSeverity;
|
|
109
135
|
}
|
|
@@ -314,9 +340,9 @@ export function extractPropsFromDts(dts: string): Map<string, string[]> {
|
|
|
314
340
|
* - taggable: false→true or undefined→true → additive
|
|
315
341
|
*/
|
|
316
342
|
export function diffSurface(baseline: SurfaceSnapshot, fresh: SurfaceSnapshot): SurfaceDelta {
|
|
317
|
-
const added: AddedEntry[] = [];
|
|
318
343
|
const changed: ChangedEntry[] = [];
|
|
319
|
-
|
|
344
|
+
let added: AddedEntry[] = [];
|
|
345
|
+
let removed: RemovedEntry[] = [];
|
|
320
346
|
|
|
321
347
|
const baseNames = new Set(Object.keys(baseline.entries));
|
|
322
348
|
const freshNames = new Set(Object.keys(fresh.entries));
|
|
@@ -335,119 +361,253 @@ export function diffSurface(baseline: SurfaceSnapshot, fresh: SurfaceSnapshot):
|
|
|
335
361
|
}
|
|
336
362
|
}
|
|
337
363
|
|
|
364
|
+
// Renamed (#1460): a removal and an addition that carry the same
|
|
365
|
+
// resourceType and kind are one entry under a new TS name. Pair them
|
|
366
|
+
// before classifying so the report shows what upstream actually did.
|
|
367
|
+
const renamed: RenamedEntry[] = [];
|
|
368
|
+
const paired = pairRenames(removed, added);
|
|
369
|
+
for (const { from, to } of paired) {
|
|
370
|
+
const entry: RenamedEntry = { from: from.name, to: to.name, entry: to.entry };
|
|
371
|
+
const changes = diffEntries(from.entry, to.entry);
|
|
372
|
+
if (changes) entry.changes = changes;
|
|
373
|
+
renamed.push(entry);
|
|
374
|
+
}
|
|
375
|
+
if (paired.length > 0) {
|
|
376
|
+
const fromNames = new Set(paired.map((p) => p.from.name));
|
|
377
|
+
const toNames = new Set(paired.map((p) => p.to.name));
|
|
378
|
+
removed = removed.filter((r) => !fromNames.has(r.name));
|
|
379
|
+
added = added.filter((a) => !toNames.has(a.name));
|
|
380
|
+
}
|
|
381
|
+
renamed.sort((a, b) => a.from.localeCompare(b.from));
|
|
382
|
+
|
|
338
383
|
// Changed
|
|
339
384
|
for (const name of baseNames) {
|
|
340
385
|
if (!freshNames.has(name)) continue;
|
|
386
|
+
const changes = diffEntries(baseline.entries[name], fresh.entries[name]);
|
|
387
|
+
if (changes) changed.push({ name, ...changes });
|
|
388
|
+
}
|
|
341
389
|
|
|
342
|
-
|
|
343
|
-
|
|
390
|
+
// Severity
|
|
391
|
+
let severity: ChangeSeverity = "none";
|
|
344
392
|
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
if (added.length > 0) {
|
|
394
|
+
severity = "additive";
|
|
395
|
+
}
|
|
347
396
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
hasChange = true;
|
|
351
|
-
}
|
|
397
|
+
// Additive-only changes in the changed[] set
|
|
398
|
+
const hasAdditiveChanges = changed.some(isAdditiveChange);
|
|
352
399
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
400
|
+
if (hasAdditiveChanges && severity === "none") {
|
|
401
|
+
severity = "additive";
|
|
402
|
+
}
|
|
357
403
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
365
|
-
const afterProps = new Map(
|
|
366
|
-
(after.props ?? []).map((p) => {
|
|
367
|
-
const [nm, req] = splitProp(p);
|
|
368
|
-
return [nm, req === "true"];
|
|
369
|
-
}),
|
|
370
|
-
);
|
|
404
|
+
// A rename is still breaking for the consumer: the old import no longer
|
|
405
|
+
// resolves, whether the entry is a resource or a property type. It is
|
|
406
|
+
// reported separately so the reader can tell a rename from a deletion.
|
|
407
|
+
const hasBreaking =
|
|
408
|
+
removed.length > 0 ||
|
|
409
|
+
renamed.length > 0 ||
|
|
410
|
+
changed.some(isBreakingChange);
|
|
371
411
|
|
|
372
|
-
|
|
373
|
-
const addedProps = [...afterProps.keys()].filter((k) => !beforeProps.has(k));
|
|
374
|
-
const nowRequired = [...beforeProps.keys()].filter(
|
|
375
|
-
(k) => afterProps.has(k) && !beforeProps.get(k) && afterProps.get(k),
|
|
376
|
-
);
|
|
377
|
-
const nowOptional = [...beforeProps.keys()].filter(
|
|
378
|
-
(k) => afterProps.has(k) && beforeProps.get(k) && !afterProps.get(k),
|
|
379
|
-
);
|
|
412
|
+
if (hasBreaking) severity = "breaking";
|
|
380
413
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (nowRequired.length > 0) { change.nowRequired = nowRequired.sort(); hasChange = true; }
|
|
384
|
-
if (nowOptional.length > 0) { change.nowOptional = nowOptional.sort(); hasChange = true; }
|
|
385
|
-
|
|
386
|
-
// Attrs diff
|
|
387
|
-
const beforeAttrs = new Set(before.attrs ?? []);
|
|
388
|
-
const afterAttrs = new Set(after.attrs ?? []);
|
|
389
|
-
const removedAttrs = [...beforeAttrs].filter((a) => !afterAttrs.has(a));
|
|
390
|
-
const addedAttrs = [...afterAttrs].filter((a) => !beforeAttrs.has(a));
|
|
391
|
-
if (removedAttrs.length > 0) { change.removedAttrs = removedAttrs.sort(); hasChange = true; }
|
|
392
|
-
if (addedAttrs.length > 0) { change.addedAttrs = addedAttrs.sort(); hasChange = true; }
|
|
393
|
-
|
|
394
|
-
// createOnly diff
|
|
395
|
-
const beforeCO = JSON.stringify((before.createOnly ?? []).sort());
|
|
396
|
-
const afterCO = JSON.stringify((after.createOnly ?? []).sort());
|
|
397
|
-
if (beforeCO !== afterCO) {
|
|
398
|
-
change.createOnlyChanged = {
|
|
399
|
-
before: before.createOnly ?? [],
|
|
400
|
-
after: after.createOnly ?? [],
|
|
401
|
-
};
|
|
402
|
-
hasChange = true;
|
|
403
|
-
}
|
|
414
|
+
return { added, changed, removed, renamed, severity };
|
|
415
|
+
}
|
|
404
416
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Diff two entries that are (or are taken to be) the same thing. Returns
|
|
419
|
+
* `null` when nothing differs.
|
|
420
|
+
*/
|
|
421
|
+
function diffEntries(before: SurfaceEntry, after: SurfaceEntry): EntryChanges | null {
|
|
422
|
+
const change: EntryChanges = {};
|
|
423
|
+
let hasChange = false;
|
|
410
424
|
|
|
411
|
-
|
|
425
|
+
if (before.resourceType !== after.resourceType) {
|
|
426
|
+
change.resourceTypeChanged = { before: before.resourceType, after: after.resourceType };
|
|
427
|
+
hasChange = true;
|
|
412
428
|
}
|
|
413
429
|
|
|
414
|
-
|
|
415
|
-
|
|
430
|
+
if (before.kind !== after.kind) {
|
|
431
|
+
change.kindChanged = { before: before.kind, after: after.kind };
|
|
432
|
+
hasChange = true;
|
|
433
|
+
}
|
|
416
434
|
|
|
417
|
-
|
|
418
|
-
|
|
435
|
+
// Props diff (only meaningful for resource entries)
|
|
436
|
+
const beforeProps = new Map(
|
|
437
|
+
(before.props ?? []).map((p) => {
|
|
438
|
+
const [nm, req] = splitProp(p);
|
|
439
|
+
return [nm, req === "true"];
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
const afterProps = new Map(
|
|
443
|
+
(after.props ?? []).map((p) => {
|
|
444
|
+
const [nm, req] = splitProp(p);
|
|
445
|
+
return [nm, req === "true"];
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const removedProps = [...beforeProps.keys()].filter((k) => !afterProps.has(k));
|
|
450
|
+
const addedProps = [...afterProps.keys()].filter((k) => !beforeProps.has(k));
|
|
451
|
+
const nowRequired = [...beforeProps.keys()].filter(
|
|
452
|
+
(k) => afterProps.has(k) && !beforeProps.get(k) && afterProps.get(k),
|
|
453
|
+
);
|
|
454
|
+
const nowOptional = [...beforeProps.keys()].filter(
|
|
455
|
+
(k) => afterProps.has(k) && beforeProps.get(k) && !afterProps.get(k),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
if (removedProps.length > 0) { change.removedProps = removedProps.sort(); hasChange = true; }
|
|
459
|
+
if (addedProps.length > 0) { change.addedProps = addedProps.sort(); hasChange = true; }
|
|
460
|
+
if (nowRequired.length > 0) { change.nowRequired = nowRequired.sort(); hasChange = true; }
|
|
461
|
+
if (nowOptional.length > 0) { change.nowOptional = nowOptional.sort(); hasChange = true; }
|
|
462
|
+
|
|
463
|
+
// Attrs diff
|
|
464
|
+
const beforeAttrs = new Set(before.attrs ?? []);
|
|
465
|
+
const afterAttrs = new Set(after.attrs ?? []);
|
|
466
|
+
const removedAttrs = [...beforeAttrs].filter((a) => !afterAttrs.has(a));
|
|
467
|
+
const addedAttrs = [...afterAttrs].filter((a) => !beforeAttrs.has(a));
|
|
468
|
+
if (removedAttrs.length > 0) { change.removedAttrs = removedAttrs.sort(); hasChange = true; }
|
|
469
|
+
if (addedAttrs.length > 0) { change.addedAttrs = addedAttrs.sort(); hasChange = true; }
|
|
470
|
+
|
|
471
|
+
// createOnly diff
|
|
472
|
+
const beforeCO = JSON.stringify((before.createOnly ?? []).sort());
|
|
473
|
+
const afterCO = JSON.stringify((after.createOnly ?? []).sort());
|
|
474
|
+
if (beforeCO !== afterCO) {
|
|
475
|
+
change.createOnlyChanged = {
|
|
476
|
+
before: before.createOnly ?? [],
|
|
477
|
+
after: after.createOnly ?? [],
|
|
478
|
+
};
|
|
479
|
+
hasChange = true;
|
|
419
480
|
}
|
|
420
481
|
|
|
421
|
-
//
|
|
422
|
-
|
|
482
|
+
// taggable diff
|
|
483
|
+
if (before.taggable !== after.taggable) {
|
|
484
|
+
change.taggableChanged = { before: before.taggable, after: after.taggable };
|
|
485
|
+
hasChange = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return hasChange ? change : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isAdditiveChange(c: EntryChanges): boolean {
|
|
492
|
+
return (
|
|
423
493
|
(c.addedProps?.length ?? 0) > 0 ||
|
|
424
494
|
(c.nowOptional?.length ?? 0) > 0 ||
|
|
425
495
|
(c.addedAttrs?.length ?? 0) > 0 ||
|
|
426
496
|
// gaining tagging is additive
|
|
427
|
-
(c.taggableChanged !== undefined && c.taggableChanged.after === true && c.taggableChanged.before !== true)
|
|
497
|
+
(c.taggableChanged !== undefined && c.taggableChanged.after === true && c.taggableChanged.before !== true)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isBreakingChange(c: EntryChanges): boolean {
|
|
502
|
+
return (
|
|
503
|
+
c.resourceTypeChanged !== undefined ||
|
|
504
|
+
c.kindChanged !== undefined ||
|
|
505
|
+
(c.removedProps?.length ?? 0) > 0 ||
|
|
506
|
+
(c.nowRequired?.length ?? 0) > 0 ||
|
|
507
|
+
(c.removedAttrs?.length ?? 0) > 0 ||
|
|
508
|
+
c.createOnlyChanged !== undefined ||
|
|
509
|
+
// losing taggable is breaking
|
|
510
|
+
(c.taggableChanged !== undefined && c.taggableChanged.before === true && c.taggableChanged.after !== true)
|
|
428
511
|
);
|
|
512
|
+
}
|
|
429
513
|
|
|
430
|
-
|
|
431
|
-
|
|
514
|
+
// ── Rename pairing ───────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
interface RenamePair {
|
|
517
|
+
from: RemovedEntry;
|
|
518
|
+
to: AddedEntry;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Pair removed and added entries that share a kind and resourceType.
|
|
523
|
+
*
|
|
524
|
+
* One removal and one addition under a resourceType pair directly. When a
|
|
525
|
+
* resourceType has several of each (property types of one renamed parent
|
|
526
|
+
* resource, for instance), pairs are chosen greedily by name similarity and
|
|
527
|
+
* whatever is left over stays a plain removal or addition. Entries with an
|
|
528
|
+
* empty resourceType are never paired; there is nothing to key them on.
|
|
529
|
+
*/
|
|
530
|
+
function pairRenames(removed: RemovedEntry[], added: AddedEntry[]): RenamePair[] {
|
|
531
|
+
const key = (e: SurfaceEntry): string => `${e.kind} ${e.resourceType}`;
|
|
532
|
+
|
|
533
|
+
const addedByKey = new Map<string, AddedEntry[]>();
|
|
534
|
+
for (const a of added) {
|
|
535
|
+
if (!a.entry.resourceType) continue;
|
|
536
|
+
const k = key(a.entry);
|
|
537
|
+
const bucket = addedByKey.get(k);
|
|
538
|
+
if (bucket) bucket.push(a);
|
|
539
|
+
else addedByKey.set(k, [a]);
|
|
432
540
|
}
|
|
433
541
|
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
542
|
+
const removedByKey = new Map<string, RemovedEntry[]>();
|
|
543
|
+
for (const r of removed) {
|
|
544
|
+
if (!r.entry.resourceType) continue;
|
|
545
|
+
const k = key(r.entry);
|
|
546
|
+
if (!addedByKey.has(k)) continue;
|
|
547
|
+
const bucket = removedByKey.get(k);
|
|
548
|
+
if (bucket) bucket.push(r);
|
|
549
|
+
else removedByKey.set(k, [r]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const pairs: RenamePair[] = [];
|
|
553
|
+
for (const [k, olds] of removedByKey) {
|
|
554
|
+
const news = addedByKey.get(k) ?? [];
|
|
555
|
+
if (olds.length === 1 && news.length === 1) {
|
|
556
|
+
pairs.push({ from: olds[0], to: news[0] });
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Ambiguous: rank every (old, new) combination by similarity and take
|
|
561
|
+
// the best remaining pair until one side runs out.
|
|
562
|
+
const candidates: Array<{ from: RemovedEntry; to: AddedEntry; score: number }> = [];
|
|
563
|
+
for (const from of olds) {
|
|
564
|
+
for (const to of news) {
|
|
565
|
+
candidates.push({ from, to, score: nameSimilarity(from.name, to.name) });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
candidates.sort((a, b) =>
|
|
569
|
+
b.score - a.score ||
|
|
570
|
+
a.from.name.localeCompare(b.from.name) ||
|
|
571
|
+
a.to.name.localeCompare(b.to.name),
|
|
446
572
|
);
|
|
573
|
+
const usedOld = new Set<string>();
|
|
574
|
+
const usedNew = new Set<string>();
|
|
575
|
+
for (const cand of candidates) {
|
|
576
|
+
if (usedOld.has(cand.from.name) || usedNew.has(cand.to.name)) continue;
|
|
577
|
+
usedOld.add(cand.from.name);
|
|
578
|
+
usedNew.add(cand.to.name);
|
|
579
|
+
pairs.push({ from: cand.from, to: cand.to });
|
|
580
|
+
if (usedOld.size === olds.length || usedNew.size === news.length) break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
447
583
|
|
|
448
|
-
|
|
584
|
+
return pairs;
|
|
585
|
+
}
|
|
449
586
|
|
|
450
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Dice similarity over character bigrams, in [0, 1]. Enough to tell
|
|
589
|
+
* `Space` apart from `Space_Tag` when pairing the property types of a
|
|
590
|
+
* renamed parent; nothing heavier is needed.
|
|
591
|
+
*/
|
|
592
|
+
function nameSimilarity(a: string, b: string): number {
|
|
593
|
+
if (a === b) return 1;
|
|
594
|
+
if (a.length < 2 || b.length < 2) return 0;
|
|
595
|
+
const bigrams = (s: string): Map<string, number> => {
|
|
596
|
+
const m = new Map<string, number>();
|
|
597
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
598
|
+
const bg = s.slice(i, i + 2);
|
|
599
|
+
m.set(bg, (m.get(bg) ?? 0) + 1);
|
|
600
|
+
}
|
|
601
|
+
return m;
|
|
602
|
+
};
|
|
603
|
+
const ba = bigrams(a);
|
|
604
|
+
const bb = bigrams(b);
|
|
605
|
+
let overlap = 0;
|
|
606
|
+
for (const [bg, n] of ba) {
|
|
607
|
+
const m = bb.get(bg);
|
|
608
|
+
if (m) overlap += Math.min(n, m);
|
|
609
|
+
}
|
|
610
|
+
return (2 * overlap) / (a.length - 1 + b.length - 1);
|
|
451
611
|
}
|
|
452
612
|
|
|
453
613
|
// ── Serialization ────────────────────────────────────────────────────
|
|
@@ -489,42 +649,19 @@ export function formatDelta(delta: SurfaceDelta): string {
|
|
|
489
649
|
}
|
|
490
650
|
}
|
|
491
651
|
|
|
652
|
+
if (delta.renamed.length > 0) {
|
|
653
|
+
lines.push(`Renamed (${delta.renamed.length}):`);
|
|
654
|
+
for (const r of delta.renamed) {
|
|
655
|
+
lines.push(` ~ ${r.from} -> ${r.to} [${r.entry.kind}] (${r.entry.resourceType})`);
|
|
656
|
+
if (r.changes) lines.push(...formatChanges(r.changes));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
492
660
|
if (delta.changed.length > 0) {
|
|
493
661
|
lines.push(`Changed (${delta.changed.length}):`);
|
|
494
662
|
for (const c of delta.changed) {
|
|
495
663
|
lines.push(` ~ ${c.name}`);
|
|
496
|
-
|
|
497
|
-
lines.push(` resourceType: ${c.resourceTypeChanged.before} -> ${c.resourceTypeChanged.after}`);
|
|
498
|
-
}
|
|
499
|
-
if (c.kindChanged) {
|
|
500
|
-
lines.push(` kind: ${c.kindChanged.before} -> ${c.kindChanged.after}`);
|
|
501
|
-
}
|
|
502
|
-
if (c.removedProps?.length) {
|
|
503
|
-
lines.push(` removed props: ${c.removedProps.join(", ")}`);
|
|
504
|
-
}
|
|
505
|
-
if (c.nowRequired?.length) {
|
|
506
|
-
lines.push(` now required: ${c.nowRequired.join(", ")}`);
|
|
507
|
-
}
|
|
508
|
-
if (c.addedProps?.length) {
|
|
509
|
-
lines.push(` added props: ${c.addedProps.join(", ")}`);
|
|
510
|
-
}
|
|
511
|
-
if (c.nowOptional?.length) {
|
|
512
|
-
lines.push(` now optional: ${c.nowOptional.join(", ")}`);
|
|
513
|
-
}
|
|
514
|
-
if (c.removedAttrs?.length) {
|
|
515
|
-
lines.push(` removed attrs: ${c.removedAttrs.join(", ")}`);
|
|
516
|
-
}
|
|
517
|
-
if (c.addedAttrs?.length) {
|
|
518
|
-
lines.push(` added attrs: ${c.addedAttrs.join(", ")}`);
|
|
519
|
-
}
|
|
520
|
-
if (c.createOnlyChanged) {
|
|
521
|
-
const before = c.createOnlyChanged.before.join(", ") || "(none)";
|
|
522
|
-
const after = c.createOnlyChanged.after.join(", ") || "(none)";
|
|
523
|
-
lines.push(` createOnly: [${before}] -> [${after}]`);
|
|
524
|
-
}
|
|
525
|
-
if (c.taggableChanged) {
|
|
526
|
-
lines.push(` taggable: ${c.taggableChanged.before ?? "(unset)"} -> ${c.taggableChanged.after ?? "(unset)"}`);
|
|
527
|
-
}
|
|
664
|
+
lines.push(...formatChanges(c));
|
|
528
665
|
}
|
|
529
666
|
}
|
|
530
667
|
|
|
@@ -538,6 +675,44 @@ export function formatDelta(delta: SurfaceDelta): string {
|
|
|
538
675
|
return lines.join("\n");
|
|
539
676
|
}
|
|
540
677
|
|
|
678
|
+
/** Indented detail lines for one entry's structural changes. */
|
|
679
|
+
function formatChanges(c: EntryChanges): string[] {
|
|
680
|
+
const lines: string[] = [];
|
|
681
|
+
if (c.resourceTypeChanged) {
|
|
682
|
+
lines.push(` resourceType: ${c.resourceTypeChanged.before} -> ${c.resourceTypeChanged.after}`);
|
|
683
|
+
}
|
|
684
|
+
if (c.kindChanged) {
|
|
685
|
+
lines.push(` kind: ${c.kindChanged.before} -> ${c.kindChanged.after}`);
|
|
686
|
+
}
|
|
687
|
+
if (c.removedProps?.length) {
|
|
688
|
+
lines.push(` removed props: ${c.removedProps.join(", ")}`);
|
|
689
|
+
}
|
|
690
|
+
if (c.nowRequired?.length) {
|
|
691
|
+
lines.push(` now required: ${c.nowRequired.join(", ")}`);
|
|
692
|
+
}
|
|
693
|
+
if (c.addedProps?.length) {
|
|
694
|
+
lines.push(` added props: ${c.addedProps.join(", ")}`);
|
|
695
|
+
}
|
|
696
|
+
if (c.nowOptional?.length) {
|
|
697
|
+
lines.push(` now optional: ${c.nowOptional.join(", ")}`);
|
|
698
|
+
}
|
|
699
|
+
if (c.removedAttrs?.length) {
|
|
700
|
+
lines.push(` removed attrs: ${c.removedAttrs.join(", ")}`);
|
|
701
|
+
}
|
|
702
|
+
if (c.addedAttrs?.length) {
|
|
703
|
+
lines.push(` added attrs: ${c.addedAttrs.join(", ")}`);
|
|
704
|
+
}
|
|
705
|
+
if (c.createOnlyChanged) {
|
|
706
|
+
const before = c.createOnlyChanged.before.join(", ") || "(none)";
|
|
707
|
+
const after = c.createOnlyChanged.after.join(", ") || "(none)";
|
|
708
|
+
lines.push(` createOnly: [${before}] -> [${after}]`);
|
|
709
|
+
}
|
|
710
|
+
if (c.taggableChanged) {
|
|
711
|
+
lines.push(` taggable: ${c.taggableChanged.before ?? "(unset)"} -> ${c.taggableChanged.after ?? "(unset)"}`);
|
|
712
|
+
}
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
715
|
+
|
|
541
716
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
542
717
|
|
|
543
718
|
function splitProp(encoded: string): [string, string] {
|
|
@@ -106,7 +106,7 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
106
106
|
return dir;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const run = (basePath: string, checkSurfaceSnapshot: boolean, armed = true) =>
|
|
109
|
+
const run = (basePath: string, checkSurfaceSnapshot: boolean | "always", armed = true) =>
|
|
110
110
|
validateLexiconArtifacts({
|
|
111
111
|
lexiconJsonFilename: "lexicon-test.json",
|
|
112
112
|
requiredNames: [],
|
|
@@ -141,8 +141,8 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("is off unless the lexicon opts in", async () => {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
144
|
+
// A lexicon with no baseline of its own, or one that generates from a
|
|
145
|
+
// moving upstream it has not pinned, must not be gated by default.
|
|
146
146
|
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
147
147
|
const result = await run(fixture({ snapshot: stale }), false);
|
|
148
148
|
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
@@ -158,6 +158,19 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
158
158
|
expect(result.success).toBe(true);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
test("\"always\" runs outside a release — a pinned upstream cannot move on its own (#1475)", async () => {
|
|
162
|
+
// k8s and azure generate from immutable refs, so drift on a PR can only
|
|
163
|
+
// come from this repo and is exactly what should fail the build.
|
|
164
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
165
|
+
const result = await run(fixture({ snapshot: stale }), "always", false);
|
|
166
|
+
const check = result.checks.find((c) => c.name === "surface-matches-snapshot");
|
|
167
|
+
expect(check?.ok).toBe(false);
|
|
168
|
+
expect(result.success).toBe(false);
|
|
169
|
+
|
|
170
|
+
const ok = await run(fixture({ snapshot: await matchingSnapshot() }), "always", false);
|
|
171
|
+
expect(ok.checks.find((c) => c.name === "surface-matches-snapshot")?.ok).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
161
174
|
test("is skipped for a lexicon with no committed snapshot", async () => {
|
|
162
175
|
// A new lexicon before its first baseline must still be able to build.
|
|
163
176
|
const result = await run(fixture(), true);
|
|
@@ -169,4 +182,37 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
169
182
|
const check = result.checks.find((c) => c.name === "surface-matches-snapshot");
|
|
170
183
|
expect(check?.ok).toBe(false);
|
|
171
184
|
});
|
|
185
|
+
|
|
186
|
+
test("an update run skips the staleness check it is about to fix (#1825)", async () => {
|
|
187
|
+
// `--update-snapshot` sets CHANT_SNAPSHOT_UPDATE for its validate run.
|
|
188
|
+
// Without this, an "always" gate deadlocks the documented re-baseline flow:
|
|
189
|
+
// validate fails on the stale snapshot, and the update writes only after a
|
|
190
|
+
// green validate.
|
|
191
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
192
|
+
const result = await validateLexiconArtifacts({
|
|
193
|
+
lexiconJsonFilename: "lexicon-test.json",
|
|
194
|
+
requiredNames: [],
|
|
195
|
+
basePath: fixture({ snapshot: stale }),
|
|
196
|
+
checkSurfaceSnapshot: "always",
|
|
197
|
+
env: { CHANT_SNAPSHOT_UPDATE: "1" },
|
|
198
|
+
});
|
|
199
|
+
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
200
|
+
expect(result.success).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an update run is exempt from the staleness check and nothing else", async () => {
|
|
204
|
+
// A failing check other than surface-matches-snapshot must still refuse
|
|
205
|
+
// the run, so a broken generate cannot be baselined.
|
|
206
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
207
|
+
const result = await validateLexiconArtifacts({
|
|
208
|
+
lexiconJsonFilename: "lexicon-test.json",
|
|
209
|
+
requiredNames: ["NotThere"],
|
|
210
|
+
basePath: fixture({ snapshot: stale }),
|
|
211
|
+
checkSurfaceSnapshot: "always",
|
|
212
|
+
env: { CHANT_SNAPSHOT_UPDATE: "1" },
|
|
213
|
+
});
|
|
214
|
+
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
215
|
+
expect(result.checks.find((c) => c.name === "required-names")?.ok).toBe(false);
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
});
|
|
172
218
|
});
|
package/src/codegen/validate.ts
CHANGED
|
@@ -27,6 +27,17 @@ export interface ValidateResult {
|
|
|
27
27
|
*/
|
|
28
28
|
export const RELEASE_GATE_ENV = "CHANT_RELEASE_GATE";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Set by `chant dev surface-diff --update-snapshot` for the validate run it
|
|
32
|
+
* performs before rewriting the baseline (chant #1825). When set to "1", the
|
|
33
|
+
* `surface-matches-snapshot` check is skipped. That check fails on exactly the
|
|
34
|
+
* staleness the update run exists to fix, and with an `"always"` gate (#1475)
|
|
35
|
+
* the two would deadlock: validate cannot pass until the snapshot is updated,
|
|
36
|
+
* and the snapshot cannot be updated until validate passes. Every other check
|
|
37
|
+
* still runs, so a broken generate cannot be baselined.
|
|
38
|
+
*/
|
|
39
|
+
export const SNAPSHOT_UPDATE_ENV = "CHANT_SNAPSHOT_UPDATE";
|
|
40
|
+
|
|
30
41
|
export interface LexiconValidationConfig {
|
|
31
42
|
/** Filename of the lexicon JSON (e.g. "lexicon-mydom.json") */
|
|
32
43
|
lexiconJsonFilename: string;
|
|
@@ -62,10 +73,18 @@ export interface LexiconValidationConfig {
|
|
|
62
73
|
* What must never happen is *publishing* a surface nobody reviewed. That is
|
|
63
74
|
* a release-time property, so it is checked at release time.
|
|
64
75
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
76
|
+
* `"always"` drops the env half (#1475). It is for a lexicon whose upstream
|
|
77
|
+
* is pinned to an immutable ref — k8s generates from a kubernetes release
|
|
78
|
+
* tag plus vendored CRDs, azure from a commit sha of the
|
|
79
|
+
* resource-manager-schemas repo — so a fresh `generate` on a PR is
|
|
80
|
+
* deterministic and the only way the surface can move is a change in this
|
|
81
|
+
* repo. For those, drift on a PR is exactly the thing to fail on: the CRD
|
|
82
|
+
* batches #1319/#1320/#1321 left the k8s baseline 393 entries behind, and
|
|
83
|
+
* the #1144 pin left azure 483 behind, because nothing compared the two
|
|
84
|
+
* until a release was attempted. Never use `"always"` for a lexicon that
|
|
85
|
+
* fetches a moving upstream.
|
|
67
86
|
*/
|
|
68
|
-
checkSurfaceSnapshot?: boolean;
|
|
87
|
+
checkSurfaceSnapshot?: boolean | "always";
|
|
69
88
|
/** Environment to read {@link RELEASE_GATE_ENV} from. Defaults to `process.env`; overridden in tests. */
|
|
70
89
|
env?: NodeJS.ProcessEnv;
|
|
71
90
|
/** Path to the generated directory (defaults to basePath/src/generated) */
|
|
@@ -193,13 +212,23 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
193
212
|
// Runs on the artifacts already on disk — no second generation — and is
|
|
194
213
|
// skipped for a lexicon with no committed snapshot, which is the case for a
|
|
195
214
|
// new lexicon before its first baseline.
|
|
215
|
+
//
|
|
216
|
+
// A re-baseline run (`chant dev surface-diff --update-snapshot`) sets
|
|
217
|
+
// {@link SNAPSHOT_UPDATE_ENV} and is exempt: the check would fail on the
|
|
218
|
+
// stale snapshot that run is about to rewrite, deadlocking the documented
|
|
219
|
+
// update flow against an `"always"` gate (#1825). Only this check is exempt —
|
|
220
|
+
// every other check above still gates the run.
|
|
196
221
|
const snapshotPath = join(config.basePath, "surface.snapshot.json");
|
|
197
|
-
const
|
|
198
|
-
|
|
222
|
+
const env = config.env ?? process.env;
|
|
223
|
+
const surfaceGate =
|
|
224
|
+
(config.checkSurfaceSnapshot === "always" ||
|
|
225
|
+
(config.checkSurfaceSnapshot === true && env[RELEASE_GATE_ENV] === "1")) &&
|
|
226
|
+
env[SNAPSHOT_UPDATE_ENV] !== "1";
|
|
227
|
+
if (surfaceGate && lexiconData && existsSync(snapshotPath) && existsSync(dtsPath)) {
|
|
199
228
|
try {
|
|
200
229
|
const fresh = extractSurface(readFileSync(lexiconPath, "utf-8"), readFileSync(dtsPath, "utf-8"));
|
|
201
230
|
const delta = diffSurface(parseSnapshot(readFileSync(snapshotPath, "utf-8")), fresh);
|
|
202
|
-
const moved = delta.added.length + delta.removed.length + delta.changed.length;
|
|
231
|
+
const moved = delta.added.length + delta.removed.length + delta.renamed.length + delta.changed.length;
|
|
203
232
|
checks.push(
|
|
204
233
|
moved === 0
|
|
205
234
|
? { name: "surface-matches-snapshot", ok: true }
|
|
@@ -208,7 +237,7 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
208
237
|
ok: false,
|
|
209
238
|
error:
|
|
210
239
|
`The generated API differs from the reviewed surface.snapshot.json ` +
|
|
211
|
-
`(${delta.added.length} added, ${delta.removed.length} removed, ${delta.changed.length} changed). ` +
|
|
240
|
+
`(${delta.added.length} added, ${delta.removed.length} removed, ${delta.renamed.length} renamed, ${delta.changed.length} changed). ` +
|
|
212
241
|
`Accept it deliberately with \`chant dev surface-diff <lexicon> --update-snapshot --bump\`, ` +
|
|
213
242
|
`never as a side effect of a release.\n${formatDelta(delta)}`,
|
|
214
243
|
},
|
|
@@ -42,7 +42,7 @@ import type { DriverStepRecord } from "./driver";
|
|
|
42
42
|
* `extract-config-bom` likewise *describes* an artifact rather than promoting
|
|
43
43
|
* one. Those must not trigger a release record (#665) — a release means "we
|
|
44
44
|
* promoted this by digest to a location", not "we hashed some bytes". The
|
|
45
|
-
* driver's own `
|
|
45
|
+
* driver's own `collectComponentOutputs` stays looser (uri/digest/key) because it
|
|
46
46
|
* only feeds `@<component>.publish.*` wiring, where a false positive is harmless.
|
|
47
47
|
*/
|
|
48
48
|
function isPromotedArtifact(output: unknown): output is { uri: string; digest?: string } {
|