Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f1993f8
wip(spec): declare the reverse-domain manifest id rule once
claude Sep 15, 2026
a41581e
feat(spec)!: manifest.id enforces the reverse-domain rule, declared once
claude Sep 15, 2026
46af720
chore(spec): regenerate api-surface, export-origins and reference docs
claude Sep 15, 2026
6464e7f
test(create-objectstack): anchor the spec kernel subpath to source fo…
claude Sep 15, 2026
c864c6b
test(spec): give the stack fixtures reverse-domain manifest ids
claude Sep 15, 2026
d0d859e
fix(cli): golden corpus and fixtures carry reverse-domain manifest ids
claude Sep 15, 2026
61d26d0
test(qa): give the dogfood and downstream-contract fixtures reverse-d…
claude Sep 15, 2026
f983362
test(qa): rename the last dogfood fixture id, completing the 21
claude Sep 15, 2026
1275a13
test: give the remaining non-conforming manifest ids reverse-domain s…
claude Sep 16, 2026
9ea5eba
test: rename the single-occurrence refused manifest ids outside packa…
claude Sep 16, 2026
ea52fd6
test: rename the coupled manifest ids with their in-file references
claude Sep 16, 2026
58acd78
Merge remote-tracking branch 'origin/main' into claude/issue-17534-ma…
claude Sep 16, 2026
e9d4431
chore(spec): regenerate api-surface and export-origins on the merged …
claude Sep 16, 2026
4f27751
test(runtime): the `''` manifest-id pins move to DOOR 1, and the chan…
claude Sep 16, 2026
ceb042b
Merge remote-tracking branch 'origin/main' into claude/issue-17534-ma…
claude Sep 20, 2026
7030cf6
chore(spec): regenerate api-surface, export-origins and reference doc…
claude Sep 20, 2026
94a4675
docs(adr): ADR-0130 D4 records the entry door order and the fail-CLOS…
claude Sep 20, 2026
0fc32b4
test(spec): two package-api pins move to the post-narrowing reality
claude Sep 20, 2026
6783718
test(cli): the #18490 package-id pins move to the post-narrowing reality
claude Sep 20, 2026
866c8cf
docs,test: the last literal manifest ids the new pattern refuses move…
claude Sep 20, 2026
1d4c99b
test: the constant-carried manifest id the new pattern refuses moves …
claude Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .changeset/17534-manifest-id-reverse-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
"@objectstack/spec": minor
"@objectstack/cli": minor
"create-objectstack": minor
---

