Repository navigation
Commit a9fb83e
fix(core,runtime,plugin-dev,plugin-security): refuse
Fixes #19926
Clause-②: no (narrowing)
Executes ruling `5805260775` (letter A, class-1): `packages: null` on a
release artifact is **malformed and refused**, never read as absent.
`ObjectStackDefinitionSchema.packages` is
`z.array(ArtifactPackageSchema).optional()`, and `.optional()` admits
`undefined`, not `null`. The schema and `composeStacks` (two or more
inputs) already refused `null`; every runtime reader read it as absent.
The readers now follow the declaration. ⛔ The schema does not change,
and no error code is added: `null` gets the envelope `{}`, `0` and `'x'`
already get, `INVALID_ARTIFACT_PACKAGES` / `status: 422`.
## Per reader, before and after (`packages: null`)
| reader | before | after |
|:--|:--|:--|
| `@objectstack/core` `resolveArtifactPackageOrder` | `[artifact]` (the
absent branch) | refused, `INVALID_ARTIFACT_PACKAGES` / 422 |
| `@objectstack/runtime` `resolveArtifactCollections` | the argument, by
identity | refused (the resolver's envelope) |
| `@objectstack/plugin-dev` `stackDeclaresTranslations` (via
`devI18nPluginOptions`) | `false` | refused (the resolver's envelope) |
| `@objectstack/plugin-security` `appSecurityPluginOptions` | read the
top level | refused (the resolver's envelope) |
| `ObjectStackDefinitionSchema` | refused, `invalid_type` | unchanged |
| `composeStacks`, two or more inputs | refused, `STACK_SCHEMA_INVALID`
/ 422 | unchanged |
Controls stay put at every reader: an absent `packages` (no key, or an
explicit `undefined`) is still the single-package branch, by identity;
an array is still read as its entries.
## The ruling's three items, and where each landed
1. **Readers.** `resolveArtifactPackageOrder` drops its `declared ===
null` branch, so its `@throws` / header wording 「present but is not an
array」 is now literally true. The runtime reader's guard and the
plugin-dev guard are spelled exactly as the resolver's absent branch
(`undefined` only), and they moved in the same commit. plugin-security
has no guard of its own; it inherits the refusal, and its docblock no
longer names a `null` absent branch. The core message names the value
`null` instead of `typeof null` (which reads `object`).
2. **Pins.** `null` is refused at the schema (`invalid_type` at
`packages`), at `composeStacks` with two inputs in both positions
(`STACK_SCHEMA_INVALID`, 422, issue path `packages`), and at each reader
(`code` + `status`), each beside its absent and array controls. The
plugin-dev LOCKSTEP pin asks the private guard and
`resolveArtifactPackageOrder` directly and asserts they return the same
envelope on `{ packages: null }` and agree on an absent key. That is the
pin the PR #19924 re-review asked for.
3. **Spec rule text.** The paragraph beside `AssembledPackageBodySchema`
that carved `null` out ("the one value this rule does not settle") now
names `null` as malformed. It is TSDoc only; the schema bytes are
unchanged.
## Pin sweep
`git grep` over every test for `packages` next to `null` found three
pins asserting the old absent reading:
`packages/runtime/src/artifact-collections.test.ts`,
`packages/plugins/plugin-dev/src/dev-i18n-packages-reader.test.ts` and
`packages/plugins/plugin-security/src/app-default-permission-set.test.ts`.
All three are flipped in this PR. Each `null` row now asserts the
refusal's substance (`code` and `status`); none of the flips just
deletes an assertion.
`packages/lint/src/validate-object-references.test.ts` also iterates
`null` over `packages`. It pins the lint reader, which is sibling
#20206's surface and is fenced out of this claim. It is not touched
here.
## Producer census (mechanism hypothesis H3)
No in-repo producer writes `packages: null`. The census covered
`examples/`, fixtures, tests and generated artifacts, with `packages`
followed by `:` and `null`, quoted or bare. The only hits were the three
test pins above and the pending
`.changeset/15293-non-array-packages-refusal.md` sentence below. `os
build` / `os validate` refuse the value at the schema before any reader
runs.
## Clause-② — measured, and it differs from the claim's line
The claim reads `Clause-②: no`. Measured: the accept set of published
exports narrows. `resolveArtifactPackageOrder` (`@objectstack/core` root
export), `devI18nPluginOptions` (`@objectstack/plugin-dev`),
`appSecurityPluginOptions` (`@objectstack/plugin-security`) and
`carriedPackageIds` (`@objectstack/runtime`) all returned an answer for
`{ packages: null }` and now throw. So the line is `no (narrowing)`:
breaking, graded `minor` under the launch-window convention, in this
body and in the changeset. The ADR-0087 disposition is `not-required
(no-migration-prescription)`. Nothing authorable moves, because the
schema already refused the value. This follows the
`.changeset/18239-merge-objects-refusal.md` precedent (a runtime
narrowing on inputs that bypassed the parse).
`check-adr-0087-registration` reads it green.
## Deviations from the claim's file surface (declared, not silent)
- **`packages/spec/src/stack.zod.ts`**: the claim fences this file ("the
schema already refuses `null`"). The edit is ruling item 3's rule text,
and it is **TSDoc only**; the schema is unchanged, which is what the
fence protects. Without it, the one statement of the rule that all four
readers cite would still say the readers treat `null` as absent, and
this PR would make that false.
- **`packages/spec/src/stack-artifact-packages.test.ts`**: ruling item
2's schema and `composeStacks` pins.
- **`packages/core/src/artifact-packages.test.ts`** (new): core had no
in-package test for the resolver. Its broader pins live in
`@objectstack/objectql`'s `artifact-load-path.test.ts`.
- **`.changeset/15293-non-array-packages-refusal.md`**: a DELIBERATE
CORRECTION of a pending release note (next section).
## Pending release note corrected, confirmation requested
`.changeset/15293-non-array-packages-refusal.md` (PR #19924, still
pending) said under "What does not change": "an absent `packages`, and
`packages: null`, still return the caller's own object by identity".
This PR makes the `null` half false in the same release, so the sentence
now reads "an absent `packages` still returns the caller's own object by
identity … `packages: null` is not absent: it is malformed, and it is
refused the same way (#19926)." `check-empty-changeset` refuses this by
design ("DELIBERATE CORRECTION -- ... say so on the PR and get it
confirmed"). **Check Changeset stays red until a person confirms the
correction here**; it is not a required context. `skip-changeset` is not
applied.
## Verification (every reading below is at head `ec9402ad05`)
All runs were serialized behind `scripts/pm/os-verify-lock.sh`, and each
read its `VERDICT command-exit` line.
- **Build.** `pnpm turbo run build
--filter='@objectstack/plugin-dev^...' --concurrency=2` (the closure of
core, runtime, plugin-security, spec and the rest): 34/34 tasks
successful, `VERDICT command-exit 0`.
- **Affected packages, whole suites.** `@objectstack/core` (project
`local`): 55 files, 1426 tests passed. `@objectstack/plugin-security`:
137 files, 2756 passed. `@objectstack/plugin-dev`: 8 files, 82 passed.
`@objectstack/runtime` (project `local`): 279 files, 3910 passed, 1
skipped.
- **Targeted.** `@objectstack/spec`: `stack-artifact-packages`,
`assembled-package-body` and `compose-stacks-concat-shape-refusal`, 3
files, 160 passed. Resolver consumers: objectql `artifact-load-path`
14/14, metadata `plugin-artifact-packages-attribution` 11/11, verify
`artifact-collections` 8/8, cli (`unit`) `stack-collections` 16/16. The
pin sweep found no other test that feeds `packages: null`.
- **Typecheck.** `typecheck` for core, plugin-security, plugin-dev and
runtime: all exit 0, each echoing `tsc --noEmit` and
`check:test-typecheck: OK`. `pnpm --filter @objectstack/spec
check:generated`: all 15 generated artifacts up to date (its
`check:test-typecheck` included), measured against the spec `dist` built
above.
- **Ablation** (one-time proof, no permanent file). The fix was
committed first. Each mutation went through
`scripts/ablation-replace.mjs`: anchor hit 1 to 0, blob moved, and after
the restore the blob equals `HEAD`'s and `git diff HEAD` is empty.
- A. Put `|| declared === null` back into the core resolver's absent
branch (src). Red: core `null` row (1 of 6 failed), runtime `null` row
(1 of 22), plugin-dev `null` row plus LOCKSTEP (2 of 20).
- A through dist. Same mutation, then core rebuilt.
`ablation-dist-preflight` found the marker in 2 built files. Red:
plugin-security `null` row (1 of 28). Restore leg: rebuilt, `--absent`
passed (marker in 0 of 14 built files, tree clean), 28 of 28 green.
- B. Only the plugin-dev guard drifts back to `|| packages === null`.
Red: the `null` row and LOCKSTEP (2 of 20). That is the lockstep pin
catching a guard that disagrees with a fixed resolver.
- C. Only the runtime guard drifts back. Red: the runtime `null` row (1
of 22).
- After the battery: tree clean against `HEAD`, and a
`--reporter=verbose` re-run of all five files lists every new or flipped
case green.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran` reconciles 87 derived, 84 run, 3 NOT
MEASURED, 0 unrun. Of the 84 run, 83 exit 0 and `check-empty-changeset`
exits 1: the deliberate correction above, left red on purpose.
`check-adr-0087-registration` passes: one declared-breaking changeset,
disposition `not-required (no-migration-prescription)`.
`check-changeset-no-major`: no `major` (the level axis reads the PR, so
it is judged in CI).
- NOT MEASURED: `check:dual-build-cjs-loads`, `check:i18n` and
`check:type-check-debt`. Reason: each exited 3, PREREQUISITE NOT MET.
They read a whole-repo build (`dist/` of packages outside this closure),
which CI builds.
- **Lint, narrowed.** `eslint --no-inline-config --format json` over the
10 changed `.ts` files: 10 files linted, 0 errors, 0 warnings.
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, stated in its own header), so this diff cannot
move any untouched file's verdict. The full `pnpm lint` is CI's.
- `origin/main` is 5 commits past this branch's base, and none of them
touches a path in this diff (`git diff --stat` of those paths is empty).
The merge queue validates the merged generation.
## Acceptance notes
- `packages/lint` (`validate-object-references`) still ignores
`packages: null`, like any non-list. Sibling #20206 owns the lint
readers; carrier: #20206.
- The CLI's post-parse readers
(`packages/cli/src/utils/stack-collections.ts` `packageBodies`,
`artifact-packages.ts` `artifactPackages`) test `Array.isArray` and read
every non-array as "no packages". They run after the strict schema parse
has already refused `null`, `{}`, `0` and `'x'`, so `null` never reaches
them through `os build` / `os validate`. Noted, not filed; carrier:
none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>packages: null as malformed, never absent (#20228)1 parent 443b2f4 commit a9fb83e
12 files changed
Lines changed: 236 additions & 41 deletions
File tree
- .changeset
- packages
- core/src
- plugins
- plugin-dev/src
- plugin-security/src
- runtime/src
- spec/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
| 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 | + | |
| 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 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
39 | 41 | | |
40 | 42 | | |
41 | 43 | | |
| |||
194 | 196 | | |
195 | 197 | | |
196 | 198 | | |
197 | | - | |
| 199 | + | |
198 | 200 | | |
199 | 201 | | |
200 | 202 | | |
| |||
205 | 207 | | |
206 | 208 | | |
207 | 209 | | |
208 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
209 | 216 | | |
210 | 217 | | |
211 | 218 | | |
| |||
214 | 221 | | |
215 | 222 | | |
216 | 223 | | |
217 | | - | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
218 | 227 | | |
219 | 228 | | |
220 | 229 | | |
| |||
Lines changed: 41 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
| |||
342 | 343 | | |
343 | 344 | | |
344 | 345 | | |
345 | | - | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
346 | 349 | | |
347 | 350 | | |
348 | 351 | | |
| |||
353 | 356 | | |
354 | 357 | | |
355 | 358 | | |
| 359 | + | |
356 | 360 | | |
357 | 361 | | |
358 | 362 | | |
| |||
370 | 374 | | |
371 | 375 | | |
372 | 376 | | |
373 | | - | |
| 377 | + | |
| 378 | + | |
374 | 379 | | |
375 | | - | |
| 380 | + | |
376 | 381 | | |
377 | 382 | | |
378 | 383 | | |
379 | 384 | | |
380 | 385 | | |
381 | 386 | | |
382 | 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 | + | |
383 | 420 | | |
384 | 421 | | |
385 | 422 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
117 | 119 | | |
118 | 120 | | |
119 | 121 | | |
| |||
163 | 165 | | |
164 | 166 | | |
165 | 167 | | |
166 | | - | |
| 168 | + | |
167 | 169 | | |
168 | 170 | | |
169 | 171 | | |
| |||
179 | 181 | | |
180 | 182 | | |
181 | 183 | | |
182 | | - | |
| 184 | + | |
183 | 185 | | |
184 | 186 | | |
185 | 187 | | |
| |||
Lines changed: 7 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
341 | 341 | | |
342 | 342 | | |
343 | 343 | | |
344 | | - | |
| 344 | + | |
| 345 | + | |
345 | 346 | | |
| 347 | + | |
346 | 348 | | |
347 | 349 | | |
348 | 350 | | |
| |||
359 | 361 | | |
360 | 362 | | |
361 | 363 | | |
362 | | - | |
363 | | - | |
364 | | - | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
365 | 368 | | |
366 | 369 | | |
367 | 370 | | |
| |||
0 commit comments