Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/20233-ui-plugin-migration-guidance-tracker-free.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/spec': patch
---

fix(spec): `os migrate meta` guidance for the `ui-*` and `plugin-*` migration entries states each lesson in words instead of citing tracker numbers

Clause-②: no

The ADR-0087 semantic entries of the `ui-*` family (component props rows, form-field
and list-view refusals, the react-tier `ListView` aliases, and the retired
interaction, notification, embed, widget and i18n vocabularies) and of the `plugin-*`
family (the plugin manifest, runtime, health-monitor and security-scanner retirements)
are printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
change. Their text sent the reader to issue-tracker numbers — some of which no longer
resolve — for what a ruling, measurement or fix had decided; it now says what was
decided, in the sentence being read. The same holds for the two `surface` headers that
carried a number. ADR ids are kept.

Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the
chain rewrites exactly what it rewrote before. The generated migration registry,
`spec-changes.json` and the protocol upgrade guide carry the same text.
20 changes: 10 additions & 10 deletions docs/protocol-upgrade-guide.md

Large diffs are not rendered by default.

85 changes: 62 additions & 23 deletions packages/cli/test/migrate-meta-engine-guidance.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* `os migrate meta` — the guidance it prints for the `engine-*` ADR-0087
* semantic entries states each lesson in words and carries no tracker number.
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`) states each lesson
* in words and carries no tracker number.
*
* ## What this pins
*
Expand All @@ -12,21 +13,26 @@
* author is shown, so it carries no tracker number: a number sends the reader
* to a page that can be deleted (some cited pages already had been), and the
* lesson the entry exists to teach then sits behind a dead link instead of in
* the sentence being read. The `engine-*` entries were rewritten to say what
* each cited ruling, measurement or fix decided; ADR ids stay, because an ADR
* lives in this repository.
* the sentence being read. The covered families were rewritten, one staged
* family at a time, to say what each cited ruling, measurement or fix decided;
* ADR ids stay, because an ADR lives in this repository. The whole printed
* block is held, so `surface` is held as well as the three prose fields.
*
* The fixture authors the shapes those entries are about — a lookup and a
* virtual `formula` field on one object — and the CLI replays the chain from
* the support floor to the highest major carrying an `engine-*` entry. Each
* family block is then located VERBATIM in what the terminal printed, and that
* printed block must hold no `#` followed by four or five digits.
* The chain reports every semantic entry of every hop it crosses, whatever the
* stack authors, so the fixture only has to be a real stack the command loads;
* it keeps the lookup and the virtual `formula` field the `engine-*` entries
* are about. The CLI replays the chain from the support floor to the highest
* major carrying a covered entry. Each covered block is then located VERBATIM
* in what the terminal printed, and that printed block must hold no `#`
* followed by four or five digits. The file keeps the name it was given when
* `engine-*` was the only covered family.
*
* ## Why it cannot pass by reading nothing
*
* - The family is derived from the registry by id prefix, so an `engine-*`
* entry added later is held to the same line on arrival — and the derived set
* must still contain the five entries this rewrite covered, so an emptied
* - The covered set is derived from the registry by id prefix, so an entry
* added later to a covered family is held to the same line on arrival — and
* the derived set must still contain every entry the rewrites covered, and
* every covered prefix must still select at least one entry, so an emptied
* prefix cannot turn every assertion below into a loop over nothing.
* - Each block is asserted PRESENT in stdout before it is asserted clean, so a
* renderer change that stopped printing the prose fails here instead of
Expand Down Expand Up @@ -65,16 +71,44 @@ const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
/** A tracker id as author-shown prose must not carry it: `#` and four or five digits. */
const TRACKER_ID = /#\d{4,5}\b/;

/** The family this pin holds, selected by entry-id prefix. */
const FAMILY_PREFIX = 'engine-';
/** The families this pin holds, selected by entry-id prefix. */
const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-'];

