From 8b5e42de2b319e3c8278e5c56ea8e016a91bd576 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 30 Jun 2026 11:20:53 -0400 Subject: [PATCH] fix(changelog): classify added fields as additive (feat), not fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A brand-new field is additive — a feature — but the SDK changelog was filing field additions under **Fixes** while the release correctly bumped minor. `model-added` and `enum-added` are hardcoded `additive`, but the `field-added` branch ran the differ's classification through `capSeverity`, which floors a would-be-breaking backend-only change at `fix`. The spec differ sometimes flags an added field `breaking` (it reads as a request-shape tightening), so the addition collapsed to `fix` -> **Fixes**, disagreeing with the `feat`/minor bump that came from a different additive fact in the same batch (e.g. workos-python 8.3.0, "Added `roles` to organization membership models"). Floor `field-added`/`value-added` at `additive` in `capSeverity`, regardless of the differ's classification: an addition can't break a caller and isn't a fix. This also covers a missing/odd classification, which previously fell through `severityToPrefix` to `fix`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/sdk-release-metadata.spec.mjs | 65 +++++++++++++++++++ scripts/sdk-release-metadata.mjs | 10 +++ 2 files changed, 75 insertions(+) create mode 100644 scripts/__tests__/sdk-release-metadata.spec.mjs diff --git a/scripts/__tests__/sdk-release-metadata.spec.mjs b/scripts/__tests__/sdk-release-metadata.spec.mjs new file mode 100644 index 0000000..c2b169b --- /dev/null +++ b/scripts/__tests__/sdk-release-metadata.spec.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { factsFromDiff } from '../sdk-release-metadata.mjs'; + +// factsFromDiff only reaches indexes.symbolScopes (scope resolution) for the +// kinds under test; an empty index leaves scope unresolved, which is fine — we +// assert on severity/prefix, not scope. +const EMPTY_INDEXES = { symbolScopes: new Map(), enumWireValues: new Map() }; + +const fieldAddedReport = (classification) => ({ + changes: [ + { + kind: 'model-modified', + name: 'OrganizationMembership', + classification: 'additive', + fieldChanges: [{ kind: 'field-added', fieldName: 'roles', classification }], + }, + ], +}); + +const addedFact = (report) => + factsFromDiff(report, EMPTY_INDEXES).find((f) => f.kind === 'field-added'); + +// Regression: a new field is a feature, never a fix. The differ sometimes flags +// an added field `breaking` (reads it as a request-shape tightening); the +// backend-only severity cap used to collapse that to `fix`, so the field landed +// under **Fixes** in the changelog even though the release bumped minor. +test('field-added flagged breaking is capped to additive (feat), not fix', () => { + const fact = addedFact(fieldAddedReport('breaking')); + assert.equal(fact.severity, 'additive'); + assert.equal(fact.prefix, 'feat'); +}); + +test('field-added with an additive classification stays a feature', () => { + const fact = addedFact(fieldAddedReport('additive')); + assert.equal(fact.severity, 'additive'); + assert.equal(fact.prefix, 'feat'); +}); + +// A missing/odd classification would previously fall through severityToPrefix to +// `fix`; an addition is additive regardless. +test('field-added with no classification still resolves to a feature', () => { + const fact = addedFact(fieldAddedReport(undefined)); + assert.equal(fact.severity, 'additive'); + assert.equal(fact.prefix, 'feat'); +}); + +// The cap must NOT leak to altering/removing changes — those stay a fix when the +// differ flags them breaking (field changes are backend-only, never major). +test('field-removed flagged breaking is still capped to fix', () => { + const report = { + changes: [ + { + kind: 'model-modified', + name: 'OrganizationMembership', + classification: 'breaking', + fieldChanges: [{ kind: 'field-removed', fieldName: 'legacy', classification: 'breaking' }], + }, + ], + }; + const fact = factsFromDiff(report, EMPTY_INDEXES).find((f) => f.kind === 'field-removed'); + assert.equal(fact.severity, 'fix'); + assert.equal(fact.prefix, 'fix'); +}); diff --git a/scripts/sdk-release-metadata.mjs b/scripts/sdk-release-metadata.mjs index b48d07f..682c3fa 100644 --- a/scripts/sdk-release-metadata.mjs +++ b/scripts/sdk-release-metadata.mjs @@ -623,7 +623,17 @@ const BACKEND_ONLY_DIFF_KINDS = new Set([ 'response-changed', 'request-body-changed', ]); +// Pure additions can't break a caller and aren't a fix — a new field/value is a +// feature. Floor these at `additive` (→ feat) regardless of what the differ +// classified them as: it sometimes flags an added field `breaking` (reads it as +// a request-shape tightening), which the backend-only cap below would otherwise +// collapse to `fix`, and a missing/odd classification would fall through to +// `fix` via severityToPrefix. Either way a new field would land under **Fixes** +// while the release bumped minor — see model-added/enum-added, hardcoded the +// same way. +const ADDITIVE_DIFF_KINDS = new Set(['field-added', 'value-added']); function capSeverity(kind, severity) { + if (ADDITIVE_DIFF_KINDS.has(kind)) return 'additive'; return BACKEND_ONLY_DIFF_KINDS.has(kind) && severity === 'breaking' ? 'fix' : severity; }