|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/cli": minor |
| 4 | +"create-objectstack": minor |
| 5 | +--- |
| 6 | + |
| 7 | +feat(spec)!: `manifest.id` enforces the reverse-domain rule its registry face already had (#17534) |
| 8 | + |
| 9 | +<!-- adr-0087: registered manifest-id-reverse-domain-required --> |
| 10 | + |
| 11 | +**BREAKING** in the accept-set sense, landing in the launch window as `minor` |
| 12 | +(the repo's convention: `major` is refused by `check-changeset-no-major`, and |
| 13 | +breaking-ness is carried by this banner plus the ADR-0087 disposition): |
| 14 | +`ManifestSchema.id` was `z.string()` and accepted any string. It now enforces |
| 15 | +reverse-domain notation — the same rule `PackageSchema.manifestId` has always |
| 16 | +carried, now declared once and referenced from both sites so the two cannot |
| 17 | +drift again. |
| 18 | + |
| 19 | +Two declarations named one identity and disagreed. The registry enforced the |
| 20 | +shape; the key an author actually writes did not. So a package scaffolded, |
| 21 | +validated, built and booted with an id the publish path would refuse, and the |
| 22 | +author met the rule for the first time at the most expensive possible moment. |
| 23 | + |
| 24 | +FROM → TO, for metadata that used to parse and now fails: |
| 25 | + |
| 26 | +```ts |
| 27 | +// FROM — accepted by defineStack, refused at publish |
| 28 | +defineStack({ manifest: { id: 'my_app', /* … */ } }); |
| 29 | +defineStack({ manifest: { id: 'com.acme.my_app', /* … */ } }); |
| 30 | + |
| 31 | +// TO — dot-separated lowercase segments; hyphens inside a segment, never underscores |
| 32 | +defineStack({ manifest: { id: 'com.example.my-app', /* … */ } }); |
| 33 | +defineStack({ manifest: { id: 'com.acme.my-app', /* … */ } }); |
| 34 | +``` |
| 35 | + |
| 36 | +The refusal carries the repair rather than restating the rule: it names the key, |
| 37 | +echoes the value, shows both documented examples, and — having first checked the |
| 38 | +candidate against the pattern itself — suggests `com.example.blank` for a bare |
| 39 | +word and `com.dogfood.flow-fixture` for a value whose only fault is an |
| 40 | +underscore. A suggestion it cannot verify it does not make. |
| 41 | + |
| 42 | +⚠️ **Changing an id is a republish, not an edit.** An id is an identity: the |
| 43 | +registry addresses a package by `manifest_id`, an installed row is keyed on it |
| 44 | +and a dependent declares it. Before renaming, confirm nothing still addresses |
| 45 | +the old value. That is why this ships as an ADR-0087 **semantic** entry |
| 46 | +(`manifest-id-reverse-domain-required`) with a structured TODO and no automatic |
| 47 | +rewrite — `objectstack migrate meta` will not rename an id for you. |
| 48 | + |
| 49 | +`manifest.namespace` is unchanged and still admits underscores, so the two are |
| 50 | +derived from a project name under different rules and neither is the other. Both |
| 51 | +scaffolders were producing ids the new rule refuses and both now derive a |
| 52 | +conforming one: the bundled `create-objectstack` template ships |
| 53 | +`com.example.blank` and interpolates `com.example.<project-name>` in kebab form, |
| 54 | +and `os init` derives its id from the project name instead of interpolating the |
| 55 | +snake_case namespace (`os init my-app` produced `com.example.my_app`). |
| 56 | + |
| 57 | +## ⚠️ One consent path reverses direction: fail-OPEN → fail-CLOSED |
| 58 | + |
| 59 | +Narrowing `manifest.id` also narrows the **accept set of the artifact load |
| 60 | +path**, and on one route that is a **fail-OPEN → fail-CLOSED reversal on a |
| 61 | +consent/permission path**. Stating it explicitly because a reversal in that |
| 62 | +direction is owed a named direction and a named population, however small the |
| 63 | +population turns out to be. |
| 64 | + |
| 65 | +**What changed.** `AssembledPackageBodySchema` extends `ManifestSchema`, so the |
| 66 | +artifact package entry schema now carries this rule too. An assembled package |
| 67 | +whose `manifest.id` is `''` used to parse: `artifactPackageId` is |
| 68 | +`manifest.id || manifest.name`, so such a package was carried under its `name`, |
| 69 | +while an install-time `grantedPermissions` record keyed by `''` matched no |
| 70 | +carried package and was registered nowhere. The package loaded **with no |
| 71 | +consent record at all** — reported as unbound, warned about, and otherwise |
| 72 | +allowed to run. That is the fail-OPEN half. Such an entry is now refused |
| 73 | +outright (`INVALID_ARTIFACT_PACKAGE_ENTRY`, 422) and the artifact does not |
| 74 | +materialize at all — fail-CLOSED. |
| 75 | + |
| 76 | +**Who is affected: artifacts carrying `manifest.id: ''`, and they were already |
| 77 | +half-broken in both directions.** |
| 78 | + |
| 79 | +- They could never be **published**: the registry face |
| 80 | + (`PackageSchema.manifestId`) has carried this exact pattern all along — the |
| 81 | + same regex literal, now the shared `MANIFEST_ID_PATTERN` — so the publish path |
| 82 | + has always refused them. |
| 83 | +- Their granted-permissions **consent already did not apply**: a record keyed by |
| 84 | + `''` bound to nothing, silently, on every load. |
| 85 | + |
| 86 | +⇒ For that population this converts a silent, already-ineffective consent |
| 87 | +binding into an explicit refusal that names `manifest.id`. Nobody who could |
| 88 | +publish an artifact loses the ability to load it; what they lose is a shape that |
| 89 | +only ever half-worked. |
| 90 | + |
| 91 | +⛔ This is the **artifact package door** refusing a malformed id, **not** the |
| 92 | +permission enforcer acquiring teeth. The install-time granted permission set is |
| 93 | +still registered and not enforced (#17147) — nothing on the tree queries that |
| 94 | +registry, and the repo-wide pin asserting so is unchanged and still green. |
0 commit comments