You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f25f6a4
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): `objectstack serve` mounts the always-on `package-registry` capability, so a package created through the API survives a restart on a stock boot (#19387)
7
+
8
+
Clause-②: no
9
+
10
+
`package-registry` has been on the always-on slate (`PLATFORM_ALWAYS_ON_CAPABILITIES`) since the `marketplace` / `package-registry` split, and `serve` appended it to every app's `requires`. But `Serve.CAPABILITY_PROVIDERS` did not key it, and the resolver's no-provider branch says nothing about a token the app did not declare itself. So an app that did not declare `requires: ['marketplace']` got no `package` service. `POST /api/v1/packages` answered `201`, printed `no 'package' service — '…' registered in-memory only (will not survive a restart)`, and `GET /api/v1/packages/:id` answered `404` after a restart.
11
+
12
+
-**`package-registry` now mounts `PackageServicePlugin`** from `@objectstack/service-package`, the provider the spec's `PLATFORM_CAPABILITY_PROVIDERS` row declares for it. A stock boot creates `sys_packages` and replays it at start, so installs and manifest edits made through the API persist.
13
+
-**Apps that declare `marketplace` boot as before, with one `PackageServicePlugin`.**`marketplace` resolves to the same provider. The capability resolver now remembers the providers it has mounted itself, so the always-on token does not mount a second copy. Without that change, a declarer's boot would print `Plugin superseded: 'package-service'`.
14
+
-**A stock database gains one table, `sys_packages`.**`PackageServicePlugin` creates it with raw DDL, as it already did for `marketplace` declarers. On the in-memory driver (`memory://`), which has no raw SQL, the boot now logs that the DDL was not run and that package hydration was skipped. Packages there last only as long as the process, as before.
15
+
-`--preset minimal` still opts out of the whole slate. `protocol.installPackage` keeps its in-memory-only branch as the documented degraded path for hosts that mount no provider.
16
+
-**`@objectstack/metadata-protocol`: the `installPackage` docblock no longer says the runtime half is missing.** It used to say that a stock boot still took the in-memory-only branch. It now says that `objectstack serve` mounts `PackageServicePlugin` for `package-registry`, so a stock boot persists, and that the in-memory-only branch is for hosts that mount no provider. The docblock ships in `dist`. No behaviour changes.
**Clause-②: yes (widening)** — a new member (`type: 'doc'`) on the published, strict navigation-item union, and a new exported schema (`DocNavItemSchema`), so the accept set an app author writes against grows. Nothing previously admitted is refused: the `docs/nav-target` build rule judges only `doc` items, which no stack could carry before. Contract-review tier.
7
+
8
+
A documentation entry on the app menu: the new `type: 'doc'` navigation item (`DocNavItemSchema`, ADR-0046) targets a `book` and/or a `doc`, and at least one is required.
9
+
10
+
```ts
11
+
{ id: 'nav_help', type: 'doc', label: 'Help Centre', book: 'crm_manual' } // opens the book
{ id: 'nav_both', type: 'doc', book: 'crm_manual', doc: 'crm_lead_guide' } // that page, in that book
14
+
```
15
+
16
+
-**`book` alone** opens the book at its first readable page with the book sidebar. Membership is derived by the book's group rules, so a doc added later that matches a rule appears under the entry with no navigation edit. The package id also names a book — the package's implicit book.
17
+
-**`doc` alone** opens that page; its book context is the doc's own book, else the package's implicit book. `doc` is a doc NAME (the source filename stem, lowercase snake_case): `crm_lead_guide.md` or `docs/crm_lead_guide` is refused.
18
+
-**Neither** is refused when the app is parsed, with a message naming both keys. The rule also reaches the published JSON Schema (`json-schema/ui/DocNavItem.json`) as an `anyOf` of `required`, so a validator reading the schema refuses the same shape.
19
+
-**Audience**: the entry has no gate of its own — it inherits the docs audience gate. A `book` entry shows the member only the pages they may read, and is not shown to a member who may read none; a `doc` entry the member may not read is not shown. `visible` / `requiredPermissions` can only narrow that further.
20
+
-**`os build` / `os validate` / `os lint`** refuse a `doc` entry whose `book` or `doc` names nothing in the package (new rule `docs/nav-target`, with a did-you-mean). This runs in the docs step because that is where docs from `src/docs/*.md` join the artifact. It checks app `navigation`, `areas[].navigation` and `manifest.navigationContributions`.
21
+
- Near-misses are answered: `docName` → `doc`, `bookName` → `book`, and `book` / `doc` written on another item type points at `type: 'doc'`.
22
+
23
+
The console renders the new entry in a later objectui release; until then a `doc` item parses and publishes, but the menu does not show it.
**BREAKING for runtime-authored `translation` items** — the registered `translation` metadata type no longer declares `settings`: platform settings copy is platform-only at BOTH application doors (#19620)
7
+
8
+
Clause-②: no
9
+
10
+
`TranslationItemSchema` — one `translation` metadata item, authored with
11
+
`defineTranslation`, in Studio, or through the metadata API — now takes the same
12
+
ten groups as a per-app bundle entry (`TranslationData`). `settings`, and its
13
+
singular `setting`, are refused by name with the platform-only prescription,
14
+
exactly as the per-app bundle has refused them since #15178. The file door and
15
+
the item door are two authoring surfaces for one app metadata type, so they
16
+
accept one shape.
17
+
18
+
### Migration — FROM → TO
19
+
20
+
| You wrote | Write instead |
21
+
| --- | --- |
22
+
|`defineTranslation({ locale: 'zh-CN', settings: { mail: { title: '邮件投递' } } })`| delete the `settings` group — there is no application-side replacement key |
23
+
| a `translation` item saved through the metadata API or Studio carrying `settings`| delete the `settings` group; the save answers `422 INVALID_METADATA` until you do |
24
+
|`const t: TranslationItem = { locale: 'en', settings: … }`| move the copy to the PLATFORM bundle (`PlatformTranslationData`), or delete it |
25
+
26
+
**The one-line fix: delete the `settings` group from the item.** Settings copy is
27
+
not application-authorable — `settings` is keyed by `SettingsManifest.namespace`
28
+
and only platform code declares a manifest. `settingsCommon` is **not** affected:
29
+
the Settings UI shell strings (the source badges, under
30
+
`settingsCommon.sourceLabels`) stay on both application faces.
31
+
Run `os migrate meta --from 17` to list the mechanical edits for existing
32
+
sources; apply them by hand.
33
+
34
+
### Rows already stored are converted, not refused
35
+
36
+
A `translation` row saved before this change keeps loading. The runtime
`sys_metadata` itself and used to merge the RAW stored payload; it now replays
39
+
the ADR-0087 conversion chain over each row before merging it, the same policy
40
+
as every other stored-metadata read seam. `translation-per-app-settings-removed`
41
+
has learned the item shape, so a stored row's `settings` is dropped there, the
42
+
rest of the item (`objects`, `apps`, …) still loads, and the server logs one
43
+
warning per row naming the row, the group and the conversion. Run
44
+
`os migrate meta --stored --apply` to persist the canonical rows.
45
+
46
+
### What changes on screen, which is not nothing
47
+
48
+
On the item door the group was STRONGER than on the bundle door. A published
49
+
item is loaded into the runtime-authored layer, which both i18n adapters read
50
+
**over** the shipped bundles — so an item's `settings` overrode the platform's
51
+
own Settings copy for its locale, rather than only filling gaps. After
52
+
upgrading, re-read the Settings screens in each locale such an item covered:
53
+
where it overrode a platform string, **the platform's string renders again**;
54
+
where it filled a gap the platform bundle leaves, the **manifest's own literal
55
+
renders, which is English**. If a platform string is wrong or missing for your
56
+
locale, correct it in the platform bundle (`@objectstack/service-settings`'s
57
+
`settingsBuiltinTranslations`).
58
+
59
+
No deprecation window: the item door refuses the key by name from this major.
60
+
61
+
### Unchanged
62
+
63
+
The platform face — `PlatformTranslationDataSchema`, `settingsBuiltinTranslations`,
64
+
and `GET /api/v1/i18n/translations/:locale`, whose served document is the merged
65
+
tree — still declares `settings`. The liveness ledger's `translation.settings`
66
+
row is deleted because the key left the ITEM's shape; the platform capability it
67
+
evidenced is untouched.
68
+
69
+
Ruling batch #210 item 2 letter B (2026-09-22) — maintainer 「210 同意」.
70
+
71
+
<!-- adr-0087: not-required (already-registered translation-per-app-settings-removed, translation-per-app-settings-platform-only) both entries already existed for the per-app bundle door; this change EXTENDS them to the translation item door in the same unreleased major — the D2 conversion learns the bare item shape and the D3 semantic entry covers both doors -->
fix(driver-turso)!: a local or replica `TursoDriver` on a remote url, or a replica off a local file, is refused at construction
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
A remote `url` beside `syncUrl` was classified as an embedded replica, and the local SQLite engine that every replica read and write goes through was handed `:memory:`. Writes succeeded and read back, then vanished on restart, and none of them reached the remote. `@libsql/client` builds no embedded replica for a remote url: it routes `libsql://` / `https://` / `http://` to its HTTP client and `wss://` / `ws://` to its WebSocket client, neither of which reads `syncUrl`. A forced `mode: 'replica'` or `mode: 'local'` beside a remote url was handed the same `:memory:` engine, and so was a replica on `:memory:`. Measured before the change, with `create`, `find`, then a fresh driver on the same config:
With the driver building its own client and the default `sync.onConnect`, two of these did fail at `connect()`, but on a libsql error that did not say why: `libsql://` + `syncUrl` with `SYNC_NOT_SUPPORTED`, and `:memory:` + `syncUrl` with `URL_INVALID`.
19
+
20
+
**BREAKING** accept-set narrowing on a published driver option, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). **The constructor now refuses configurations it accepted before**, at `new TursoDriver()`, ahead of the Knex base and of any client, with the ADR-0112 envelope `code: 'VALIDATION_ERROR'`, `status: 400`. A remote url here means one of the lowercase schemes `TursoDriver.detectMode` classifies as remote: `libsql://`, `https://`, `http://`, `wss://`, `ws://`. Refused:
21
+
22
+
- a remote url beside `syncUrl`;
23
+
- a remote url under a forced `mode: 'replica'` or `mode: 'local'`;
24
+
- a replica on a url `@libsql/client` reads as in-memory (`:memory:`, or `file::memory:` with or without a query string), beside `syncUrl` or under a forced `mode: 'replica'`. `@libsql/client` refuses such an embedded replica itself. For `:memory:` and a bare `file::memory:` the local engine was a private in-memory database. With a query string it was a file literally named after the url's path (for example `:memory:?cache=shared`) in the working directory, which no sync reaches;
25
+
- under a forced `mode: 'replica'` only, any `url` that is not a local `file:` path, such as a bare path or an uppercase scheme.
26
+
27
+
The remote-url refusal names the scheme it met. Neither refusal echoes the url, which may carry a token. Both loaders (`@objectstack/runtime`'s host factory and the datasource factory) reach this refusal through the same constructor, so a datasource declaring one of these configurations now fails by name when its loader builds the driver.
28
+
29
+
**What stays accepted**, pinned by preservation tests: a `file:` url with `syncUrl` (the embedded replica), a `file:` or `:memory:` local database, a remote url on its own or with `mode: 'remote'`. `TursoDriver.detectMode()` still classifies a remote url beside `syncUrl` as `'replica'`: the refusal sits in the constructor, not in a re-classification.
30
+
31
+
**Not refused, unchanged here:** a url with no `mode` that is none of `file:`, `:memory:` or a lowercase remote scheme, such as an uppercase `LIBSQL://` or a bare path like `./data/app.db`, still auto-detects `'local'` and still runs on `:memory:`, with or without `syncUrl`. So does the same url under a forced `mode: 'local'`. That fall-through is tracked as #19976.
32
+
33
+
**What an affected author does.** Each refusal names its ways out. For a remote url in a local or replica mode:
34
+
35
+
- to use the remote database, drop `syncUrl` (and `sync`) and any forced `mode`; the remote url alone sends every read and write to it;
36
+
- for an embedded replica, point `url` at a local file and keep the remote in `syncUrl`: `url: 'file:./data/replica.db', syncUrl: 'libsql://my-db.turso.io'`.
37
+
38
+
For a replica off a local file, point `url` at a local `file:` path beside `syncUrl`. A throwaway in-memory database instead drops `syncUrl` (and `sync`) and any forced `mode: 'replica'`, and keeps `url: ':memory:'`.
39
+
40
+
Blast radius, measured on this tree: no example, template, published skill, hand-written doc or factory default declares a remote url beside `syncUrl`, and the host boot path (`OS_DATABASE_URL`) passes no `syncUrl`. Outside this package's own tests, the in-repo configurations carrying the pair are test fixtures that never construct the real driver: loader fixtures that exercise the config builder or a capturing constructor, stored-row redaction fixtures and a schema-parse fixture. Whether any out-of-repo deployment declares it is NOT measured and is not claimed to be zero.
41
+
42
+
<!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing performed at the driver constructor: no key, spec symbol, Zod schema, object definition or stored representation is added, removed or renamed — `TursoDriverConfig.url`, `syncUrl` and `mode` keep their names and types, and both `TursoConfigSchema` copies are untouched. What moves is which CONFIGURATIONS `new TursoDriver()` accepts, so `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. Each refusal names its ways out, and which one an author wants (a remote database, an embedded replica on a local file, or a plain local database) is authoring intent no ledger line can decide. -->
0 commit comments