Skip to content

Commit 866c8cf

Browse files
committed
docs,test: the last literal manifest ids the new pattern refuses move to reverse-domain
`ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, so every remaining literal `manifest.id` in the tree that the pattern refuses is either a fixture that would not survive its own schema or published guidance teaching an author a spelling the runtime rejects. Both are renamed here; nothing about the pattern moves. Fixtures (no assertion reads the id string, so each case keeps its meaning): - packages/lint/src/validate-translation-references.test.ts — the 13 `crm_core` / `crm_service` package ids become `com.example.crm-core` / `com.example.crm-service` (these arrived from `main` through the merge). - packages/create-objectstack/src/rewrite-identity.test.ts — the scaffolded config fixture's `id: 'x'` becomes `com.example.x`, the spelling its own sibling fixture in the same file already used. Published guidance: - packages/spec/prompts/create-new-project.md — `com.example.my_erp` → `com.example.my-erp` (this file ships in the spec tarball). - content/docs/getting-started/your-first-project.mdx — `my-app` → `com.example.my-app`, which is what `create-objectstack` now derives for a project named `my-app`; `namespace: 'my_app'` is untouched (a namespace admits underscores and this key does not). - content/docs/api/declarative-endpoints.mdx, content/docs/protocol/kernel/http-protocol.mdx — `acme-crm` → `com.acme.crm`; `namespace: 'acme'` untouched. - content/docs/api/metadata-api.mdx — `plugin-auth` → `com.objectstack.plugin-auth` on both request bodies and both echoed responses. - packages/services/service-package/README.md — `crm` → `com.example.crm`, including the `get`/`delete` calls that address the same package. - docs/qa/platform-checklist/areas/{api-backend,platform-core}.json — the scratch probe ids the two install-door items POST move to reverse-domain spellings; both items bump `revision` and append the history entry that edit owes. Deliberately NOT renamed, each for a stated reason: - packages/runtime/src/domain-handler-registry.test.ts:591 `pkg-a` — it pins the HTTP install door's residual, which answers 201 to an id every other door now refuses; renaming it would delete the only pin on that gap. - the `FROM` examples in this PR's own changeset — showing the refused spelling is what a FROM → TO migration note is for. - the `manifestId` values in the local install-ledger fixtures (packages/cli/src/commands/doctor-ledger-*.test.ts, packages/cloud-connection/src/*.test.ts) — that is a plain `manifestId: string` field on the ledger entry interface, judged by no schema; it is not `ManifestSchema.id` and not `PackageSchema.manifestId`. - the `Not A Reverse Domain` fixtures in packages/spec — the negative controls of the template-manifest gate and its test. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6783718 commit 866c8cf

10 files changed

Lines changed: 56 additions & 44 deletions

File tree

‎content/docs/api/declarative-endpoints.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { defineStack } from '@objectstack/spec';
4747

4848
export default defineStack({
4949
manifest: {
50-
id: 'acme-crm',
50+
id: 'com.acme.crm',
5151
name: 'Acme CRM',
5252
version: '1.0.0',
5353
type: 'app',

‎content/docs/api/metadata-api.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ List installed packages.
105105
Install a package from its manifest (SDK: `client.packages.install`). Re-installing an
106106
already-installed `id` returns **409 Conflict** unless `overwrite: true`.
107107

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

111111
### `POST /packages/publish`
112112

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

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

119119
### `GET /packages/:id`
120120

‎content/docs/getting-started/your-first-project.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ import * as objects from './src/objects/index.js';
109109

110110
export default defineStack({
111111
manifest: {
112-
id: 'my-app',
112+
id: 'com.example.my-app',
113113
namespace: 'my_app',
114114
version: '0.1.0',
115115
type: 'app',

‎content/docs/protocol/kernel/http-protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,7 @@ import { defineStack } from '@objectstack/spec';
11791179

11801180
export default defineStack({
11811181
manifest: {
1182-
id: 'acme-crm',
1182+
id: 'com.acme.crm',
11831183
name: 'Acme CRM',
11841184
version: '1.0.0',
11851185
type: 'app',

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@
922922
"title": "Package REST lifecycle: POST /packages creates (201), a duplicate name is refused 409 (no silent manifest clobber), PATCH partial-patches the manifest, and an explicit overwrite re-install replaces in place without duplicating",
923923
"since": "v16",
924924
"status": "active",
925-
"revision": 1,
925+
"revision": 2,
926926
"priority": "P2",
927927
"surface": "api",
928928
"personas": [
@@ -931,31 +931,31 @@
931931
"fixtures": {
932932
"app": "showcase",
933933
"requires": [
934-
"a runtime that accepts package install/patch over HTTP (os dev's dispatcher install route, packages.install → POST /api/v1/packages); a scratch package id (e.g. qa_pkg_lifecycle_probe) so no shipped package is mutated"
934+
"a runtime that accepts package install/patch over HTTP (os dev's dispatcher install route, packages.install → POST /api/v1/packages); a scratch package id (e.g. com.example.qa-pkg-lifecycle-probe) so no shipped package is mutated"
935935
],
936936
"knownGaps": [
937937
"if the deployment blocks runtime package install (read-only metadata), record it as a fixture requirement and treat the item blocked(environment) rather than failing the install probes"
938938
]
939939
},
940940
"steps": [
941-
"boot showcase isolated; sign in as admin; choose a scratch id qa_pkg_lifecycle_probe",
942-
"POST /api/v1/packages with { manifest: { id: 'qa_pkg_lifecycle_probe', name: 'QA Probe', version: '1.0.0', scope: 'custom', type: 'app' } }; capture status + body; then GET /api/v1/packages/qa_pkg_lifecycle_probe to confirm it persisted (the dispatcher install lands in BOTH the in-memory registry and durable sys_packages)",
941+
"boot showcase isolated; sign in as admin; choose a scratch id com.example.qa-pkg-lifecycle-probe",
942+
"POST /api/v1/packages with { manifest: { id: 'com.example.qa-pkg-lifecycle-probe', name: 'QA Probe', version: '1.0.0', scope: 'custom', type: 'app' } }; capture status + body; then GET /api/v1/packages/com.example.qa-pkg-lifecycle-probe to confirm it persisted (the dispatcher install lands in BOTH the in-memory registry and durable sys_packages)",
943943
"POST the SAME manifest again with NO overwrite flag; capture status + body",
944-
"PATCH /api/v1/packages/qa_pkg_lifecycle_probe with { name: 'QA Probe Renamed', description: 'edited', version: '1.1.0' }; GET the package back and confirm the patch persisted and that id/scope/type and the lifecycle fields (enabled/status/installedAt) are untouched",
944+
"PATCH /api/v1/packages/com.example.qa-pkg-lifecycle-probe with { name: 'QA Probe Renamed', description: 'edited', version: '1.1.0' }; GET the package back and confirm the patch persisted and that id/scope/type and the lifecycle fields (enabled/status/installedAt) are untouched",
945945
"PATCH three malformed bodies: { name: '' }, { version: 'not-semver' }, and {} (nothing to update); capture each rejection",
946946
"re-install with the explicit opt-in: POST /api/v1/packages?overwrite=true (or body { overwrite: true }) carrying the same id and a changed manifest (version 2.0.0); capture status",
947-
"GET /api/v1/packages and count rows whose id == qa_pkg_lifecycle_probe — must be exactly one",
947+
"GET /api/v1/packages and count rows whose id == com.example.qa-pkg-lifecycle-probe — must be exactly one",
948948
"confirm the LIVE routing seam: POST /api/v1/packages resolves to the dispatcher install route (REST moved marketplace publish OFF the bare path to POST /api/v1/packages/publish in #3610), and PATCH /api/v1/packages/:id answers non-404 though it is absent from packages/rest/src/rest-route-ledger.ts (dispatcher-only)"
949949
],
950950
"acceptance": [
951951
{
952952
"clause": "a scratch package installs → 201: POST /api/v1/packages returns 201 with the created package, and a follow-up GET reads it back (the install lands in the in-memory registry AND durable sys_packages, per packages.ts routing through protocol.installPackage)",
953953
"oracle": "api",
954-
"verify": "POST status 201; GET /api/v1/packages/qa_pkg_lifecycle_probe returns the package",
954+
"verify": "POST status 201; GET /api/v1/packages/com.example.qa-pkg-lifecycle-probe returns the package",
955955
"evidence": "the POST + GET responses"
956956
},
957957
{
958-
"clause": "a duplicate name is refused 409, NEVER silently overwritten: the second POST (no overwrite) answers 409 with message \"Package 'qa_pkg_lifecycle_probe' already exists\" and the bare-409 derived code RESOURCE_CONFLICT (HttpStatusErrorCodeMap 409) — the #2995 data-loss footgun (a silent re-install destroying the existing manifest) is closed",
958+
"clause": "a duplicate name is refused 409, NEVER silently overwritten: the second POST (no overwrite) answers 409 with message \"Package 'com.example.qa-pkg-lifecycle-probe' already exists\" and the bare-409 derived code RESOURCE_CONFLICT (HttpStatusErrorCodeMap 409) — the #2995 data-loss footgun (a silent re-install destroying the existing manifest) is closed",
959959
"oracle": "api",
960960
"verify": "step-3 response: status 409, code RESOURCE_CONFLICT, message names the existing id; the stored manifest is unchanged from step 2",
961961
"evidence": "the 409 response + a re-read proving the manifest survived"
@@ -975,7 +975,7 @@
975975
{
976976
"clause": "the explicit overwrite re-install replaces in place WITHOUT duplicating: POST with overwrite=true (body or query) succeeds and the subsequent GET /api/v1/packages lists exactly ONE row for the id — the overwrite is the deliberate opt-out of the 409 guard, and it never leaves two package rows behind",
977977
"oracle": "api",
978-
"verify": "step-6 status is a success (201/200); step-7 count of qa_pkg_lifecycle_probe rows == 1; the manifest reflects the overwrite (version 2.0.0)",
978+
"verify": "step-6 status is a success (201/200); step-7 count of com.example.qa-pkg-lifecycle-probe rows == 1; the manifest reflects the overwrite (version 2.0.0)",
979979
"evidence": "the overwrite response + the deduped list"
980980
},
981981
{
@@ -1006,6 +1006,12 @@
10061006
"date": "2026-08-08",
10071007
"change": "new — package REST lifecycle (create/409-dup/patch/overwrite-reinstall), grounded in the dispatcher packages domain and the 409 data-loss guard; per PENDING-GAPS §G3",
10081008
"ref": "claude/platform-test-checklist-ocwugl"
1009+
},
1010+
{
1011+
"revision": 2,
1012+
"date": "2026-09-20",
1013+
"change": "the scratch probe id these steps POST is rewritten to a reverse-domain spelling (com.example.qa-pkg-lifecycle-probe) — `ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, so the underscored spellings this item used to name are refused by every door but the HTTP install door, and a checklist that teaches one is the trap the pattern was added to close. Steps, legs and expected verdicts are otherwise unchanged",
1014+
"ref": "#18319"
10091015
}
10101016
]
10111017
},

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,7 @@
12811281
"title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived",
12821282
"since": "v15",
12831283
"status": "active",
1284-
"revision": 1,
1284+
"revision": 2,
12851285
"priority": "P1",
12861286
"surface": "api",
12871287
"personas": [
@@ -1292,7 +1292,7 @@
12921292
"requires": [
12931293
"a runtime that accepts package install over HTTP — os dev's dispatcher install route, POST /api/v1/packages → protocol.installPackage (the same door api-backend.package-rest-lifecycle drives for its 201/409/PATCH contract; this item drives the manifest GATES that run BEFORE the row is written)",
12941294
"the RUNNING runtime's protocol major, read from the server rather than assumed — every engines range below is derived from it as RT. A literal '^11' rots the moment the major turns, and a rotted range makes the compatible and incompatible legs swap places silently",
1295-
"scratch package ids only (qa_manifest_probe_*, com.example.leave, com.example.holiday) so no shipped package is mutated"
1295+
"scratch package ids only (com.example.qa-manifest-probe-*, com.example.leave, com.example.holiday) so no shipped package is mutated"
12961296
],
12971297
"knownGaps": [
12981298
"the DURABLE half of the handshake is NOT scored here: ADR-0087 also refuses an incompatible `sys_packages` row during boot-time rehydration, with boot CONTINUING (v15 release notes). Staging it needs an already-stored incompatible row — DB-level access, or a runtime whose major moved under a previously-installed package. Record blocked(fixture) if attempted. The install-path refusal this item does score is the same assertProtocolCompat call one boundary earlier, so a green here says nothing about the rehydration arm",
@@ -1303,13 +1303,13 @@
13031303
},
13041304
"steps": [
13051305
"boot showcase isolated; sign in as admin. Read the runtime's protocol version from the running server (the boot banner / GET /api/v1/meta), take its leading major as RT, and derive every range below from RT — never from a literal",
1306-
"BASELINE: POST /api/v1/packages with { manifest: { id: 'qa_manifest_probe_a', name: 'Manifest Probe A', version: '1.0.0', scope: 'custom', type: 'app', namespace: 'qa_mfp', engines: { protocol: '^RT' } } }; capture status + body and GET it back — this is the compatible control every refusal below is judged against",
1307-
"INCOMPATIBLE: POST id 'qa_manifest_probe_old' with engines: { protocol: '^(RT-1)' }; capture the refusal IN FULL (status, code, message, rangeSource, targetMajor, migrateCommand). Then GET /api/v1/packages/qa_manifest_probe_old and GET /api/v1/packages and confirm NO row exists for that id",
1308-
"PRECEDENCE, both directions: POST 'qa_manifest_probe_prec_ok' with engines: { protocol: '^RT', platform: '^(RT-1)' } → expect install (protocol is consulted first, so the incompatible platform range is never reached); POST 'qa_manifest_probe_prec_no' with engines: { protocol: '^(RT-1)', platform: '^RT' } → expect refusal whose diagnostic names `engines.protocol` as the range source",
1309-
"LEGACY leg: POST 'qa_manifest_probe_legacy' with NO `engines` and engine: { objectstack: '^(RT-1)' } → expect refusal whose diagnostic names `engine.objectstack` as the source; repeat with '^RT' → expect install. This proves the third precedence rung is wired, not merely typed",
1310-
"GRANDFATHERED: POST 'qa_manifest_probe_norange' with no `engines` and no `engine` at all → expect install, plus the server's `[protocol] package '…' declares no engines.protocol range` warning in the log",
1311-
"UNPARSED: POST 'qa_manifest_probe_wsrange' with engines: { protocol: 'workspace:*' } → expect install, plus the `declares an unrecognized engines.protocol range` warning. This is the direction that would break every workspace-linked package if it inverted",
1312-
"NAMESPACE OWNERSHIP, three legs: (a) POST 'qa_manifest_probe_b' declaring namespace 'qa_mfp' — the one probe A already owns → expect refusal naming the namespace, the current owner and the incoming id; (b) re-POST probe A's OWN manifest unchanged (same id, same namespace) → expect NO conflict (reinstall/HMR is a normal path); (c) POST 'qa_manifest_probe_shared' declaring a shareable platform namespace (base / system / sys) → expect NO conflict (shareable namespaces are exempt)",
1306+
"BASELINE: POST /api/v1/packages with { manifest: { id: 'com.example.qa-manifest-probe-a', name: 'Manifest Probe A', version: '1.0.0', scope: 'custom', type: 'app', namespace: 'qa_mfp', engines: { protocol: '^RT' } } }; capture status + body and GET it back — this is the compatible control every refusal below is judged against",
1307+
"INCOMPATIBLE: POST id 'com.example.qa-manifest-probe-old' with engines: { protocol: '^(RT-1)' }; capture the refusal IN FULL (status, code, message, rangeSource, targetMajor, migrateCommand). Then GET /api/v1/packages/com.example.qa-manifest-probe-old and GET /api/v1/packages and confirm NO row exists for that id",
1308+
"PRECEDENCE, both directions: POST 'com.example.qa-manifest-probe-prec-ok' with engines: { protocol: '^RT', platform: '^(RT-1)' } → expect install (protocol is consulted first, so the incompatible platform range is never reached); POST 'com.example.qa-manifest-probe-prec-no' with engines: { protocol: '^(RT-1)', platform: '^RT' } → expect refusal whose diagnostic names `engines.protocol` as the range source",
1309+
"LEGACY leg: POST 'com.example.qa-manifest-probe-legacy' with NO `engines` and engine: { objectstack: '^(RT-1)' } → expect refusal whose diagnostic names `engine.objectstack` as the source; repeat with '^RT' → expect install. This proves the third precedence rung is wired, not merely typed",
1310+
"GRANDFATHERED: POST 'com.example.qa-manifest-probe-norange' with no `engines` and no `engine` at all → expect install, plus the server's `[protocol] package '…' declares no engines.protocol range` warning in the log",
1311+
"UNPARSED: POST 'com.example.qa-manifest-probe-wsrange' with engines: { protocol: 'workspace:*' } → expect install, plus the `declares an unrecognized engines.protocol range` warning. This is the direction that would break every workspace-linked package if it inverted",
1312+
"NAMESPACE OWNERSHIP, three legs: (a) POST 'com.example.qa-manifest-probe-b' declaring namespace 'qa_mfp' — the one probe A already owns → expect refusal naming the namespace, the current owner and the incoming id; (b) re-POST probe A's OWN manifest unchanged (same id, same namespace) → expect NO conflict (reinstall/HMR is a normal path); (c) POST 'com.example.qa-manifest-probe-shared' declaring a shareable platform namespace (base / system / sys) → expect NO conflict (shareable namespaces are exempt)",
13131313
"NAMESPACE DERIVATION: POST id 'com.example.leave' with NO namespace declared → GET it back and read manifest.namespace; then POST 'com.example.holiday' WITH an explicit namespace 'timeoff' → GET it back and confirm the declared value survived untouched",
13141314
"teardown: DELETE every scratch package created above, or simply discard the isolated file DB — the cheaper path an isolated boot makes free. ⚠️ Several legs here are NOT re-runnable in place (step 8b depends on probe A still being installed, and 8a stops being a conflict once probe A is gone), so a re-run starts from a clean DB rather than from the tail of the previous one"
13151315
],
@@ -1377,6 +1377,12 @@
13771377
"date": "2026-08-24",
13781378
"change": "new — authored to classify the `manifest` capability for the coverage ratchet, which had flagged it UNCLASSIFIED since its liveness ledger landed. Grounding decided the shape: the manifest is an authored, enforced surface (namespace ownership refuses at install, an incompatible engines range refuses before the registry write, a namespace-less runtime package has one derived), not an internal ledger, so it earns items rather than a waiver. This item takes the INSTALL boundary; cli.plugin-manifest-build-contract takes the packaging boundary; api-backend.package-rest-lifecycle already covered the install door's 201/409/PATCH contract and is mapped alongside them. Every range in the steps is derived from the running runtime's major rather than written literally, so the compatible and incompatible legs cannot swap places when the major turns",
13791379
"ref": "#11421"
1380+
},
1381+
{
1382+
"revision": 2,
1383+
"date": "2026-09-20",
1384+
"change": "the scratch probe ids these steps POST are rewritten to reverse-domain spellings (com.example.qa-manifest-probe-*) — `ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, so the underscored spellings this item used to name are refused by every door but the HTTP install door, and a checklist that teaches one is the trap the pattern was added to close. Steps, legs and expected verdicts are otherwise unchanged",
1385+
"ref": "#18319"
13801386
}
13811387
]
13821388
},

‎packages/create-objectstack/src/rewrite-identity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
2828
const writeConfig = (ns: string) =>
2929
fs.writeFileSync(
3030
path.join(dir, 'objectstack.config.ts'),
31-
`export default defineStack({\n manifest: {\n id: 'x',\n namespace: '${ns}',\n },\n});\n`,
31+
`export default defineStack({\n manifest: {\n id: 'com.example.x',\n namespace: '${ns}',\n },\n});\n`,
3232
);
3333

3434
const writeObject = (file: string, name: string) =>

0 commit comments

Comments
 (0)