Skip to content

Commit dfd8e39

Browse files
fix(spec): os migrate meta guidance for the ui-* and plugin-* migration entries states each lesson in words, not tracker numbers (stage 2) (#20324)
Part of #20233 Clause-②: no **Stage 2 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on the parent card sets the shape: the lesson in words, and no number, dead or alive. This stage covers the next two families by site count, `ui-` and `plugin-`: **111 sites → 0** in the three prose fields, plus the `surface` field of the two entries that carried an id there (ruling A in the stage-1 ACCEPT, `5858839916`). Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The stage-1 pin is widened to hold `engine-`, `ui-` and `plugin-`. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-1 instrument, re-implemented: a TypeScript-AST walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each `entry` object literal it evaluates the string value of `replacement`, `reason`, `acceptanceCriteria` and (counted separately) `surface`, joining string literals with `+`, then counts `#` followed by 4 or 5 digits at a word boundary. **Tree:** `objectstack-ai/objectstack` at `2aa25efb4e` (this branch's base, the stage-1 merge). Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), the same reading stage 1 took. - **Dark (comment lines):** 734 `//` lines in entry files carry a tracker id, and none is counted — for example `18.client-envelope-convergence-analytics-automation.ts` has 5 such lines and counts 0. Comment lines belong to the sibling card, and ⛔ this PR touches none (the count is 734 before and after). - **Dark (field boundary):** `17.authoring-schemas-strict-unknown-keys.ts` carries one id in `surface`; it counts 0 in the three-field total and 1 in the `surface` column. **Re-measured on the base, matching the stage-1 census:** `ui-` 17 entries, **65** sites (replacement 6 / reason 58 / acceptanceCriteria 1), 42 distinct ids; `plugin-` 11 entries, **46** sites (1 / 39 / 6), 31 distinct ids. `surface`: 1 site each. `engine-`: 0 (stage 1). Whole tree: 267 entries, **950** sites, 9 `surface` sites. **After this PR:** `ui-` 0, `plugin-` 0, `engine-` 0; whole tree **950 → 839** sites and `surface` **9 → 7**. The next family by site count is `driver-` / `kernel-` / `system-` (44 each). | entry | sites (replacement / reason / acceptanceCriteria) | `surface` | |---|---|---:| | `17.plugin-activation-events-retired` | 5 (0 / 4 / 1) | 0 | | `18.plugin-auto-restart-never-reinitialised` | 11 (0 / 7 / 4) | 0 | | `18.plugin-manifest-contributes-dead-members-retired` | 3 (0 / 2 / 1) | 0 | | `18.plugin-manifest-contributes-routes-retired` | 6 (1 / 5 / 0) | 1 | | `18.plugin-manifest-dead-containers-retired` | 3 (0 / 3 / 0) | 0 | | `18.plugin-manifest-kind-globs-retired` | 2 (0 / 2 / 0) | 0 | | `17.plugin-manifest-loading-retired` | 2 (0 / 2 / 0) | 0 | | `17.plugin-runtime-family-retired` | 5 (0 / 5 / 0) | 0 | | `18.plugin-security-scan-result-surface-retired` | 6 (0 / 6 / 0) | 0 | | `18.plugin-security-scanner-retired` | 3 (0 / 3 / 0) | 0 | | `18.ui-cloud-connection-widgets-unknown-keys-refused` | 3 (0 / 3 / 0) | 0 | | `18.ui-form-field-length-malformed-refused` | 8 (2 / 6 / 0) | 0 | | `18.ui-form-field-precision-scale-integer-refused` | 4 (1 / 3 / 0) | 0 | | `18.ui-form-view-predicate-features-root-refused` | 2 (0 / 2 / 0) | 0 | | `17.ui-interaction-config-family-retired` | 7 (1 / 6 / 0) | 0 | | `18.ui-list-view-groupbyfield-padded-refused` | 1 (0 / 1 / 0) | 0 | | `18.ui-list-view-grouping-field-padded-refused` | 2 (0 / 2 / 0) | 0 | | `18.ui-mcp-connect-agent-unknown-keys-refused` | 5 (0 / 5 / 0) | 0 | | `17.ui-notification-action-embed-config-retired` | 8 (0 / 8 / 0) | 0 | | `18.ui-object-grid-page-size-positive-integer-refused` | 4 (0 / 4 / 0) | 0 | | `18.ui-react-list-view-binding-aliases-retired` | 2 (0 / 2 / 0) | 1 | | `18.ui-record-blocks-unknown-keys-refused` | 3 (0 / 3 / 0) | 0 | | `18.ui-reference-rail-unknown-keys-refused` | 2 (0 / 2 / 0) | 0 | | `17.ui-widget-i18n-family-retired` | 14 (2 / 11 / 1) | 0 | | four entries with no site: `plugin-version-semver-2-0-0`, `ui-action-undoable-unfulfillable-refused`, `ui-bulk-action-param-unknown-keys-refused`, `ui-report-joined-container-selection-refused` | 0 | 0 | | **total, 28 entries** | **111 (7 / 97 / 7)** | **2** | ## Every citation read, and what the text now says I read each cited issue or PR myself with single-card REST reads: the body, and the comments where a ruling or a measurement lives. Ids are in code spans so this body posts no cross-references. `objectui#N` ids were read from `objectstack-ai/objectui`; bare ids from this repository. | cited | what it decided (read) | how the text now carries it | |---|---|---| | `#3733` | The pruned `cached` field key: the parse succeeded and the removed key was dropped without a word; the orphan schema was deleted. | "an earlier field-key prune measured exactly that — the parse succeeded and the removed key was dropped without a word" | | `#3950` | Removed the plugin sandboxing / integrity / approval config nothing read: an exported schema with no consumer is read as a capability. | "the lesson of the unwired plugin sandboxing / integrity / approval config …: an exported schema with no consumer is read as a capability", and the plugin-runtime "earlier removal of this module's discovery/sandbox config island" | | `#4001` | Maintainer, 2026-08-03: every authorable surface refuses an unknown key (strict), in the v17 window, measured file by file for an authoring door. | "the component-props unknown-key gate (an authorable surface refuses a key it does not declare …)"; "the v17 unknown-key strictness sweep (its ui/ batch 14)"; "the batch of the v17 unknown-key strictness sweep that measured this file as having no authoring door" | | `#4115` | Ruling A: an objectui symbol named like a spec export must import it, or take a name of its own (or an allowlist row), enforced by a CI guard. | "renamed off the spec's names under objectui's rule that a symbol named like a spec export must import it or take a name of its own" | | `#4484` | `findStream` removed with no tombstone: a TS/API surface nothing parses, so tsc at the call site carries the ban. | "`contracts.IDataDriver.findStream` (removed with no tombstone, because nothing parses a driver object)" | | `#4535` | The dual-source cleanup: 52 names declared twice across entry points, taken to 0. | "the dual-source cleanup removed the `./ui` copies …" | | `#4583` | The datasource capability flags were dead; `readOnly` was precisely validated and inert, and the CRM example called a datasource a read replica while writes went through. The strictness ledger records the "more convincing lie" lesson there. | "(the lesson of the datasource capability flags: `readOnly` was precisely validated and read by nothing, while a shipped example called a datasource a read replica and wrote through it)" | | `#4610` | Removed the `./ui` `Notification` / `NotificationConfig` copies (zero import sites measured); the `./api` inbox row stayed live. | "the dual-source cleanup removed the `./ui` copies of `NotificationSchema` / `NotificationConfigSchema` (the same names declared differently on other entry points)" | | `#4653` | Maintainer ruling A, 2026-08-02: converge `activationEvents` on the kernel's structured `{ type, pattern }` shape, re-exported from studio. | "once the kernel and studio copies had converged on the kernel's structured `{ type, pattern }` shape" | | `#4657` | Retire both `activationEvents` keys (REMOVE, v17 window): kernel tombstone, studio strict refusal, the orphan schema deleted. | "Both keys took ADR-0049's REMOVE answer, not ENFORCE, while protocol 17 was still unreleased"; its id sentence in plugin-runtime is covered by the entry id `plugin-activation-events-retired` | | `#4834` | Maintainer, 2026-08-03: REMOVE the rest of the plugin-runtime family; hot loading returns with its implementation, if ever. | "The maintainer's ruling of 2026-08-03 is that decision, answered REMOVE: …"; "the maintainer's REMOVE ruling on the rest of the plugin-runtime family"; `plugin-runtime-family-retired` by id elsewhere | | `#4875` | The health-check timeout guard is cleared when the race settles, and deliberately not `unref`'d (an unref'd guard can swallow the timeout). | "its guard timer (kept ref'd while the race is undecided, cleared the moment it settles)" | | `#4910` | Inbound rate limiting was built from a new seam: a `server:` key that carries only the keys its executor consumes. | "the way inbound rate limiting came back, as a new key carrying only what its executor consumes" | | `#4914` | Maintainer, 2026-08-04: REMOVE `manifest.loading`, with a hard precondition of a clean cloud and objectui bare-name sweep. | "the maintainer ruled REMOVE on 2026-08-04, on condition that a bare-name sweep of cloud and objectui came back clean first" | | `#4938` | Maintainer, 2026-08-04: retire `HttpServerConfig`'s seven unreachable keys with their container. | "the `HttpServerConfig` retirement (seven keys no runtime read and no authoring door reached, retired with their container)" | | `#4988` | Maintainer, 2026-08-04: retire the five ui interaction files; touch, dnd, keyboard and motion are renderer built-in behaviour, offline belongs to a sync engine. | "The 2026-08-04 ruling retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in behaviour and offline belongs to a sync engine …"; `ui-interaction-config-family-retired` by id in the widget entry | | `#5015` | REMOVE `NotificationActionSchema` / `EmbedConfigSchema`, 2026-08-04: a no-door dead surface retires implementation-first, as three same-shape rulings that week had decided. | "left the disposition to ADR-0049's enforce-or-remove, which came back REMOVE on 2026-08-04: a dead surface with no authoring door retires implementation-first, …" | | `#5021` | Maintainer, 2026-08-04: retire all nine unconsumed theme token groups; theme-driven typography is not near-term. | "the theme-token retirement (theme-driven typography is not a near-term capability, so nine token groups nothing consumed were retired)" | | `#5040` | Build the declarative ApiEndpoint executor in 17.x; the v17 loud refusal of a non-empty `apis:` becomes execution. | "live since protocol 17, once the declarative endpoint executor was built and the loud refusal of a non-empty `apis:` became execution" | | `#5055` | Maintainer, 2026-08-06: retire the doorless widget and i18n shapes (8 of 9 widget sites; `FieldWidgetProps` kept). | already stated by the entry ("The 2026-08-06 ruling weighed …"); the trailing id is dropped | | `#5068` | Maintainer, 2026-08-05, direction A: parse a component's `properties` against its `ComponentPropsMap` row by `type`, at publish and lint; a type with no row is skipped because the type union is open. | "the props gate's dispatch (it parses `properties` against the type's row at publish and lint time, and skips a type with no row because the type union is open)" | | `#5781` | Correction: objectui did re-export the `./ui` notification names; the removal stands. | "falsified for objectui, which re-exported both names, … the removal itself stands" | | `#6011` | Maintainer: close the `ctx.user` `roles` alias now, no window. | "`actor-user-roles-to-positions` (the `ctx.user` `roles` alias, closed at once on the maintainer's word rather than given a window)" | | `#7751` | Maintainer, 2026-08-12, direction A: the `object-*` blocks get `ComponentPropsMap` rows, key sets from renderer read points. | "the shape it was given when the `object-*` blocks first got `ComponentPropsMap` rows measured from their read points" | | `#8321` | `Field.scale` / `precision` refuse non-integer and negative values (`int().min(0)`). | "(that surface tightened first, to a non-negative integer)"; "after that surface converged on `z.number().int().min(0)`" | | `#8691` | A strict `record:reference_rail` row, key set from the renderer's read points. | "the class already closed for `record:reference_rail` …"; "after the rail was given its strict row" | | `#8744` | Strict rows for `record:alert` / `record:quick_actions` / `record:history`. | "… and then for `record:alert` / `record:quick_actions` / `record:history`, each by declaring a strict `ComponentPropsMap` row" | | `#11168` | The kind-registration log names the declared `id`; the `kind` bucket is reachable through `GET /metadata/:type`; `globs` had zero readers. | "Measured by the engine-lane fix that made kind registration log its declared `id` (which also found the `kind` bucket itself reachable …)" | | `#11169` | Maintainer, 2026-08-24 (「接受你的建议。」): remove `globs` through the full ADR-0049 ceremony. | "maintainer ruling 2026-08-24 (「接受你的建议。」) …: remove, through the full ADR-0049 ceremony" | | `#11327` | The doc-correction half of the routes ruling (2026-08-22, 「接受所有」, Option B): four author-facing sites redirected to the imperative `http.server` mount. | "the author-facing corrections landed FIRST (the skill's decision table, the dispatcher protocol doc, ADR-0088:40 and app.mdx, each redirected to the imperative mount)"; the ruling sentence states Option B's content | | `#11566` | Maintainer, 2026-08-24: `maxLength` → `z.number().int().min(1)`. | "`maxLength` by the maintainer's 2026-08-24 ruling"; "tightened first, to a positive integer, by maintainer rulings" | | `#11575` | Strict, empty rows for `cloud-connection:panel` / `marketplace:installed-list`. | "the strict, empty `cloud-connection:panel` / `marketplace:installed-list` rows closed the previous two" | | `#11825` | Maintainer, 2026-08-25: retire the declarative `AdvancedPluginLifecycleConfig` container; the classes stay a host-driven library. | "which is why the maintainer retired its declarative config container on 2026-08-25 and kept the classes as a host-driven library"; "The maintainer's 2026-08-25 keep of the host-driven library still stands" | | `#11852` | Both failure routes (returned, thrown or timed out) funnel into one failure step: one counter, one threshold comparison. | "the two failure routes — a returned failure and a thrown or timed-out check — sharing one failure counter and one threshold comparison" | | `#11949` | Maintainer, 2026-08-25, option B: `minLength` → `int().min(1)`, zero refused. | "`minLength` by the 2026-08-25 one, which refused zero too" | | `#11955` | `successThreshold` binds from every status that records a failure. | "The fix that made `successThreshold` bind from every status that records a failure made that MORE convincing" | | `#12174` | The form-field row keys are live, so they were shape-tightened in place on the object-field templates. | "The form-field row still carried the object field's old shape … The row keys are LIVE … The schema now refuses …" (unchanged tail) | | `#12269` | Closure A: `packages/mcp` gets its own canonical-envelope gate. | "door 3 of the canonical-envelope gate `@objectstack/mcp` was given for its shipped page" | | `#12340` | Maintainer, 2026-08-26: retire the `'disk'` / `'distributed'` state strategies (silent memory fallbacks) and `distributedConfig`; "a vocabulary of nothing is not a vocabulary". | "the `'disk'` / `'distributed'` state strategies that fell back to memory in silence"; "(ruled 2026-08-26: a vocabulary of nothing is not a vocabulary)" | | `#12400` | The cloud leg for `capabilities` / `configuration` / `extensions` measured clean at cloud `15f55df`. | "dispatched once the cloud half of the census below came back clean" (the census clause names `15f55df`) | | `#12428` | Refuse/retire: `startWatching` throws instead of logging success; `watchPatterns` is tombstoned because a key leaving a surviving def has no route-3 exit. | "the file-watching placeholder whose `startWatching` logged success while watching nothing"; "for the reason the file-watching retirement recorded" | | `#12665` | Implement the maintainer's 2026-08-27 option B: a form view may not name `features.*` in a predicate; refused at authoring. | "Ruled by the maintainer on 2026-08-27 (option B — vocabulary narrowing: a form view may not name `features.*` in a predicate, and the authoring door refuses it loudly)" | | `#14791` | Maintainer, 2026-09-07: retire the `objectName` / `viewType` aliases now, no window. | "(2026-09-07)" in the sentence that states the retirement | | `#15513` | Maintainer, 2026-09-05: retire the incident-response, training and change-management families whole; not roadmapped. | "retired whole, with the training and change-management families (maintainer ruling 2026-09-05: not roadmapped, so retired rather than marked experimental …)" | | `#15930` | Retired `PluginSecurityScanner`; no replacement, repair refused. | "the scanner retirement recorded as plugin-security-scanner-retired. That retirement removed PluginSecurityScanner …" | | `#15932` | Maintainer, 2026-09-07 (「同意」): retire the scan-result family and `securityScan`. | "maintainer ruling 2026-09-07 (adopted verbatim 「同意」): retire the scan-result family and its securityScan sibling, because once the scanner was gone nothing so much as imported their types" | | `#16526` | Maintainer, 2026-09-07, option A: cloud does not re-host the consumer-less control-plane files; they are deleted. | "(ruled 2026-09-07: cloud does not re-host the control-plane files it never consumed)" | | `#17360` | Ruling C: refuse a padded grouping field name at the producer (not a trim). | "Ruled by the maintainer on 2026-09-10 (「其他同意」): refuse at the producer." | | `#17499` | Refuse a padded `groupByField` on kanban, gantt and timeline. | "The same padded-name defect the grouping-level narrowing … refused, on the axis that one scoped out by name, and given the same refusal." | | `#19046` | Bound the grid component arm's page sizes to positive integers. | the sentence now opens "This door still carried the shape …"; the declaration half is stated in the entry | | `objectui#3161` | Batch 7/8 of the objectui burn-down under the `#4115` rule (renames). | folded into the `#4115` sentence | | `objectui#3169` | objectui stopped declaring symbols under names the spec owns; its rename tripwire fails both ways. | "the rename tripwire objectui added when it stopped declaring symbols under names the spec owns" | | `objectui#3289` (PR) | `@object-ui/fields`' validation slot renamed onto the spec's `error` and connected to its producer. | "an objectui fix of 2026-08-03, made to follow the spec, renamed …" | | `objectui#5595` | The console FormPage honours the form's own `maxLength` override. | "(fixed so that a form's own bound wins over the object's, as its docstring promised)" | | `objectui#5898` | The form-view bridge maps every spec key or explains why not. | "mapField, which maps every spec key or explains why it does not" | | `objectui#6262` | The measured `features.*` asymmetry, and the ruling record. | folded into the `#12665` sentence | | `objectui#7347` | The measured padded-grouping failure, and ruling C's record. | folded into the `#17360` sentence | | `objectui#9853` | Measured `pagination.pageSize: 0` reaching `ObjectGrid`. | "an objectui grid measurement found that …" | | `objectui#9896` (PR) | A non-positive `pageSize` is refused at all three grid read points. | "objectui's grid plugin repaired the consumer half — it now refuses a non-positive page size at all three read points …" | The eight dead ids, and the two ids whose page does not say what the text claimed (`#2561`, `#3896`), are in **Acceptance notes**. No call-shaped token moves: a `name(` census over `registry.ts` is identical before and after, so textual call-spelling ratchets read the same. ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened The stage-1 pin now selects every entry whose id starts with a covered prefix: `engine-`, `ui-` or `plugin-`. It spawns the real CLI (`os migrate meta --from 16 --to 18`) once, locates each covered block **verbatim** in stdout, and asserts the printed block carries no `#` plus 4 or 5 digits. That block includes `surface`. Anti-vacuity: - the derived set must contain all 29 rewritten entries (5 `engine-`, 10 `plugin-`, 14 `ui-`), and every covered prefix must select at least one entry; - presence in stdout is asserted before cleanliness; - the detector is exercised on both sides first (lit on 4 and 5 digits, dark on 3, 6 and `ADR-0112`). The chain reports every semantic entry of every crossed hop, whatever the stack authors (`applyMetaMigrations` maps `step.semantic` straight to TODOs), so the fixture is kept as-is and the header now says so. The file keeps its stage-1 name; a rename is left to the stage that covers the last family. An entry added later to a covered family is held on arrival — see Acceptance notes for the one known in-flight case. ## Ablation — the widened pin can fail on a `ui-` block and on `surface` From committed state, HEAD `8a1076b0a6`, with `scripts/ablation-replace.mjs` in wrap mode and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated (stage 1's attempt 2 records why the entry file is the wrong target). - **Mutation.** In `registry.ts`, the `surface` of `ui-react-list-view-binding-aliases-retired`: anchor `(the react-tier overlay aliases published as deprecated` → `(the react-tier overlay aliases #11284 published as deprecated`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `ab26922f` → `9a200491`. - **Mutate leg.** Spec build under the lock: command-exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `ui-react-list-view-binding-aliases-retired: the printed guidance cites a tracker id: expected '#11284' to be undefined`. - **Restore.** Tool-proven: blob `ab26922f` == HEAD, `git diff HEAD` empty. - **Restore leg.** Spec build under the lock: command-exit 0. The `--absent` preflight found the marker in none of 222 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. ## Verification Final head **`071dc7fc1d`** unless a line says otherwise. - **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own pre-existing `skipIf`). - **Spec tests that read these entries or the registry:** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/migrations` plus the 14 other spec test files that read `MIGRATIONS_BY_MAJOR` or one of these entries' text (`plugin-runtime-tier-truthful-text`, `interaction-config-retirement`, `widget-i18n-retirement`, …) and `scripts/build-schemas-check-mode.test.ts`: `Test Files 15 passed`, `Tests 438 passed`. - ⚠️ The first `src/migrations` run, at `8a1076b0a6`, went **red, 2 failed**: `migrations.test.ts` pinned the two tracker numbers in `ui-notification-action-embed-config-retired`'s `reason`. The second commit re-pins the same guard on the sentences that now carry the lesson (see Acceptance notes). - **CLI unit:** `src/utils/spec-release-changes.test.ts` 6 passed; `test/vitest-tiers-partition.test.ts` 22 passed (at `8a1076b0a6`; no CLI file changed after it). - **Call-spelling census that reads `registry.ts`:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-query-signature.test.ts` gives 15 passed. - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0. `pnpm --filter @objectstack/cli typecheck` exits 0 (at `8a1076b0a6`), and its test layer holds the recorded 3 files / 28 errors, unchanged. - **Build:** `pnpm exec turbo run build --filter="@objectstack/cli^..." --concurrency=2` gives 55/55 (at `8a1076b0a6`); `@objectstack/spec` rebuilt at the final head, command-exit 0. - **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives **88** families at `071dc7fc1d` (the same set as at `8a1076b0a6`). `--ran` over the recorded exit codes reads **88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:generated` (15 artifacts current), `check:doc-authoring`, `check:issue-citations`, `check:nul-bytes`, `check:adr-0087-registration`, `check:changeset-no-major` and `check:dual-build-cjs-loads` (104 require entry points across 66 packages load). - Union discipline: the first pass started before the second commit, so the 21 families that started before that edit were re-run at the final head, and the 6 that refused on a spec `dist` stamp made stale by that edit (5 × exit 3, `check:generated` exit 1 naming `api-surface` stale by stamp, `check:dual-build-cjs-loads` exit 3) were re-run after rebuilding spec at the final head. The reconciled list takes each family's latest run. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 27 changed `.ts` files reports 27 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 27 are in it (no file-ignored warning). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** a driver-free bare clone's `merge-tree --write-tree` of this head against `origin/main` `7b1e4a4871` (six commits past the base, one of them adding 25 semantic entries) exits 0 with no conflict. The census over that merged tree reads `engine-` 0, `plugin-` 0 and `ui-` 0 across 18 entries: main's new `ui-report-joined-chart-retired` carries tracker ids only in comment lines. ## Acceptance notes - **Dead ids, rewritten from the code on `main`.** Eight cited numbers answer 404 on both the issues and the pulls endpoint, re-probed with a 200 control (`#11327`): `#10627`, `#10724`, `#10726`, `#10812`, `#11284`, `#11328`, `#11332`, `#14919`. Each sentence was rewritten from what `main` records, and anything it could not confirm was dropped: - `#10627` / `#10724` / `#10726` / `#10812` / `#11328` (contributes routes and dead members): `packages/spec/src/kernel/manifest.zod.ts` carries all ten `retiredKey()` tombstones and the census comment; `packages/objectql/src/engine.ts` (`manifest.contributes?.kinds`) is re-measured as the only non-spec read; `plugin-rest-api.zod.ts` and `metadata-plugin.zod.ts` point at the imperative `http.server` mount. Dropped: "triage graded 2026-08-21" and the 2026-08-24 date on the routes entry's cloud sentence (the cloud sha `5b5925a` is kept: `#12400`'s body corroborates it). The routes ruling's 「接受所有」 and Option B are kept: `#11327`'s body records them. - `#11332` (dead containers): the three `manifest.capabilities` / `configuration` / `extensions` tombstones on `main`. Dropped: "triage graded 2026-08-23". - `#11284` (react-tier convergence): `packages/spec/src/ui/react-blocks.ts` records the 2026-08-23 maintainer ruling that the react tier converges on the metadata-tier vocabulary, deprecating first. - `#14919` (scanner): `packages/core/src/security/index.ts`'s tombstone and `security-scanner-retirement.pin.test.ts` record the 2026-09-05 ruling, the removed class and types, and repair refused. Dropped: "ruled A: retire in three surfaces" and the batch numbers, which `main` does not state. - **A bare id that names the wrong card.** The widget / interaction entries' "(`#2561`)" resolves here to an unrelated security-lifecycle umbrella. The claim ("objectui holds TYPE re-exports … never validators, and says so") is objectui's own decision on `objectui#2561`: keep the `@objectstack/spec/ui` re-exports type-only. It was rewritten from objectui's `packages/types/src/__tests__/p2-spec-exports.test.ts` at objectui `main`, which records that decision. - **`#3896`, cited as "follow-up" and "close-out".** `#3896` itself is the sharing-rule `criteria` REST bypass and says neither. The "follow-up" is PR `#3950` (its title says so); the "close-out" is the inert-key sweep recorded on `main` in `docs/protocol-upgrade-guide.md` and the strictness ledger. Both sentences now say what those decided. - **Cross-repo ids.** Ten sites are spelled `objectui#N` (nine) or "objectui PR " plus a number (one). The stage-1 census counted them by number with the rest; they were read from `objectstack-ai/objectui` (all 200), not from this repository, where the same numbers name unrelated cards. - **One bare-number spelling went too.** The scan-result entry spelled a deleted card as "issue" plus its number, twice, without `#`. The instrument cannot see it, but it is a tracker number shown to the author, and it sat in a rewritten sentence. - **In-flight entry the widened pin will hold.** Open PR #20262 adds `18.ui-form-layout-inline-grid-retired.ts` with one tracker id in its entry text (read from the PR's file list: one `#` plus five digits). It is not on `main`, so it is untouched here. Once this lands, `ui-` is covered: #20262 must rewrite that site before it lands, or the pin goes red on its merge ref. - **Comment lines are untouched.** `18.ui-list-view-groupbyfield-padded-refused.ts` keeps its `//` comment citing a number; comment and docblock lines are the sibling card's surface. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stage 1; their `--check` legs are green. - **One file beyond the claim's surface: `packages/spec/src/migrations/migrations.test.ts`.** Its guard on `ui-notification-action-embed-config-retired` (the entry must keep explaining its orphaning and name the objectui correction) matched the two tracker numbers by regex, so the rewrite turned it red. The same two assertions now match the sentences that carry the lesson (`dual-source cleanup removed the ./ui copies`, `objectui, which re-exported both names`); the negative assertion beside them is unchanged. No other test pins a covered entry's text. A `git grep` of test files for the 24 ids finds four besides the pin and this one: three (`plugin-runtime-tier-truthful-text`, `interaction-config-retirement`, `widget-i18n-retirement`) were run above and pass, and the fourth (`packages/core`'s `granted-permissions-not-enforced.pin.test.ts`) names the loading entry's file only inside a failure message. ## Line budget Entry files: **333 changed lines** (+210 / −123) across 24 files, against the stage-1 ≈400 budget. The whole diff is **830 lines** (+533 / −297) in 30 files. Of the rest, `registry.ts` is 333, the two projections are 52 (`spec-changes.json` 32, the upgrade guide 20), the widened pin is 85, `migrations.test.ts` is 6 and the changeset is 21. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eea8787 commit dfd8e39

30 files changed

Lines changed: 533 additions & 297 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `ui-*` and `plugin-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `ui-*` family (component props rows, form-field
10+
and list-view refusals, the react-tier `ListView` aliases, and the retired
11+
interaction, notification, embed, widget and i18n vocabularies) and of the `plugin-*`
12+
family (the plugin manifest, runtime, health-monitor and security-scanner retirements)
13+
are printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
14+
change. Their text sent the reader to issue-tracker numbers — some of which no longer
15+
resolve — for what a ruling, measurement or fix had decided; it now says what was
16+
decided, in the sentence being read. The same holds for the two `surface` headers that
17+
carried a number. ADR ids are kept.
18+
19+
Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the
20+
chain rewrites exactly what it rewrote before. The generated migration registry,
21+
`spec-changes.json` and the protocol upgrade guide carry the same text.

‎docs/protocol-upgrade-guide.md‎

Lines changed: 10 additions & 10 deletions
Large diffs are not rendered by default.

‎packages/cli/test/migrate-meta-engine-guidance.test.ts‎

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

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

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

71-
/** The entries rewritten when the family was brought to this line — the anti-vacuity floor. */
77+
/**
78+
* The entries rewritten when each family was brought to this line — the
79+
* anti-vacuity floor. A covered entry that carried no tracker id to begin with
80+
* is held by its prefix and needs no row here.
81+
*/
7282
const REWRITTEN = [
7383
'engine-dotted-filter-refused',
7484
'engine-dotted-projection-refused',
7585
'engine-find-formula-filter-refused',
7686
'engine-find-formula-order-by-refused',
7787
'engine-update-upsert-retired',
88+
'plugin-activation-events-retired',
89+
'plugin-auto-restart-never-reinitialised',
90+
'plugin-manifest-contributes-dead-members-retired',
91+
'plugin-manifest-contributes-routes-retired',
92+
'plugin-manifest-dead-containers-retired',
93+
'plugin-manifest-kind-globs-retired',
94+
'plugin-manifest-loading-retired',
95+
'plugin-runtime-family-retired',
96+
'plugin-security-scan-result-surface-retired',
97+
'plugin-security-scanner-retired',
98+
'ui-cloud-connection-widgets-unknown-keys-refused',
99+
'ui-form-field-length-malformed-refused',
100+
'ui-form-field-precision-scale-integer-refused',
101+
'ui-form-view-predicate-features-root-refused',
102+
'ui-interaction-config-family-retired',
103+
'ui-list-view-groupbyfield-padded-refused',
104+
'ui-list-view-grouping-field-padded-refused',
105+
'ui-mcp-connect-agent-unknown-keys-refused',
106+
'ui-notification-action-embed-config-retired',
107+
'ui-object-grid-page-size-positive-integer-refused',
108+
'ui-react-list-view-binding-aliases-retired',
109+
'ui-record-blocks-unknown-keys-refused',
110+
'ui-reference-rail-unknown-keys-refused',
111+
'ui-widget-i18n-family-retired',
78112
];
79113

80114
interface FamilyEntry {
@@ -88,7 +122,7 @@ interface FamilyEntry {
88122

89123
const FAMILY: FamilyEntry[] = Object.entries(MIGRATIONS_BY_MAJOR).flatMap(([major, step]) =>
90124
step.semantic
91-
.filter((s) => s.id.startsWith(FAMILY_PREFIX))
125+
.filter((s) => COVERED_PREFIXES.some((prefix) => s.id.startsWith(prefix)))
92126
.map((s) => ({ ...s, toMajor: Number(major) })),
93127
);
94128

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

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

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

156-
it('selects the whole family, including every entry the rewrite covered', () => {
192+
it('selects every covered family, including every entry the rewrites covered', () => {
157193
const ids = FAMILY.map((e) => e.id);
194+
for (const prefix of COVERED_PREFIXES) {
195+
expect(ids.some((id) => id.startsWith(prefix)), `no entry selected for ${prefix}`).toBe(true);
196+
}
158197
for (const id of REWRITTEN) expect(ids, `family lost ${id}`).toContain(id);
159198
});
160199

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

0 commit comments

Comments
 (0)