Skip to content

Commit e6d8238

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23077-url-target-doors
packages/spec/dropped-refinements.baseline.json conflicted (both sides add sites). The merge takes main's side whole; the next commit re-derives this branch's sites from build-schemas' own printed entries on the merged tree. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
2 parents ed59283 + 4a87d09 commit e6d8238

103 files changed

Lines changed: 4767 additions & 3337 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/cloud-connection': minor
5+
'@objectstack/cli': minor
6+
---
7+
8+
An unmanaged install no longer copies a package's protection lock, in either spelling: every copied item is editable, and the install's answer names each lock it dropped with the package's reason (ADR-0131 D6, ADR-0010)
9+
10+
Clause-②: yes (widening: the new response field `strippedLocks` on `ImportUnmanagedPackageResponseSchema`, relayed on the install-local answer for `installMode: 'unmanaged'`)
11+
12+
An unmanaged install copies a package into this environment as the environment's own metadata. A package can lock an item in two spellings: the `_lock` envelope (`_lock`, with `_lockReason`, `_lockSource` and `_lockDocsUrl`), or the author-facing `protection: { lock, reason, docsUrl }` block that a managed install translates into it. Until now an item locked with `_lock` was copied with that lock, so the next save of the copy answered `403 ITEM_LOCKED`; an item locked with `protection` was copied with an inert block that bound nothing, so the vendor's lock vanished without a word. The package permitted the unmanaged install through its `installModes`, so it chose to let environments take editable copies; a lock whose source is the package does not survive the copy.
13+
14+
**This entry supersedes two sentences of the entry `15213-install-modes`, in this same release.** That entry says "One exception is carried: an item the package ships with an authored protection lock (`_lock`) keeps that lock on the copy as written today, and a follow-up on #15213 strips it per #22971's ruling A", and later "except an item shipped with an authored `_lock`, which keeps that lock". What holds now: the copy carries no lock in either spelling, its next save edits it, and the install's answer lists each lock dropped. Where the two entries disagree, this one is current.
15+
16+
- **The copy** (`@objectstack/metadata-protocol`). `importUnmanagedPackage` writes every item without `_lock`, `_lockReason`, `_lockSource` and `_lockDocsUrl`, and, on a type whose schema declares one, without its `protection` block. Its answer gains `strippedLocks`: one entry per lock that bound (`no-overlay`, `no-delete` or `full`), in the order the items were written — `{ type, name, lock, reason? }`, with `reason` the package's reason verbatim and absent when it gave none. Each entry is the lock a managed install of the same item would have bound: an item carrying both spellings is reported once, with the `protection` block's lock and reason, because that is the one a managed install binds. A package that locks nothing answers `strippedLocks: []`.
17+
- **Refusals are unchanged.** A lock the spec's declarations refuse (an unknown `_lock` value, a reason that is not a string, a malformed `protection` block) is not stripped: the save door refuses that item as before, `422 INVALID_METADATA`, and the install is rolled back. So is a `protection` block on a type whose schema does not declare one (`action`, `hook`, `page`). A managed install is unchanged: the package stays the authority, and its locks bind.
18+
- **The declaration** (`@objectstack/spec`). `ImportUnmanagedPackageResponseSchema` declares `strippedLocks`; `lock` takes the three binding values of `MetadataLockSchema`.
19+
- **The install door** (`@objectstack/cloud-connection`, `POST /api/v1/marketplace/install-local` with `installMode: 'unmanaged'`). The answer carries the import's `strippedLocks` as the import states it.
20+
- **`os package install --install-mode unmanaged`** (`@objectstack/cli`). Each dropped lock prints as one line under `Locks removed`, spelled `<type>.<name>: <lock> — <reason>`, so the installer reads why the vendor locked an item before editing it. An answer from a runtime that states no list prints no lock line.
21+
22+
Nothing is removed or narrowed: the answer gains a field, and every refusal of the unmanaged install is unchanged.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 17 more author-time rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 17 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- list-view search: `searchable-field-unknown` (448 → 129), `searchable-field-unprovisioned` (362 → 175) and `searchable-field-unsearchable` (328 → 139);
11+
- list-view sort: `sort-field-unprovisioned` (378 → 191), `sort-field-unsortable` (332 → 154) and `sort-field-unknown` (301 → 142);
12+
- visibility predicates: `visibility-predicate-unknown-function` (375 → 165), `visibility-bare-identifier` (221 → 102) and `visibility-predicate-syntax` (214 → 113);
13+
- flow filters: `flow-filter-token-unknown` (427 → 182);
14+
- actions: `action-no-placement` (424 → 171);
15+
- import mappings: `mapping-target-field-unknown` (405 → 189);
16+
- dashboards: `unconsumed-widget-option` (396 → 135);
17+
- app navigation: `nav-object-ungranted` (370 → 118);
18+
- hook bodies: `hook-body-write-unprovisioned-anchor` (365 → 178), `hook-body-write-unknown-field` (275 → 110) and `hook-body-source-unparseable` (254 → 105).
19+
- **One line, one remedy.** Where a fix line explained its remedy, the explanation moved: why a stale or inadmissible `searchableFields` entry is a `400 INVALID_FIELD` on list search, that a view narrows the object's searchable set and never widens it, and that a lookup column stores only an id; that both runtime doors refuse a formula sort with `400 INVALID_SORT`; how to vouch for an unprovisioned anchor (`columnMap`) or opt the object out of the injection (`ownership: 'none'`, `systemFields: { audit: false }`); the two vocabularies a flow filter token resolves in; the other-language operator spellings CEL does not parse, why re-spelling cannot fix an unregistered function, and which namespaces each surface binds; why gating a nav entry does not clear a missing grant; which hook-body write shapes are checked; what a partially parsed body leaves unchecked.
20+
- **A list the verdict already covers is not echoed.** `searchable-field-unknown`, `sort-field-unknown` and `mapping-target-field-unknown` no longer list every field of the object: the verdict's "Did you mean" names the near miss. `mapping-target-field-unknown` still lists the object's compound fields and their parts, as every import-mapping refusal does.
21+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Two ids had no entry and gain one, so their `rule:` line now ends with the `os explain` pointer: `action-no-placement` ("where an action can be placed") and `unconsumed-widget-option` ("which widget options a renderer reads"). Five entries gain text. The action locations the new entry spells out are held to the spec's `ACTION_LOCATIONS` by the rule's test.
22+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), 10 of the 17 fire; the `hint` FROM → TO, measured at the door:
23+
- a `view` write: `sort-field-unknown` (422 `issues[].hint`, a name / a dotted path 114 / 261 → 47 / 135), `sort-field-unsortable` (422, 318 → 140), `sort-field-unprovisioned` (2xx `advisories[].hint`, 363 → 176), `searchable-field-unknown` (422, a name / a dotted path 281 / 430 → 48 / 129), `searchable-field-unsearchable` (422, a formula / outside the declared set / a lookup / a json column 222 / 142 / 323 / 157 → 110 / 68 / 135 / 64), `searchable-field-unprovisioned` (2xx advisory, 361 → 174), `visibility-predicate-syntax` (422, a recognised spelling / none 106 / 214 → 106 / 107), `visibility-predicate-unknown-function` (422, 371 → 165) and `visibility-bare-identifier` (422, a bare name / an unbound namespace 160 / 215 → 41 / 96);
24+
- an `object` write: `searchable-field-unknown` (422, the object's own set / a built-in list view 266 / 261 → 55 / 50), `searchable-field-unsearchable` (422, 222 → 110) and `searchable-field-unprovisioned` (2xx advisory, 361 → 174);
25+
- a `flow` write: `flow-filter-token-unknown` (422, `{TOMORROW()}` / `{ROUND(x)}` / `{Math.round(x)}` / `{TODAY() - 45 - 10}` 336 / 358 / 427 / 100 → 91 / 113 / 182 / 100).
26+
27+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the two `*-unprovisioned` toasts shorten; Studio renders no 422 issue's `hint`. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
28+
- **Never at the runtime gate:** `action-no-placement`, `mapping-target-field-unknown`, `unconsumed-widget-option`, `nav-object-ungranted` and the three `hook-body-*` ids run on the CLI only.
29+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The description of `branding.logo` on an app now states the shape of the one asset it takes: a 1:1 square app icon, SVG preferred, at least 64 px, which each surface scales to the size it needs, with no second size and no wordmark slot. Text only; no schema changes: the key stays an optional string, the accept set is byte-identical, and a non-square value is not refused because a URL string does not carry its ratio.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): an empty state names its one next step — `emptyState.link: { label, url }` — and a list page carries its own `interfaceConfig.emptyState`
6+
7+
Clause-②: yes (widening: two new accepted keys)
8+
9+
- **`EmptyStateSchema.link`.** An empty state takes one optional link beside `title`, `message` and `icon`: `{ label, url }`, both required and nothing else, for example `link: { label: 'How to publish a package', url: '/docs/publish-a-package' }`. It opens a page or an address and runs no action, flow or script. The shape is closed: a missing `label` or `url`, any other key and a non-object are refused at parse.
10+
- **The targets a `type: 'url'` action opens, and no others.** `link.url` admits an in-app path that starts with `/` or `./`, an address that opens with `http://`, `https://` or `mailto:`, and a template that opens with `${…}` (such as `${ctx.origin}/docs`), which is judged when it is clicked. Every other scheme (`javascript:`, `data:`, `tel:`, …), a protocol-relative `//host`, `/` followed by a backslash, a tab, line feed or carriage return, and a bare relative target such as `docs/x` are refused at `link.url` with the reason and the fix, in the same sentences a url action's `target` is refused with. An empty `url` is refused as empty.
11+
- **Every door that carries the empty state takes the link:** a list view's `emptyState` (and so an object's `listViews` and a view document's), the `object-grid` page block's `emptyState`, and a list page's new `interfaceConfig.emptyState`.
12+
- **`interfaceConfig.emptyState`.** A `type: 'list'` page takes its own empty state, the list view's shape itself: `{ title, message, icon, link }`, refused at parse exactly as the list view's is.
13+
- **Guidance.** `action` or `button` written on an empty state is now answered with the link, and still with the list view's `addRecord` block for an add-record entry point; a `url` or `href` written flat on the empty state is pointed into `link`.
14+
- **Not rendered yet.** No console renderer draws the link or a list page's own empty state today. Both parse and are not drawn, and their schema descriptions say so (`[PLANNED — not yet rendered]`). Until the console reads it, a list page still draws its source view's empty state, if any.
15+
- **Published JSON Schema.** `link.url` publishes as a string with `minLength: 1`. The target rule is a parse-time refinement the published file does not express; the file names its position under `x-dropped-refinements`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`PageHeaderProps.actions` now says what its list is: the whole set the header draws. An action that declares `record_header` (or `record_more`) in its `locations` but is not named in an authored `page:header`'s `actions` is not drawn in that header, and a header with no `actions` draws none of the object's own actions; a named id whose action declares neither location renders nowhere, and the host's own Edit / Share / Delete chrome is separate from the list. The `.describe()` was "Action IDs to show in header", which read as a filter over the declared placements. Text only: the accepted set of `actions` is unchanged, and no rule or renderer behaviour moves.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': patch
4+
---
5+
6+
fix(spec, objectql): a `tree` field with no `reference` points at its own object, so its `deleteBehavior` takes effect when a parent is deleted
7+
8+
Clause-②: yes (widening)
9+
10+
A `tree` field is a record's parent in a hierarchy on its own object. `reference` is optional on it, and when you write one it must be the object's own name — the object schema refuses any other target and tells you to drop the key. But a `tree` with no `reference` behaved as if it pointed at nothing. Deleting a parent answered success and left every child holding the deleted id, whatever `deleteBehavior` said (`set_null`, `restrict` or `cascade`). The same field also could not be expanded (`expand=parent` answered "declares no target object"), could not be filtered through (`{ parent: { title: … } }` was refused), and accepted a parent id that does not exist.
11+
12+
Now a `tree` with no `reference` is a reference to the object that declares it, exactly as if you had written the object's name:
13+
14+
- **`@objectstack/spec`.** `ObjectSchema` and `ObjectExtensionSchema` write the object's own name (an extension's `extend`) into such a field's `reference` when they parse it. `defineStack` and `ObjectSchema.create()` output, and built artifacts, now carry the object's name in `reference` on those fields, byte-identical to a field that wrote it. Nothing you could write before is refused now. A new export, `materializeTreeSelfReferences(objectName, fields)`, is the one helper both the parse and the engine use.
15+
- **`@objectstack/objectql`.** The object registry writes the same self-reference when an object is registered. That covers an object saved through the metadata API (`PUT /meta/object/:name` stores the body as written) and a direct `registerObject` call, neither of which goes through the parse.
16+
17+
What changes on a parent delete:
18+
19+
- `set_null` (the default) clears the children's parent field and keeps the children.
20+
- `restrict` refuses the delete with `DELETE_RESTRICTED` (409), naming the tree's own object as the dependent.
21+
- `cascade` deletes the whole subtree.
22+
23+
`expand`, nested relation filters, the write-time check that a parent id exists, the dangling-reference audit and `os verify --delete-behaviors` now treat the field as a self-reference too. A `tree` that already wrote its own name, and every `lookup` and `master_detail`, behave exactly as before.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/objectql': patch
4+
---
5+
6+
A language the environment added to an email template, which the package does not ship, is no longer treated as packaged code
7+
8+
Clause-②: no
9+
10+
An `email_template` is identified by its name and its `locale`, so the languages of one template form a bundle. When an administrator customizes a template in a language its package does not ship (for example a `zh-CN` version of a template the package ships in `en-US` only), that version is the environment's own item. Until now it was registered and served wearing the package's protection envelope, taken from the shipped `en-US` version:
11+
12+
- the registry's artifact lookup answered the environment's `zh-CN` version as the package's own `zh-CN` artifact;
13+
- `GET /meta/email_template/:name?member=zh-CN` and the `/meta/email_template` list served it with `provenance: 'package'`, the package's id and its version;
14+
- the registry heal that runs after `DELETE ?member=zh-CN` refused to retire it as packaged code.
15+
16+
- `@objectstack/metadata-protocol`: the artifact envelope (`_packageId`, `_packageVersion`, `_provenance`, and an artifact lock) is copied only onto a version of the same language as the shipped artifact. Each place that copies it looks up the artifact of the body's own language first. A language the package does not ship is registered and served as the environment's item. A language the package does ship keeps its envelope, its lock and its reset exactly as before. Types without languages are unchanged.
17+
- `@objectstack/objectql`: `SchemaRegistry.removeRuntimeShadow(type, name, member)` lifts a named language's shadow only when the package ships that language. For any other language it declines, and the heal's last step (`removeOverlayEntry`) retires the entry. Naming no language still sweeps the whole bundle, as before.
18+
19+
A lock still binds the template name, at the read and at the write gate alike: a template the package locks stays locked in every language.

0 commit comments

Comments
 (0)