Repository navigation
Commit d2b188f
fix(cli): os migrate meta converts objects built with ObjectSchema.create and the other strict authoring factories (#20801)
Fixes #20696
Clause-②: no
## What was wrong
`os migrate meta` loads the config through the authored-source shim in
`packages/cli/src/utils/config.ts`. The shim swaps each
`@objectstack/spec` entrypoint for a module that wraps its helpers as
try-real-then-authored, so a retired key reaches the conversion chain
instead of stopping the load. It wrapped only the exports matching
`DEFINE_HELPER_RE` (the `define*` helpers). `ObjectSchema.create(...)`
parses when it is called, inside `defineStack`'s argument, so it threw
before the wrapped `defineStack` ever ran.
Reproduced on `main` at `91e8fa194` before any edit:
- **Repro.** The card's `defineStack({ objects: [ObjectSchema.create({
..., tenancy: { enabled: true, organizationField: 'organization_id' }
})] })`, with `ObjectSchema` imported from `@objectstack/spec/data`. `os
migrate meta --from 17 --json` exits 1 with `{"error":"[\n {\n \"code\":
\"unrecognized_keys\", ...` (the raw `ZodError` array). The tombstone
inside that array says `Run os migrate meta --from 17`.
- **Control.** The same object as a plain literal exits 0: `applied`
holds `object-tenancy-organization-field-removed` at
`objects[0].tenancy.organizationField`, and `schemaValid` is `true`.
## What changed
Everything below is in `packages/cli/src/utils/config.ts`, as the triage
direction asks. `packages/spec` is untouched: `ObjectSchema.create`
stays strict everywhere, and the shim is installed only by `os migrate
meta`.
- **One written list.** `STRICT_AUTHORING_FACTORIES` names the five
factories below as owner export → member, plus the entrypoint each was
measured on.
| factory | home |
|:--|:--|
| `ObjectSchema.create` | `@objectstack/spec/data` |
| `App.create` | `@objectstack/spec/ui` |
| `Dashboard.create` | `@objectstack/spec/ui` |
| `Report.create` | `@objectstack/spec/ui` |
| `Action.create` | `@objectstack/spec/ui` |
The shim wraps an entry on every entrypoint whose owner export carries
that member. Unlisted factories are not wrapped. `DEFINE_HELPER_RE` is
unchanged, and no second pattern is added.
- **How a factory is wrapped.** The wrap happens in place, on a `Proxy`
over the real owner. Only the listed member gets the
try-real-then-authored wrap; every other member (`parse`, `safeParse`,
`shape`, ...) passes through untouched. It has to be a `Proxy` rather
than a copy because `ObjectSchema` is a lazy-schema `Proxy` whose
`ownKeys` trap throws. `Reflect.ownKeys(ObjectSchema)` answers
`TypeError: 'ownKeys' on proxy: trap result did not include
'prototype'`, so `Object.keys(ObjectSchema)` cannot see `create`. For
the same reason, the shim reads the owner by property and never
enumerates it.
- **The refused-artifact line on stderr is rendered.** While the config
loads, the shim prints one stderr line per artifact the current schema
refused. A raw `ZodError` on that line (its `message` is the issues as a
JSON array) is now printed through the project's own `formatZodError`,
as a `ObjectSchema.create validation failed (1 issue):` block with one
`✗ path: message` line per issue. Errors that already carry prose, such
as `defineStack`'s `StackSchemaInvalidError` or `ObjectSchema.create`'s
own unknown-key and `system-data` refusals, keep their message. The
template is shared, so `define*` helpers that throw a raw `ZodError`,
such as `defineAgent`, get the same rendering. This is the same defect
class inside the same generated template: the card's third pin forbids a
raw `ZodError` array, and without this change the fixed repro would
still print one, only on stderr instead of in the exit payload.
## Where the list lives, and how it was measured
**The list lives on the cli side.** It does not have to be exported by
`packages/spec`: the shim is its only reader, and the pin holds it to
the spec surface from `packages/cli`. So there is no `needs_decision`.
Measured at `5f3c0f648` over all 19 JS entrypoints in
`@objectstack/spec`'s `exports` (the two `.json` entries excluded),
reading each export's `create` by property:
- 18 distinct exported values carry a `create` member, under 31 export
names (each identity factory is exported a second time as its
`*Schema`).
- **5 are strict** (throw on a refused input): the five in the list.
Positive control: `ObjectSchema.create` is found at
`@objectstack/spec/data`.
- **13 are identity** (`(config) => config`): `ApiDocumentationConfig`,
`ApiEndpoint`, `ApiTestCollection`, `BatchTask`, `MiddlewareConfig`,
`OpenApiSpec`, `QueueConfig`, `RestApiConfig`, `RestApiPluginConfig`,
`RestApiRouteRegistration`, `RestServerConfig`, `Task`, `WorkerConfig`.
They refuse nothing, so there is nothing to tolerate.
- **Siblings.** An eager-mode (`OS_EAGER_SCHEMAS=1`) sweep of every
function member of every exported value checked the other members:
`Field.*` (32 builders), `SCIM.*`, `RLS.*`, `OData.*`,
`StorageNameMapping.resolveTableName`. None of them validates.
- **Top-level exports.** The only non-`define*` top-level functions
whose own source calls `.parse(` are `connectorFetchOptions`,
`describeHighPrivilegeBits` and `utcInstantMs`. They are runtime
helpers, not authoring factories.
- **Producers.** `git grep` on this tree counts 35
`ObjectSchema.create(` sites and 3 `App.create(` sites under
`examples/`. `Dashboard.create`, `Report.create` and `Action.create`
have 0 sites in `examples/`, but they are wrapped anyway because the
triage asked for every strict factory from one list.
## Pins
`packages/cli/test/migrate-meta-strict-factories.test.ts` is `unit`
tier: in-process over `MigrateMeta.run` and `loadConfig`, against a temp
project that links the real `@objectstack/spec`. It spawns no process
and boots no kernel. It has five cases:
1. **The card's repro migrates.** It exits cleanly, `applied` holds the
tenancy conversion at `objects[0].tenancy.organizationField`, and
`schemaValid` is `true`. The stderr line names `ObjectSchema.create()`
and carries no raw `ZodError` array.
2. **The plain-literal control is unchanged**, and the factory spelling
now produces the same `applied` list as the literal.
3. **An unrelated strict error surfaces as a refusal.** The fixture adds
an unknown field type next to the retired key. The retired key is still
converted and `schemaValid` is `false`. The human report reads `does not
yet pass schema validation — 1 refusal left after the chain` and lists
`✗ objects.0.fields.stage.type: ...`. No stream (stdout or stderr,
`--json` or human) contains a raw `ZodError` array.
4. **Every listed factory is tolerated through the shim, and only
through it.** The fixture is generated from the list, and each refused
call comes back exactly as authored, announced once by name.
`ObjectSchema.safeParse` still works through the `Proxy`. Loading the
same source without `authoredSource` still rejects.
5. **The list stays honest in both directions.** Every entry is live at
its `home` and throws on a refused input. Every `create` that spec
exports and the list does not name returns its argument by identity. A
new strict factory in spec therefore turns this case red and names
itself.
## Reverse verification
I committed the fix first, then ran each mutation with
`scripts/ablation-replace.mjs`. It checks that the anchor hits exactly
once, that the file's hash on disk changes, and that it is restored
afterwards (hash equal to `HEAD`, empty `git diff HEAD`). The subject is
`src` through relative imports, so no `dist` rebuild was involved.
Predicted direction for both: red.
- **A: `ObjectSchema` entry deleted from the list** (at `5f3c0f648`).
Cases 1, 2, 3 and 5 are red; case 4 is green.
- Cases 1 and 3 fail on `expected 1 to be undefined`, meaning exit 1 at
load.
- Case 2 fails on ``no `applied` in the --json payload: {"error":"[\n
{\n \"code\": \"unrecognized_keys\", ...``, which is the original
defect.
- Case 5 fails on ``add a strict `create` to STRICT_AUTHORING_FACTORIES
...``, naming the unlisted strict factory.
- Case 4 staying green is correct: its population is the list itself, so
removing an entry removes it from that case, and case 5 is the one that
catches the removal.
- **B: the ZodError rendering disabled** (`error.name === 'ZodError'`
changed to `'AblatedZodError'`, at `888e0c78d`). Cases 1 and 3 are red
on `expected '[authored-source] ObjectSchema.create…' not to match
/"code":\s*"/`. Cases 2, 4 and 5 are green, as predicted.
## Verification
- **cli unit tier** (`pnpm --filter @objectstack/cli exec vitest run
--project unit --maxWorkers=2`) at `5f3c0f648`: `Test Files 237 passed
(237)`, `Tests 3370 passed (3370)`. The integration tier is declared to
CI: this diff touches no integration-tier file and no spawn or boot
entry point.
- **cli typecheck** (`pnpm --filter @objectstack/cli typecheck`, which
is `tsc --noEmit` plus `check:test-typecheck`) at `5f3c0f648`: exit 0.
The test layer passes (`OK ... 3 file(s) / 28 error(s) / 6 pinned
signature(s) held`, all pre-existing debt), and `tsconfig.test.json
--listFilesOnly` includes the new pin file.
- **`pnpm lint`** (full repo, `eslint . --no-inline-config`) at
`5f3c0f648`: exit 0 in 183s. The last commit, `3d877c6d4`, is a
docblock-only change (it corrects the measured counts). At that head, a
targeted `eslint --no-inline-config --format json` over the two touched
TS files reports both linted with 0 errors and 0 warnings. The changeset
`.md` is outside eslint's population.
- **Gates.** `dispatch-gates --commands` derives 63 families from this
diff: the 56 in the dispatch list, plus 7 that the changeset adds
(`check-adr-0087-registration` ×2, `check-empty-changeset` ×2,
`release-rehearsal-clone --self-test`, `check:objectui-changeset`,
`check:pm-changeset-deadline-census`). All 63 exited 0 at `5f3c0f648`.
`--ran` reconciliation reports: `63 derived, 63 run, 0 NOT-MEASURED, 0
UNRUN` (a derived zero, since every entry recorded an exit code). They
were re-run at the final head `3d877c6d4`; see the report comment on the
card.
- **Branch.** It carries one merge of `origin/main` (`085ca6bc1`). After
the merge I rebuilt `@objectstack/spec` and the six closure packages
that the merge changed, plus `@objectstack/cli`.
## Acceptance notes
- **Checks that only a factory makes are not part of `schemaValid`.**
Some checks run only when a factory is called and are not part of the
stack schema: `ObjectSchema.create`'s refusal of a `managedBy:
'system-data'` object that grants no create, edit or delete, its
`referenceVia` sibling check, and its refusal of an explicit `required:
false` on a `controlled_by_parent` master-detail reference. A source
that trips one of them now migrates with that refusal on the stderr line
and `schemaValid: true`. `os validate` still refuses it (exit 1).
- Measured: `system-data` with every write closed, plus the retired
tenancy key, gives `applied:
[object-tenancy-organization-field-removed]`, `schemaValid: true`, the
refusal prose on stderr, and `os validate` exit 1.
- Precedent: `defineStack` has behaved this way under the existing shim.
Its call-time namespace-prefix check throws
`StackNamespacePrefixInvalidError` while
`ObjectStackDefinitionSchema.safeParse` of the same stack succeeds.
- Reading: `schemaValid` is documented as whether the migrated stack
parses under the installed schema, and that is what it reports. The
changeset states this boundary.
- **Lazy-schema proxies cannot be enumerated.** `Reflect.ownKeys` on a
lazy-schema proxy (`ObjectSchema`, for one) throws, because the trap
omits the target function's non-configurable `prototype`. I found no
public door that reaches it, so this is an observation, not a filed
finding. Carrier: none.
- **Sibling cards.** #20620 (the verdict-first report) has a changeset
already on `main`, and this diff does not touch that report code. For
#20583 (`os lint --json` reads the conversion record), nothing here
changes the record that tool reads. The only premise this moves for
either card is that `os migrate meta` now opens stacks whose objects are
built with a strict factory.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5bed1f6 commit d2b188f
3 files changed
Lines changed: 567 additions & 27 deletions
File tree
- .changeset
- packages/cli
- src/utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
| 261 | + | |
261 | 262 | | |
262 | 263 | | |
263 | 264 | | |
| |||
268 | 269 | | |
269 | 270 | | |
270 | 271 | | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
271 | 275 | | |
272 | 276 | | |
273 | 277 | | |
274 | 278 | | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
283 | 363 | | |
284 | | - | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
285 | 368 | | |
286 | 369 | | |
287 | 370 | | |
288 | | - | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
289 | 380 | | |
290 | | - | |
| 381 | + | |
291 | 382 | | |
292 | 383 | | |
293 | 384 | | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
294 | 427 | | |
295 | 428 | | |
296 | 429 | | |
| |||
317 | 450 | | |
318 | 451 | | |
319 | 452 | | |
320 | | - | |
| 453 | + | |
| 454 | + | |
321 | 455 | | |
322 | 456 | | |
323 | 457 | | |
324 | 458 | | |
325 | 459 | | |
326 | 460 | | |
327 | 461 | | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
328 | 468 | | |
329 | 469 | | |
330 | 470 | | |
| |||
370 | 510 | | |
371 | 511 | | |
372 | 512 | | |
373 | | - | |
| 513 | + | |
374 | 514 | | |
375 | 515 | | |
376 | 516 | | |
| 517 | + | |
377 | 518 | | |
| 519 | + | |
378 | 520 | | |
379 | | - | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
380 | 527 | | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
| 528 | + | |
393 | 529 | | |
394 | 530 | | |
395 | 531 | | |
| |||
0 commit comments