/** The entries rewritten when the family was brought to this line — the anti-vacuity floor. */
/**
* The entries rewritten when each family was brought to this line — the
* anti-vacuity floor. A covered entry that carried no tracker id to begin with
* is held by its prefix and needs no row here.
*/
const REWRITTEN = [
'engine-dotted-filter-refused',
'engine-dotted-projection-refused',
'engine-find-formula-filter-refused',
'engine-find-formula-order-by-refused',
'engine-update-upsert-retired',
'plugin-activation-events-retired',
'plugin-auto-restart-never-reinitialised',
'plugin-manifest-contributes-dead-members-retired',
'plugin-manifest-contributes-routes-retired',
'plugin-manifest-dead-containers-retired',
'plugin-manifest-kind-globs-retired',
'plugin-manifest-loading-retired',
'plugin-runtime-family-retired',
'plugin-security-scan-result-surface-retired',
'plugin-security-scanner-retired',
'ui-cloud-connection-widgets-unknown-keys-refused',
'ui-form-field-length-malformed-refused',
'ui-form-field-precision-scale-integer-refused',
'ui-form-view-predicate-features-root-refused',
'ui-interaction-config-family-retired',
'ui-list-view-groupbyfield-padded-refused',
'ui-list-view-grouping-field-padded-refused',
'ui-mcp-connect-agent-unknown-keys-refused',
'ui-notification-action-embed-config-retired',
'ui-object-grid-page-size-positive-integer-refused',
'ui-react-list-view-binding-aliases-retired',
'ui-record-blocks-unknown-keys-refused',
'ui-reference-rail-unknown-keys-refused',
'ui-widget-i18n-family-retired',
];

interface FamilyEntry {
Expand All @@ -88,7 +122,7 @@ interface FamilyEntry {

const FAMILY: FamilyEntry[] = Object.entries(MIGRATIONS_BY_MAJOR).flatMap(([major, step]) =>
step.semantic
.filter((s) => s.id.startsWith(FAMILY_PREFIX))
.filter((s) => COVERED_PREFIXES.some((prefix) => s.id.startsWith(prefix)))
.map((s) => ({ ...s, toMajor: Number(major) })),
);

Expand All @@ -102,9 +136,11 @@ function printedBlock(e: FamilyEntry): string {
}

/**
* A stack authoring the shapes the family's entries are about: a relation a
* dotted path would follow, and a virtual `formula` field no driver
* materialises a column for.
* A real stack for the command to load. It keeps the shapes the `engine-*`
* entries are about — a relation a dotted path would follow, and a virtual
* `formula` field no driver materialises a column for — though which blocks
* print does not depend on it: every semantic entry of a crossed hop is
* reported.
*/
const FAMILY_FIXTURE = `
export default {
Expand Down Expand Up @@ -144,7 +180,7 @@ afterAll(() => {
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
});

describe('os migrate meta — the engine-* guidance carries no tracker number', () => {
describe('os migrate meta — the guidance of the covered families carries no tracker number', () => {
it('the detector fires on a tracker id and stays dark on every other number shape', () => {
expect(TRACKER_ID.test(`see #${'9'.repeat(4)}`)).toBe(true);
expect(TRACKER_ID.test(`see #${'9'.repeat(5)}`)).toBe(true);
Expand All @@ -153,12 +189,15 @@ describe('os migrate meta — the engine-* guidance carries no tracker number',
expect(TRACKER_ID.test('ADR-0112')).toBe(false);
});

it('selects the whole family, including every entry the rewrite covered', () => {
it('selects every covered family, including every entry the rewrites covered', () => {
const ids = FAMILY.map((e) => e.id);
for (const prefix of COVERED_PREFIXES) {
expect(ids.some((id) => id.startsWith(prefix)), `no entry selected for ${prefix}`).toBe(true);
}
for (const id of REWRITTEN) expect(ids, `family lost ${id}`).toContain(id);
});

it('prints every family block verbatim, and no printed block names a tracker id', () => {
it('prints every covered block verbatim, and no printed block names a tracker id', () => {
for (const e of FAMILY) {
const block = printedBlock(e);
expect(stdout.includes(block), `${e.id}: its block is not in the printed output`).toBe(true);
Expand Down
Loading
Loading