Repository navigation
Commit 0b4be0b
fix(metadata-protocol)!: the direct metadata save refuses an object without its package namespace prefix (#24702)
Fixes #24547
Clause-②: no (narrowing) (the active save door newly refuses an object
whose name lacks its package's namespace prefix, which today it makes
live; a narrowing owes one `CONTRACT_REVIEW_TIER` review of record
before the queue)
Ruling-ref: 6278955141 (the maintainer: 「同意」 — pull forward into 18.0 as
a refusal). Surface amendment: 6280587888 (the per-item publish door:
same rule, same code, same judge).
## What changes
ADR-0129 D2: a package declares its prefix once (`manifest.namespace`),
and every object it defines is named `namespace_shortName`. The package
draft publish (`publishPackageDrafts`) refused an unprefixed object
draft with `NAMESPACE_PREFIX`. The direct (publish-mode) `saveMetaItem`
and the per-item publish (`publishMetaItem`) made the very same name
`active` under the very same package. Now all three doors ask one judge
and give one answer.
- `packages/metadata-protocol/src/protocol.ts`
- New private `objectNamespacePrefixViolation(name, packageId)`. It
reads the package's declared namespace from the installed manifest and
returns `validateObjectNamespacePrefix`'s own sentence, or `null`. A
package with no namespace, and a write bound to no package, are not
judged.
- `publishPackageDrafts`' pre-flight now calls that judge. This is a
pure refactor: same namespace read, same verdict, same code.
- `saveMetaItem` asks the same judge for an `object` in publish mode. It
is judged against the package the save writes: the one it names, or the
one a package-less publish save resolved into. On a violation it throws
`code: 'NAMESPACE_PREFIX'`, `status: 422`, with the verbatim
prescription.
- Position: after the ADR-0029 D9.9 refusal, and before the
protection/lock gates and the body parse.
- Not asked of draft saves: the door that promotes the draft judges it,
the publish pre-flight batch-atomically and the per-item publish before
its promotion writes.
- Not asked of the server-stated rewrites of stored rows (`source` set:
`migrate-stored`, the flow-credential move).
- The package copies (`package-duplicate`, `package-unmanaged-import`)
are asked, like any author.
- `promoteDraftForPublish`, the per-item publish's promotion, asks the
same judge for an `object` draft on `promoteInto`, the package the
active row is written under. On a violation it throws the same
`NAMESPACE_PREFIX` / 422 with the same sentence, before
`repo.promoteDraft` writes, so nothing is promoted and the draft stays a
draft. The exemptions are the same: no package, no namespace, `sys_*`,
and sealed managed content (as for the D9.9 refusal beside it).
`publishPackageDrafts` also promotes through here, but its pre-flight
refuses the same name first.
- A comment in `duplicatePackage` that called the direct save "a door no
prefix gate reads" is corrected.
- No new error code. `NAMESPACE_PREFIX` is already registered to
`@objectstack/metadata-protocol` in `ERROR_CODE_LEDGER`
(`packages/spec/src/api/error-code-ledger.zod.ts`), and the publish
pre-flight already stamps it. No ADR edit and no `packages/spec` change.
- Changeset: `.changeset/24547-active-save-namespace-prefix.md`. It is
`minor` and **BREAKING**, has FROM / TO / Fix lines covering both doors
(the direct save and the per-item publish), the `Clause-②: no
(narrowing)` line and `adr-0087: not-required
(no-migration-prescription)`. `check:adr-0087-registration` accepts that
disposition.
## Measured on the wire (fresh showcase on this branch, after the fix:
the table at `3b94654c58`; the per-item rows, and a re-read of the
direct PUT and publish-drafts with the same answers, at `afb7f17447`)
| request | answer |
|---|---|
| `POST /api/v1/packages` `com.v1.rollup` | 201 (namespace derived:
`rollup`) |
| `PUT /api/v1/meta/object/v1_report?package=com.v1.rollup` | **422**
`{"error":"Object 'v1_report' is missing the package namespace prefix.
Rename it to 'rollup_v1_report' (namespace =
'rollup').","code":"NAMESPACE_PREFIX"}`; `GET
/api/v1/meta/object/v1_report` then 404 |
| same door, missing `sharingModel` (sibling refusal, for the envelope)
| 422 `{"error":"object/rollup_no_owd failed author-time validation:
…","code":"INVALID_METADATA","issues":[…]}` |
| `PUT …/meta/object/rollup_v1_report?package=com.v1.rollup` (prefixed)
| 200 `state: active` |
| `PUT …/meta/object/v1_loose` (no package) | 200 `state: active` |
| `PUT …/meta/object/v1_report?package=com.v1.rollup&mode=draft` | 200
`state: draft` (unchanged) |
| `POST /api/v1/packages/com.v1.rollup/publish-drafts` | 200 `outcome:
refused`, `failed[0].code: NAMESPACE_PREFIX`, the same sentence
(unchanged) |
| `POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup`
(per-item publish of that draft) | **422** `{"error":"Object 'v1_report'
is missing the package namespace prefix. Rename it to 'rollup_v1_report'
(namespace = 'rollup').","code":"NAMESPACE_PREFIX"}`. Then `GET
/api/v1/meta/object/v1_report` 404 and `POST /api/v1/data/v1_report` 404
`OBJECT_NOT_FOUND`, and a following publish-drafts still lists the draft
in `failed[]` |
| prefixed draft `rollup_v1_report`, then `POST
…/meta/object/rollup_v1_report/publish?package=com.v1.rollup` | 200,
then `GET` 200 |
The `/meta` PUT door answers in the flat REST dialect, so the code is
top-level `code`, beside `error`. That is the same place the door puts
`INVALID_METADATA`.
## Pins
(`packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts`,
8 cases)
1. An unprefixed object under a namespaced package is refused. The pin
asserts `code: NAMESPACE_PREFIX` and `status: 422` (the envelope, not a
bare throw), the first sentence, the rename, and that no row is written.
2. The prefixed name saves `active`.
3. Control: a package that declares no namespace saves the unprefixed
name.
4. Control: a save naming no package saves the unprefixed name.
5. A package-less publish save that resolves into the namespaced package
(its active row is bound there) is refused, and the stored row is
untouched.
6. A draft save is unchanged. `publishPackageDrafts` refuses it with
`NAMESPACE_PREFIX`, and `publishMetaItem` refuses it too.
`failed[0].error` and the per-item refusal's message both equal the
active door's message byte for byte, and the draft is still the only
row.
7. The per-item publish of an unprefixed object draft under a namespaced
package is refused. The pin asserts `code: NAMESPACE_PREFIX` and
`status: 422`, the first sentence and the rename. Nothing is promoted
and the draft stays a draft.
8. Control: the per-item publish promotes a prefixed object draft under
the same package.
Reproduced first, on the unfixed code: cases 1, 5 and 6 red (the active
save answered `saved`), cases 2–4 green.
Ablation (`scripts/ablation-replace.mjs`, on the committed fix): `if
(prefixViolation) {` was changed to `if (false && prefixViolation) {`.
The anchor went 1 → 0 and the blob 72a5cff → 88f5360fcedf. Cases 1,
5 and 6 went red and 2–4 stayed green. Restore was proven: blob == HEAD
and `git diff HEAD` empty. The direction was red, as expected. No dist
build was needed: the suite imports `./protocol.js` from source.
Second ablation, the per-item publish branch, on the committed fix
`afb7f17447`.
- Declared direction, before running: red on exactly cases 6 and 7,
green on the other six.
- `const prefixViolation =
this.objectNamespacePrefixViolation(request.name, promoteInto);` was
changed to `const prefixViolation = false &&
this.objectNamespacePrefixViolation(request.name, promoteInto);`. The
anchor went 1 → 0 and the blob aecdeed → b1797c55d012.
- Result: '2 failed | 6 passed', exactly cases 6 and 7.
- Restore was proven: blob == HEAD and `git diff HEAD` empty.
## Verification on the final head `afb7f17447` (the per-item publish
fix, on top of a merge of `origin/main` `d1173825b7` through
`os-regen-merge.sh`)
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 251
files passed (3 skipped), 20798 tests passed.
- objectql, the whole suite on the fixed dist: 449 files and 8452 tests,
all passed. In round 0, before the fixture change, its only 14 failures
were the three adjusted files, each failing on the new refusal.
- `pnpm --filter @objectstack/metadata-protocol typecheck` and `pnpm
--filter @objectstack/objectql typecheck`: exit 0
(`check:test-typecheck` OK, ledger unchanged).
- Derived gate union (`node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`): 78 commands, all exit 0. `--ran`
reconciliation: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.
`check:dual-build-cjs-loads` and `check:lean-entry-closure` first
answered exit 3 (PREREQUISITE NOT MET: the fresh worktree had no full
build yet). Re-run after the build: exit 0.
- Dogfood, run once on `3b94654c58` (not re-run in this round), on the
files that write objects under a package: `package-first-authoring`,
`flow-shipped-name-published-door` and `install-local-unmanaged-mode`
all passed. `wall-environment-metadata-refusal` had 2 failures,
unrelated to this diff; see Acceptance notes.
- Not run locally, left to CI: `test:repo`, which this diff does not
touch, and the consumer packages' own suites outside the measured set
below.
## File surface beyond the claim (declared)
-
`packages/objectql/src/protocol-duplicate-package-{unprefixed-object,names-in-step,permission-field-keys}.test.ts`.
These ADR-0070 D4 duplicate fixtures staged an unprefixed object under a
namespaced package through the direct save. That is the exact shape this
change refuses, so without this change `Test Core` goes red. Each file
now stages those objects with a `saveStoredBeforeRule` helper, which
hides the package's namespace for the one save and then restores it.
That models a row stored before the door read the rule, so every case
keeps what it pins. No assertion changed.
- `scripts/engine-double-contract.pinned.json`: the
`check:engine-double-contract --write` remedy for the new suite's pinned
double. It adds 3 rows and loses 0.
## Shipped-tree exposure (measured)
- Unprefixed objects saved through the direct door under a namespaced
package:
- `examples/`: 0. Code packages pass `defineStack`, and no test does a
package-bound object save.
- `packages/metadata-protocol` tests: 0. The full suite is green.
- `packages/objectql` tests: 4 staging sites in 3 files, adjusted as
above.
- Controls:
- the runtime integration tests use `com.probe.p` / `com.probe.q`, from
which `deriveNamespaceFromPackageId` derives `null` (a single-letter
segment);
- the dogfood package-bound saves use unregistered bases, or
`crm_`-prefixed names.
## Other doors that make an object live (enumerated; line numbers at
`3b94654c58`)
- `publishMetaItem` (through `promoteDraftForPublish`; `POST
/api/v1/meta/:type/:name/publish`) is now judged too, per the seat's
surface amendment 6280587888 on the card. At `3b94654c58` it still made
the unprefixed object live: 200, then `GET` 200 and a data insert 201.
At `afb7f17447` it answers 422 `NAMESPACE_PREFIX` (table above).
- Covered through `saveMetaItem`: `duplicatePackage` (`:27916`),
`importUnmanagedPackage` (`:28629`), and the datasource external import
(`packages/services/service-datasource/src/plugin.ts:206`, which writes
no package and runs its own prefix check).
- Restore-only doors, which bring back a body that was already live:
`revertCommit` (`:29484`), `rollbackToPackageCommit` (`:29975`) and
`rollbackMetaItem` (`:30046`). `loadMetaFromDb` (`:31403`) hydrates
stored rows at rest.
- `installPackage` registers a manifest and makes no object live.
## Acceptance notes
- ADR-0129 D2 and the `packages/spec/src/kernel/namespace-prefix.ts`
header name `MetadataManager.publishPackage()` as the runtime
chokepoint. That method
(`packages/metadata/src/metadata-manager.ts:1879`) runs no object-name
prefix check. The runtime doors that do are `publishPackageDrafts` and,
with this change, `saveMetaItem`. No edit here, because both files are
outside this card's surface. Carrier: none.
- `importUnmanagedPackage` now meets this refusal for a source package
that declares no namespace and whose objects lack the prefix derived
from its id. The refusal arrives mid-copy and the existing rollback
undoes it, rather than at plan time. Not measured: no fixture has that
shape. Carrier: none.
- Unrelated dogfood red, reproduced on this head:
`packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts`,
the "thirteen write doors" case, 2 failures under `isolated` and
`group`. Its package publish of a `crm_lead.*` view is refused with
`list-view-object-outside-closure`. That is the list-view closure
refusal `origin/main` brought in, and nothing in this diff touches a
view. The Dogfood Regression Gate does not run on PRs, so this surfaces
on `main`'s push battery.
## For the contract-tier reviewer
- The `source !== undefined` exemption (server-stated rewrites) is
deliberate. The two package-copy faces are deliberately judged.
- The refusal reads `request.packageId` after the package-less publish
address resolution. Case 5 pins that.
- The per-item publish branch sits after the D9.9 refusal on
`promoteInto` and before `repo.promoteDraft`, with the same
sealed-managed exemption as that refusal. Case 7 and the second ablation
pin it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d20bf9b commit 0b4be0b
7 files changed
Lines changed: 578 additions & 22 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
- scripts
| 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 | + | |
| 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 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 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 | + | |
0 commit comments