Skip to content

Commit b5cec3d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21516-unresolved-name-refusal
2 parents 6752a29 + 901e7cf commit b5cec3d

85 files changed

Lines changed: 6502 additions & 652 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The protocol 17 → 18 conversion summaries no longer cite tracker numbers; each one states the decision behind it in words
6+
7+
Clause-②: no
8+
9+
A conversion's `summary` is the line an author reads when upgrading metadata: `os migrate meta --json` reports it under `specChanges` (its chain already runs to protocol 18), and it becomes the "Change" column of the upgrade guide's protocol 17 → 18 table and the `to` text of `spec-changes.json`'s `converted[]` records once protocol 18 ships. Thirty-five of the protocol-18 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example:
10+
11+
- The six duration-key renames (`hook.timeout` → `timeoutMs`, `apis[].cacheTtl` → `cacheTtlSeconds` and the rest) say the rule they follow: a duration key carries its unit in its name.
12+
- `translation-per-app-settings-removed` says why both application doors lose `settings`: settings copy belongs to the platform, and the bundle entry and the translation item are two doors of one type that accept one shape.
13+
- `flow-decision-mode-inclusive-explicit` says the decision node now follows mainstream engines (first match wins) and that taking every true edge must be declared.
14+
- `list-view-sort-string-clause-to-array` and `page-component-filter-record-to-rule-array` say what "one orthography platform-wide" means for each, and why combinator filters are named rather than flattened.
15+
16+
Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-analytics': patch
3+
---
4+
5+
The read-scope comparand refusals, the native-SQL cross-field backstop and the two display-SQL echo refusals no longer cite tracker numbers; each one states the decision behind it in words
6+
7+
Clause-②: no
8+
9+
Some strings the analytics service shows to operators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
10+
11+
- The read-scope compiler's undefined-comparand refusal says an undefined comparand is refused rather than read as null, on the SQL drivers and on this door alike. Its refusal of a non-boolean `$null`, `$exists` or `$empty` comparand says a non-boolean comparand for any of the three is refused rather than coerced, on every driver and on this door alike. Both still say they fail closed, and that the producer to fix is whoever built the read scope, never the caller of the query.
12+
- The native-SQL strategy's cross-field backstop and the `/analytics/sql` echo's refusal of a field-reference comparison say the engine path's driver enforces the cross-field rules (declared same-table columns only, never the tenant-isolation column, one comparison class) with metadata it owns, so those rules are enforced in one place, next to the metadata they read.
13+
- That echo refusal and the echo's unmapped-operator refusal say the echo renders every predicate the query runs with, or refuses.
14+
15+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: eight list members of an `object-grid`, `object-kanban` or `object-calendar` page block take the shape the block reads instead of any value — the grid's `fields`, `selection`, `selectable`, `rowActions`, `bulkActions` and `batchActions`, the kanban's `columns` and the calendar's `calendar` (#21464)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-grid-kanban-calendar-list-members-typed -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path.
12+
13+
**`@objectstack/spec`**
14+
15+
- **Eight members are typed.** `ComponentPropsMap['object-grid']`, `['object-kanban']` and `['object-calendar']` declared these members as `z.unknown()` (an array of it for the lists), although each renderer reads them with one shape. Any value passed, and an off-shape one was dropped or substituted with no report: an object entry in `fields` named no field; a `{ name }` entry in `bulkActions` was skipped; a kanban lane list mixing objects and strings drew a blank lane; a calendar block with no `startDateField` placed no event.
16+
- **The list view's own members, by reference**, where a list view declares one: the grid's `selection` (`{ type }`, with `none`, `single` or `multiple`), `rowActions` and `bulkActions` (action-name strings), and the calendar's `calendar` (`{ startDateField, endDateField?, titleField?, colorField?, allDayField? }`). `batchActions`, the second spelling of `bulkActions` that the grid reads first, takes `bulkActions`'s def. Neither spelling is retired here.
17+
- **The measured shape**, where no list view declares the member: the grid's `fields` (field-name strings), the grid's `selectable` (`true`, `false`, `'single'` or `'multiple'`), and the kanban's `columns` (all lanes `{ id, title, cards?, limit?, className?, collapsed? }`, or all bare value strings). A lane `id` and `title` are strings, a static card carries a string `id` and `title` beside its row's own values, and `limit` is a positive integer.
18+
- **`ObjectGridProps`, `ObjectKanbanProps` and `ObjectCalendarProps`** (and their `…Parsed` twins) carry these types on the eight members instead of `unknown`.
19+
- **The grid's `columns` is not narrowed** and still accepts any value. The list view's column entry is its by-reference shape, and the grid's draw path reads exactly that, but the grid's group headers also draw the labels from an authored column's `options` (the column whose `field` is the grouping field, ahead of the field's own options). The list view's column entry declares no `options`, so typing `columns` now would refuse a value the grid draws. It is held until that read is ruled.
20+
- **The enumeration pin** loses eight lines and keeps the grid's `columns` as held for that ruling. One `z.unknown()` member is added and recorded: the rest of a static kanban card (its row's own values, beside the typed `id` and `title`).
21+
22+
## FROM → TO
23+
24+
| you wrote | write instead |
25+
|:--|:--|
26+
| `object-grid` `fields: [{ field: 'name', width: 240 }]` | `fields: ['name']`, or the entry on `columns` |
27+
| `object-grid` `selection: 'multiple'` | `selection: { type: 'multiple' }` |
28+
| `object-grid` `selectable: 'none'` | `selectable: false`, or `selection: { type: 'none' }` |
29+
| `object-grid` `bulkActions: [{ name: 'approve' }]` (also `batchActions`, `rowActions`) | `bulkActions: ['approve']`, or the full def on `bulkActionDefs` |
30+
| `object-kanban` `columns: [{ id: 'done', title: 'Done' }, 'todo']` | one spelling per list: `columns: [{ id: 'done', title: 'Done' }, { id: 'todo', title: 'To Do' }]` |
31+
| `object-kanban` a lane `color: 'red'` | `className: 'border-t-2 border-red-500'` |
32+
| `object-kanban` a lane `{ id: 1, title: 'One' }` | `{ id: '1', title: 'One' }` |
33+
| `object-calendar` `calendar: { dateField: 'kickoff', endField: 'wrapup' }` | `calendar: { startDateField: 'kickoff', endDateField: 'wrapup' }` |
34+
35+
The one-line fix: write each member as the list view declares it, or as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entry `ui-object-grid-kanban-calendar-list-members-typed` carries that judgment.
36+
37+
## Who is affected, measured
38+
39+
A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants, and the control is `objectName` on the same nodes.
40+
41+
- **objectstack** at `49161683fb`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 57 `object-grid`, 30 `object-kanban` and 5 `object-calendar` nodes (the control on 47 / 27 / 4 of them). The one authored value among the eight members is a kanban `columns` (lanes, in the protocol docs), and it parses. No node authors another of the eight.
42+
- **objectui** at the `.objectui-sha` pin `89cad75d55`: 689 `object-grid`, 240 `object-kanban` and 160 `object-calendar` nodes (the control on 293 / 108 / 98). Across the eight members, 241 values are static, and 233 of them parse. Each of the 8 that do not is a test fixture whose value the renderer drops, skips or refuses: 2 object entries in `bulkActions` (the renderer skips them, and the tests assert the skip), 3 object entries in `fields` that copy the hand-off the list view makes to the grid at run time (not an authored page), a lane `color` (retired in the console; the test marks it an undeclared member), and 2 uses of the calendar's retired `dateField` / `endField` aliases (the test asserts their refusal). No refused value is one the renderer draws. 24 values are not static (helper parameters, `.map` results and the run-time hand-offs); none of them is an authored page. The grid's `columns` (310 static values) is held because 2 of them author a column `options` the grid draws in its group headers, a fixture written to pin that behaviour.
43+
- **Deployed metadata** was not measured.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
Public forms on a walled tenancy posture: a form whose object is walled by an organization column is no longer offered to anonymous visitors. An anonymous submission carries no organization, and on a walled posture an insert into such an object without one is refused, so the form used to render and then answer `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on every submit. Both anonymous form endpoints (`GET /forms/:slug` and `POST /forms/:slug/submit`) now answer it exactly as they answer a withdrawn form (`404 FORM_NOT_FOUND`), so an anonymous caller learns nothing about the deployment's tenancy. The administrator's read of the form (`GET /meta/view/:name`) states why in `_diagnostics.warnings`, located at the form's `sharing`, with the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. Forms bound to tenancy-disabled objects, and single-posture deployments, are unchanged.
6+
7+
Clause-②: no
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
'@objectstack/metadata-protocol': minor
4+
---
5+
6+
fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups, so the package's permission sets and their grants go with it
7+
8+
Clause-②: yes
9+
10+
`DELETE /api/v1/marketplace/install-local/:manifestId` removed the package's ledger entry and nothing else. After a restart the package's objects were gone, but its `managed_by: package` rows in `sys_permission_set`, and every grant of them, survived the uninstall. That broke ADR-0090's "No ghost grants" promise on this door.
11+
12+
The door now runs the uninstall cleanups that domain plugins register with the protocol (`registerUninstallCleanup`) once the ledger entry is gone. It uses the same registry and the same runner as the protocol's own uninstall, so `plugin-security`'s `security.package-permissions` cleanup removes the package's sets with their position and user bindings, and any cleanup registered later fires here too. The cleanups run with the package's manifest id and no organization, because an install-local package is installed for the whole runtime.
13+
14+
The response carries each outcome as `data.cleanups`, the way the protocol's uninstall reports them. A failed cleanup is reported there and named in the operator log with its remedy (install the package again, then uninstall it again). When the protocol cannot run the cleanups, the response says so as one failed `protocol.runUninstallCleanups` outcome. An uninstall that does not happen (a refused caller, an id this door never installed, a ledger write that fails) revokes nothing.
15+
16+
`@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation` gains `runUninstallCleanups({ packageId, organizationId?, actor? })`, the one runner of the uninstall-cleanup registry. It runs every registered cleanup for the package and answers one `UninstallCleanupOutcome` per cleanup. It never throws: a failed cleanup is an outcome, and a thrown fault's driver text goes to the operator log, not into the outcome. `deletePackage` now calls it as its last step in place of its own loop, and its `cleanups` are unchanged. The only visible difference there is the log tag of a failed cleanup's warning, now `[protocol.runUninstallCleanups]` instead of `[protocol.deletePackage]`.
17+
18+
`@objectstack/cloud-connection` now declares its dependency on `@objectstack/metadata-protocol`, which it already received through `@objectstack/runtime`, for the cleanup outcome types.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
`os dev -a PATH` and `os start --artifact PATH` now serve the artifact they name, also from a directory that holds an `objectstack.config.ts` (#21501).
6+
7+
Clause-②: no
8+
9+
- **One precedence, written once.** The order is `--artifact` > `OS_ARTIFACT_URL` > `OS_ARTIFACT_PATH` > `<cwd>/dist/objectstack.json` > `<home>/dist/objectstack.json` (`os start` only) > a cwd `objectstack.config.ts`, except that a cwd config joins the boot when the resolved artifact is its own compiled output. It is the order the `os start` reference already published. `os start` and `os dev` both resolve through one module, and the `serve` child they spawn boots exactly their answer.
10+
- **Beside a config.** The child used to read the supervisor's answer only when the working directory held no config. So `os dev -a X` and `os start --artifact X` printed `Artifact: X` and served the config's `dist/objectstack.json`, or the config itself. A named artifact now boots alone, exactly as it boots from a directory with no config. The config takes part only when the artifact is its own compiled output: `<config dir>/dist/objectstack.json`, or the path the command compiled it to. A bare `os dev`, a bare `os start` in a project, and `os start --artifact ./dist/objectstack.json` take that path, and are unchanged. A host config (its `plugins` hold code) boots its own module there, because its compiled output cannot carry that code.
11+
- **`OS_ARTIFACT_PATH` beside a config** follows the same rule: `OS_ARTIFACT_PATH=Y os start` serves `Y` without loading the config. Under `os start --artifact ./dist/objectstack.json` the flag now also wins over an exported `OS_ARTIFACT_PATH` inside the config boot.
12+
- **`os dev` under a local `OS_ARTIFACT_PATH`** compiles the cwd config into that path, so the file there is the config's own compiled output. The config takes part in the boot that serves it, and a host config compiled there keeps its plugins.
13+
- **`os dev` gains the `OS_ARTIFACT_URL` rung.** `--artifact` outranks it. Before, the reference stayed in the child's environment and won. Without the flag the reference drives the boot, as under `os start`. The `Artifact:` row names it (redacted), and nothing is compiled into, watched for or judged stale against it.
14+
- **Banner rows.** `os start` and `os dev` print `Config:` only when the config takes part in the boot. The child says it is not loading a config that sits beside a named artifact, instead of `No objectstack.config.ts found`.
15+
- **The ready banner names what loaded.** On a config boot, a non-host config whose app was served from its compiled artifact gets `Artifact: dist/objectstack.json` in the ready banner, and a host config keeps `Config: objectstack.config.ts`. No ready-banner row names a file the boot did not load.
16+
17+
Upgrading: a project that ran `os dev -a`, `os start --artifact` or `OS_ARTIFACT_PATH` beside its config, and relied on that config being loaded, should drop the override or point it at `./dist/objectstack.json`.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime)!: an app-authored body may not bind a hook to, or write, the stored-metadata tables (#21520)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) no metadata body, authorable key, spelling, export or stored shape moves; what changes is which tables a sandboxed hook or action body may be bound to and may write, so `objectstack migrate meta` has nothing to rewrite. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a refused binding or a refused write (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows what an app-authored body may do with the two stored-metadata tables, `sys_metadata` and `sys_metadata_history`. For an app-authored body, the metadata protocol is now their only writer: a change to metadata goes through the metadata API, where it is validated and its provenance is recorded.
12+
13+
- **Binding.** A hook with a sandboxed `body` whose `object` names either table, alone or in a list, is no longer bound. The refusal is made at registration, at the one point every body hook becomes a handler, so it holds on every door a hook binds by: a code bundle or boot artifact, an installed artifact, and a hook authored at runtime through the metadata door. It carries `PERMISSION_DENIED` / 403, names the metadata API, and is recorded against the hook in the bind log at `error` (thrown under strict binding). A wildcard (`'*'`) body hook still binds; its body is not run for either table's events, and the bind says so once at `info`.
14+
- **Writing.** A sandboxed action or hook body's write of either table through `ctx.api` — every write verb, inside a transaction or not, with or without elevation — answers `PERMISSION_DENIED` / 403 before the write runs, so nothing lands and the answer does not depend on what the write names.
15+
- **Unchanged:** a body's reads of the two tables (still served as the generic data door serves them); host code that registers its own action handlers or hooks; the platform's own hooks, which are code and still fire on the metadata door's save; and every other object.
16+
17+
The route: change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) rather than from a body, and bind hooks to the objects an app owns. No shipped example binds a body hook to either table or writes one from a body. It ships as `minor` under the launch-window convention for accept-set narrowings.

0 commit comments

Comments
 (0)