feat(spec)!: `manifest.id` enforces the reverse-domain rule its registry face already had (#17534)

<!-- adr-0087: registered manifest-id-reverse-domain-required -->

**BREAKING** in the accept-set sense, landing in the launch window as `minor`
(the repo's convention: `major` is refused by `check-changeset-no-major`, and
breaking-ness is carried by this banner plus the ADR-0087 disposition):
`ManifestSchema.id` was `z.string()` and accepted any string. It now enforces
reverse-domain notation — the same rule `PackageSchema.manifestId` has always
carried, now declared once and referenced from both sites so the two cannot
drift again.

Two declarations named one identity and disagreed. The registry enforced the
shape; the key an author actually writes did not. So a package scaffolded,
validated, built and booted with an id the publish path would refuse, and the
author met the rule for the first time at the most expensive possible moment.

FROM → TO, for metadata that used to parse and now fails:

```ts
// FROM — accepted by defineStack, refused at publish
defineStack({ manifest: { id: 'my_app', /* … */ } });
defineStack({ manifest: { id: 'com.acme.my_app', /* … */ } });

// TO — dot-separated lowercase segments; hyphens inside a segment, never underscores
defineStack({ manifest: { id: 'com.example.my-app', /* … */ } });
defineStack({ manifest: { id: 'com.acme.my-app', /* … */ } });
```

The refusal carries the repair rather than restating the rule: it names the key,
echoes the value, shows both documented examples, and — having first checked the
candidate against the pattern itself — suggests `com.example.blank` for a bare
word and `com.dogfood.flow-fixture` for a value whose only fault is an
underscore. A suggestion it cannot verify it does not make.

⚠️ **Changing an id is a republish, not an edit.** An id is an identity: the
registry addresses a package by `manifest_id`, an installed row is keyed on it
and a dependent declares it. Before renaming, confirm nothing still addresses
the old value. That is why this ships as an ADR-0087 **semantic** entry
(`manifest-id-reverse-domain-required`) with a structured TODO and no automatic
rewrite — `objectstack migrate meta` will not rename an id for you.

`manifest.namespace` is unchanged and still admits underscores, so the two are
derived from a project name under different rules and neither is the other. Both
scaffolders were producing ids the new rule refuses and both now derive a
conforming one: the bundled `create-objectstack` template ships
`com.example.blank` and interpolates `com.example.<project-name>` in kebab form,
and `os init` derives its id from the project name instead of interpolating the
snake_case namespace (`os init my-app` produced `com.example.my_app`).

## ⚠️ One consent path reverses direction: fail-OPEN → fail-CLOSED

Narrowing `manifest.id` also narrows the **accept set of the artifact load
path**, and on one route that is a **fail-OPEN → fail-CLOSED reversal on a
consent/permission path**. Stating it explicitly because a reversal in that
direction is owed a named direction and a named population, however small the
population turns out to be.

**What changed.** `AssembledPackageBodySchema` extends `ManifestSchema`, so the
artifact package entry schema now carries this rule too. An assembled package
whose `manifest.id` is `''` used to parse: `artifactPackageId` is
`manifest.id || manifest.name`, so such a package was carried under its `name`,
while an install-time `grantedPermissions` record keyed by `''` matched no
carried package and was registered nowhere. The package loaded **with no
consent record at all** — reported as unbound, warned about, and otherwise
allowed to run. That is the fail-OPEN half. Such an entry is now refused
outright (`INVALID_ARTIFACT_PACKAGE_ENTRY`, 422) and the artifact does not
materialize at all — fail-CLOSED.

**Who is affected: artifacts carrying `manifest.id: ''`, and they were already
half-broken in both directions.**

- They could never be **published**: the registry face
(`PackageSchema.manifestId`) has carried this exact pattern all along — the
same regex literal, now the shared `MANIFEST_ID_PATTERN` — so the publish path
has always refused them.
- Their granted-permissions **consent already did not apply**: a record keyed by
`''` bound to nothing, silently, on every load.

⇒ For that population this converts a silent, already-ineffective consent
binding into an explicit refusal that names `manifest.id`. Nobody who could
publish an artifact loses the ability to load it; what they lose is a shape that
only ever half-worked.

⛔ This is the **artifact package door** refusing a malformed id, **not** the
permission enforcer acquiring teeth. The install-time granted permission set is
still registered and not enforced (#17147) — nothing on the tree queries that
registry, and the repo-wide pin asserting so is unchanged and still green.
2 changes: 1 addition & 1 deletion content/docs/api/declarative-endpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { defineStack } from '@objectstack/spec';

export default defineStack({
manifest: {
id: 'acme-crm',
id: 'com.acme.crm',
name: 'Acme CRM',
version: '1.0.0',
type: 'app',
Expand Down
8 changes: 4 additions & 4 deletions content/docs/api/metadata-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ List installed packages.
Install a package from its manifest (SDK: `client.packages.install`). Re-installing an
already-installed `id` returns **409 Conflict** unless `overwrite: true`.

**Body**: `{ manifest: { id: "plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, settings?: { ... }, enableOnInstall?: true, overwrite?: false }`
**Response**: `{ package: { id: "plugin-auth", version: "1.0.0", ... }, message?: "..." }`
**Body**: `{ manifest: { id: "com.objectstack.plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, settings?: { ... }, enableOnInstall?: true, overwrite?: false }`
**Response**: `{ package: { id: "com.objectstack.plugin-auth", version: "1.0.0", ... }, message?: "..." }`

### `POST /packages/publish`

Publish a package (manifest + metadata) to the package marketplace registry. This is
publisher tooling, not part of the app SDK surface.

**Body**: `{ manifest: { id: "plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, metadata: { objects: [...], views: [...], ... } }`
**Response**: `{ success: true, message: "...", package: { id: "plugin-auth", version: "1.0.0" } }`
**Body**: `{ manifest: { id: "com.objectstack.plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, metadata: { objects: [...], views: [...], ... } }`
**Response**: `{ success: true, message: "...", package: { id: "com.objectstack.plugin-auth", version: "1.0.0" } }`

### `GET /packages/:id`

Expand Down
2 changes: 1 addition & 1 deletion content/docs/getting-started/your-first-project.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import * as objects from './src/objects/index.js';

export default defineStack({
manifest: {
id: 'my-app',
id: 'com.example.my-app',
namespace: 'my_app',
version: '0.1.0',
type: 'app',
Expand Down
2 changes: 1 addition & 1 deletion content/docs/protocol/kernel/http-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,7 @@ import { defineStack } from '@objectstack/spec';

export default defineStack({
manifest: {
id: 'acme-crm',
id: 'com.acme.crm',
name: 'Acme CRM',
version: '1.0.0',
type: 'app',
Expand Down
16 changes: 8 additions & 8 deletions content/docs/references/api/package-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Installed package row whose manifest is the assembled package body

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -257,7 +257,7 @@ Installed package with runtime lifecycle state

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -320,7 +320,7 @@ Installed package row whose manifest is the assembled package body

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -504,7 +504,7 @@ Install package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -548,7 +548,7 @@ Install package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -667,7 +667,7 @@ Install package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -800,7 +800,7 @@ Upgrade package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -895,7 +895,7 @@ Resolve dependencies request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ Install package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const result = ManifestSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/kernel/package-registry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Install package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -287,7 +287,7 @@ Installed package with runtime lifecycle state

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/kernel/package-upgrade.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Upgrade package request

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down Expand Up @@ -285,7 +285,7 @@ Pre-upgrade state snapshot for rollback capability

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique package identifier (reverse domain style) |
| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
| **version** | `string` | ✅ | Package version (semantic versioning) |
Expand Down
Loading
Loading