Skip to content

Commit cc15060

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-25019-sqlite-file-failure-family
2 parents f7ea1c5 + 157ebd0 commit cc15060

68 files changed

Lines changed: 5995 additions & 201 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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
feat(cli, objectql): `os db orphans` — report the detail rows whose required master-detail master does not exist, and delete them only behind `--delete`, after a pre-delete export
7+
8+
Clause-②: yes (widening)
9+
10+
**What was wrong.** A system-context write is exempt from the write-time reference check, so an elevated writer could store a master-detail id that names no master. Such a row's access derives from that master, so every user door answered 404 for it, the administrator's `GET` and `DELETE` included, and a list query left it out. The integrity audit reported the row and, by design, never rewrites it. No door existed to remove it, so it stayed until someone edited the database by hand.
11+
12+
**`os db orphans` (new command).** An explicit operator command. Nothing on any boot, upgrade, migrate or lifecycle path runs it.
13+
14+
- **The default run is a report** that writes no row or table and creates no file. It boots read-only, as `os migrate plan` does. It lists each detail row whose required `master_detail` master does not exist, with its object, record id, field and missing master id. It also lists the deeper detail rows that the delete's cascade removes with it. `--json` prints one document, `{ mode: 'report', plan }`.
15+
- **What is never deleted, and is said.** A reference whose target object could not be checked (not registered, no driver, or a failed read) is counted as `undetermined`. Unknown is not missing. A dangling reference on the same objects through a field that is not a required master-detail is listed in `otherDangling`. An orphan whose cascade could not be read is listed as `withheld`, with the reason. A scan bounded by `--max-rows` (default 100000) names every object it could not read in full.
16+
- **`--delete` requires `--export <path>`.** The export holds every record the run deletes, as the driver stores it. It is written owner-only, is created rather than overwritten, and is read back before the first delete. If it cannot be written or does not hold every record, the run refuses with nothing deleted. Just before deleting, the run reads the report again and skips an orphan whose master now exists, or whose cascade now reaches a record the export does not hold. Each delete is the engine's own system-context delete, so the master-detail's cascade removes the deeper details. A `restrict` dependent refuses the delete, and a cascade on one datasource is one unit of work, so nothing of that orphan is removed. A `set_null` reference to a deleted row is cleared. A roll-up on the missing master cannot be recomputed, and the run notes this rather than failing. Each delete is judged by reading its records back.
17+
- **Refusals**, each exit 1 with nothing deleted, and the code under `--json` as `error`: `export_required`, `export_exists`, `export_failed`, `export_incomplete`, `confirmation_required` (`--json` without `--yes`), `boot_failed`, `no_engine` and `scan_failed`.
18+
19+
**`@objectstack/objectql`: `ObjectQL.inspectCascadeDelete(object, id)` (new method).** A read-only method that returns every record a by-id `delete()` of that record removes, root first. It is the cascade set the delete collects for itself, read through the same walk. It writes nothing, runs no hook, and files no reference-check elevation record, because no delete happens. It reads as the system with no organization. `restrict` and `set_null` relations are not listed, because neither removes a record. A dependents probe that fails propagates.
20+
21+
Nothing that ran before changes: the audit still reports and never rewrites, and the system-write exemption is unchanged.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/cli': minor
5+
---
6+
7+
feat(spec): a `page:tabs` item label gains a translation key, `pages.<page>.components.<tabsId>.items.<value>.label`
8+
9+
Clause-②: yes (widening)
10+
11+
- **Before.** `pages.<page>.components` is keyed by component id, and a `page:tabs` item is not a component and carries no id, so a tab label had no key. `items` was refused as an unknown key, and `translatePage` never read `items[].label`. A localized page kept its tab strip in the authoring language.
12+
- **Now.** `pages.<page>.components.<tabsId>.items.<value>.label` is a declared key. It is addressed by the tabs component's own `id` and the item's `value` (the stable `?tab=` token), and `translatePage` overlays it onto that item's `label`, across the locale chain. The entry is closed: `title`, `name` and `text` are renames onto `label`. An item without a `value` has no key, because its renderer token is index-derived and an array index is never a bundle key. Author a `value` on the item, or keep localizing that label with an inline locale map.
13+
- **`walkAddressedPageComponents`.** A visitor's edit to a panel's own keys (its `label`) now stands when the visitor keeps the panel list's authored shape. Panel children are still descended from the authored document.
14+
- **`objectstack validate`.** `@objectstack/lint`'s translation-reference check accepts the key over a declared `page:tabs` item `value`. It refuses it as `translation-target-unknown` over an unknown value (with "Did you mean"), over a component of another type (a `page:accordion` is pointed at the inline locale map, since its panels have no stable key), and under a component id the page does not declare.
15+
- **`os i18n extract` / coverage.** The extractor writes a slot for every valued tab item, and the coverage check demands it. An inline locale map on the item label is counted as authored, as before.
16+
- **Unchanged.** Every existing `pages` key, and an inline locale map on a tab label that the bundle does not key.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/rest': minor
4+
'@objectstack/lint': minor
5+
'@objectstack/cli': minor
6+
---
7+
8+
feat(spec): a form view's `title` is translated through `objects.<object>._views.<view>.title`, and `GET /forms/:slug` serves it in the visitor's language
9+
10+
Clause-②: yes (widening)
11+
12+
- **Before.** A form view's `title` (`FormViewSchema.title`) had no translation key. The `_views.<view>` entry named `title` as a rename onto `label`, `translateView` never read `config.title`, and `GET /forms/:slug` answered the top-level `label` from the untranslated view. A public form that declared a title served it in the authoring language to every visitor, so declaring one replaced a translated heading (the object label) with an untranslated one in every other locale.
13+
- **Now.** `objects.<object>._views.<view>.title` is a declared key, under the same `_views` node and view key as the view's `label`. `translateView` overlays it onto a served form item's `config.title`, on the same locale chain and override rule as every other view string: only a form that authors a title is translated, a missing key keeps the authored title, and a title edited on top of the packaged view keeps the edit. `heading` and `formTitle` are named as renames onto `title`.
14+
- **`GET /forms/:slug`.** The served form's `title` is the translated one, and the top-level `label` is taken from the translated view when the view authors a `label`. A view that authors none is still answered without one, so the console heading (`label`, then `form.title`, then the translated object label) is unchanged for it.
15+
- **`objectstack validate`.** `@objectstack/lint`'s translation-reference check accepts a `title` key over a form view (a `formViews.<key>` entry, the default `form`, or a `viewKind: 'form'` item) and refuses it, as `translation-target-unknown`, over a list view, pointing at the view's `label` key.
16+
- **`os i18n extract`.** The CLI extractor emits `_views.<view>.title` for each form a view container serves (keyed by the composer's runtime name) and for a `viewKind: 'form'` item, so the skeleton carries the slot and the i18n coverage gate reports an authored title a supported locale leaves untranslated.
17+
- **Unchanged.** Every other `_views` key, the view `label` and `description` reads, and a form without a title.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
fix(core): the import resolves a lookup cell by the target object's stored title field
6+
7+
Clause-②: no
8+
9+
- **Before.** A lookup cell was matched against the target by its id, then by the lookup's own `displayField`, then by a fixed list of keys (`name`, `title`, `label`, `full_name`, `email`, `username`). The target object's declared title field was never asked. So a cell that named the record by its title, such as `Wren` for a target whose title is `nickname`, answered `reference_not_found`, although the list export writes lookup cells by that same title.
10+
- **Now.** After the id and the lookup's `displayField`, the import asks the target's title field, resolved by ADR-0079's one rule (`resolveDisplayField`: `nameField`, then the deprecated `displayNameField` alias, then the derived title). It is read from the same object declaration the import already reads to check the target's exposure, so no extra metadata read is made. Two records that share the title answer `reference_ambiguous`, like the import's other ambiguous matches. The cell is never linked to the first hit.
11+
- **A formula title is not matched.** A `formula` field has no stored column, and the engine refuses a filter over it. The import no longer sends that query, for the title or for any other candidate the target declares as a formula, so a cell against a formula title still answers `reference_not_found`. How a formula title should be matched is still open.
12+
- **Unchanged.** The fixed fallback keys and their order. The exposure check still runs first: a target whose declared exposure does not serve `list` answers `reference_not_found` for every cell and is never read.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
spec!: `WebhookSchema` declares `secret` (a credential value) and `headers` (a credential map, as a whole) with `.meta({ credential: 'value' | 'map' })`, and `getMetadataTypeRedactor(type)` gains a third resolution — after explicit registrations and built-ins — a redactor derived from the credential markers on the type's registered schema. Every meta read door now withholds a webhook's `secret` and `headers`, with or without `@objectstack/plugin-webhooks` mounted (ruling B+ on #24966).
6+
7+
**BREAKING:** the `/meta` read doors no longer serve a webhook's `secret` or `headers`.
8+
9+
Clause-②: no (narrowing) — the `/meta` read doors return less for `webhook`: a stored `secret` or non-empty `headers` map is dropped from the served item and named in `redactedKeys`; the cleared forms (an empty string, an empty map) are served as written. A save that omits the two fields keeps the stored values (the metadata protocol's carry-forward); to remove a credential, save its cleared form. The dispatcher reads the declaration itself, so delivery signing is unchanged.
10+
11+
A metadata type that stores a credential in a declared field marks it on its schema instead of registering a redactor; a spec ratchet reds on a credential-named field that carries no marker.
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) no authorable key, export or stored shape is removed or renamed; the read doors serve less, and a stored webhook needs no migration -->
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a failed flow run can carry the refusal its write node met — `AutomationResult.userMessage` / `AutomationResult.innerRefusal`, and one `FlowFailureDetailsSchema` for every door's `400 FLOW_FAILED` details
6+
7+
Clause-②: yes (widening)
8+
9+
The maintainer's 「同意」 on ruling B′ of #24914 (decision batch #379 item 5): the inner refusal of a refused flow write node reaches the client structurally, the way a record save's refusal does, and its code takes the wire spelling. This release is the contract half. The engine and the four doors start emitting the new members when the services and runtime half (#24914) lands; until then nothing on the wire changes.
10+
11+
- **`AutomationResult` (`@objectstack/spec/contracts`) gains two optional members.** `userMessage` has `ApiErrorSchema.userMessage`'s semantics: it is producer-marked end-user text, present only when the inner refusal's producer opted in at throw time, and it never replaces `error`. `innerRefusal` is `{ code, message }`. Its `code` is the wire spelling a record save of the same write answers in `error.code` (`UNIQUE_VIOLATION` for a unique refusal), parsed against the same closed `ErrorCode` set `ApiErrorSchema.code` uses. `TriggerFlowResponseSchema.data` mirrors both, so the `TriggerFlowResponse['data']` = `AutomationResult` parity pin holds.
12+
- **New `FlowInnerRefusalSchema` and `FlowFailureDetailsSchema` (`@objectstack/spec/api`).** The second one is the `error.details` of the `400 FLOW_FAILED` arm of the trigger, resume, `/actions` and endpoint doors, declared once: `errorMessage?`, `summary?` and `innerRefusal?`. On the resume door, `ResumeFailureDetailsSchema`'s `runId` / `status` / `repairable` sit beside them in the same object, and each schema parses its own members.
13+
- **The #9585 `/actions` ceiling, relaxed by the ruling.** That ceiling was exactly two keys in `error.details` (`errorMessage`, `summary`) and no new schema key. Ruling B′ adds one key and names it, `innerRefusal`, so the ceiling is now the three keys `FlowFailureDetailsSchema` declares, at every door.
14+
- **Unchanged.** `error.message` keeps each door's wording byte for byte, `error.code` stays `FLOW_FAILED`, and `AutomationResult.code` stays the closed union the #9378 ruling settled. The inner code is nested in `innerRefusal`, so no new ledger code is added. Every new member is optional: an envelope without them parses exactly as before.
15+
16+
**For consumers.** A flow runner can show a failed run's refusal the way a record form shows a save's refusal. Read the envelope's top-level `userMessage` when it is present. Otherwise read `details.innerRefusal` (`code` and `message`), and fall back to `message` last. A client running behind the server ignores the new members and keeps its current behaviour.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/plugin-security': minor
4+
---
5+
6+
feat(objectql, plugin-security): the engine composes its `ReadableIds` leaf for a delegated (on-behalf-of) caller, scoped to the delegator intersection, instead of refusing it (ADR-0139 D5.1 as amended; O5 answered)
7+
8+
Clause-②: no
9+
10+
**Before.** A read carrying an on-behalf-of link whose final `where` held a `ReadableIds` leaf was refused `INTERNAL_ERROR` / 500 before any driver was asked (ADR-0139 D5.1 as accepted), and `getReadFilter` answered the deny sentinel for every delegated context, so the leaf's consumers routed such a caller to their bounded, fail-closed probe.
11+
12+
**Now.**
13+
14+
- `@objectstack/objectql`: the executor boundary composes the leaf for a delegated caller exactly as for any other. It asks `canReadObject` and `getReadFilter` with the delegated context as it stands (`onBehalfOf` intact) and composes what the security service answers; it derives no intersection of its own. The two remaining D5 classes, a parent an object-scoped read middleware narrows and a parent bound to another datasource, are still refused by name with the same envelope.
15+
- `@objectstack/plugin-security`: `getReadFilter` answers a delegated context the ADR-0090 D10 intersection, the six layers the engine middleware ANDs into a delegated `find` of the same object: the principal's row-level security and `controlled_by_parent` derivation, the delegator's two over the delegator's own rebuilt context, and the sharing visibility filter under each identity at its own depth. A delegated read through the leaf serves exactly the rows a delegated direct read of the parent serves. It fails closed as the chain does: a delegator who does not exist, a delegator the object does not admit, a principal or a delegator that resolves no permission set, and any resolution failure answer the deny sentinel, and the leaf is `$in: []`.
16+
17+
**What a reader sees.** No shipped consumer writes a leaf for a delegated caller yet (each still routes it to its probe; that routing retires per consumer on its own card), so a delegated read of `sys_activity`, `sys_audit_log`, `sys_comment`, `sys_comment_reaction` or `sys_attachment` is served as before. A door that binds `getReadFilter` directly, such as the analytics raw-SQL path, scopes a delegated context to the intersection now; as accepted it denied that context outright.
18+
19+
**Unchanged.** `controlled_by_parent`'s own read scope (ADR-0055 §2 mechanism (b), the pre-resolved id set) and the nested master chain, which keep their own cards; the delegated `find`, `findOne`, `count` and `aggregate` through the engine middleware, which already applied the intersection.

‎content/docs/deployment/cli.mdx‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ predicate/schema/binding mistakes that fail silently at runtime), and `os dev --
8282
| `os dev [package]` | | Start development mode with hot reload |
8383
| `os serve [config]` | | Start the ObjectStack server with plugin auto-detection |
8484
| `os db clean` | | Reclaim SQLite free space with a one-time `VACUUM` (ADR-0057) |
85+
| `os db orphans` | | Report detail rows whose required master-detail master does not exist; delete them only with `--delete` |
8586

8687
#### `os init`
8788

@@ -423,6 +424,48 @@ os db clean --database file:./data/app.db # explicit target
423424
**Options:**
424425
- `-d, --database <url>` — SQLite database URL/path (defaults to `$OS_DATABASE_URL`, then the per-project dev DB)
425426
427+
#### `os db orphans`
428+
429+
Reports the detail rows whose **required** `master_detail` master does not exist, and —
430+
only behind `--delete` — deletes them. **Report-only by default: without `--delete` it
431+
writes no row or table and creates no file.** It is never run for you; nothing on any
432+
boot, upgrade, migrate or lifecycle path invokes it.
433+
434+
Such a row is written by an elevated (system-context) writer, which the write-time
435+
reference check exempts. Its access derives from a master that does not exist, so no
436+
user door reads or deletes it — an administrator's `GET` and `DELETE` answer 404 — and
437+
the platform's integrity audit reports it without ever rewriting it. This command is
438+
where an operator acts on that report.
439+
440+
The report lists each orphan's object, record id, field and missing master id, and the
441+
deeper detail rows its delete removes with it (the master-detail's own cascade). It
442+
also names, without ever deleting them, the dangling references on those objects that
443+
are not a required master-detail, and counts the references whose target could not be
444+
checked at all: unknown is not missing, and none of those rows is listed or deleted.
445+
446+
```bash
447+
os db orphans # report (writes no rows)
448+
os db orphans --json # the same report, machine-readable
449+
os db orphans --delete --export ./orphans-backup.json
450+
```
451+
452+
**Options:**
453+
- `--delete` — delete the listed orphans and their cascade (default off)
454+
- `--export <path>` — **mandatory with `--delete`**: every record the run deletes, as stored, written
455+
owner-only and read back before the first delete; refuses to overwrite an existing file
456+
- `--max-rows <n>` — rows read per object and in total (default 100000); the report names every object it
457+
could not read in full
458+
- `-y, --yes` — skip the confirmation prompt
459+
- `--database-url <url>`, `--json`
460+
461+
`--delete` deletes only what the same run listed. Just before deleting it reads the
462+
report again and skips an orphan whose master now exists, and it skips one whose cascade
463+
now reaches a record the export does not hold. Each delete is the engine's own
464+
system-context delete, so a `restrict` dependent refuses it (reported as failed, with
465+
nothing of that orphan removed) and a `set_null` reference to a deleted row is cleared.
466+
A roll-up on the missing master cannot be recomputed, and the run says so. Keep the
467+
export until you are certain the sweep was right: it is the only copy.
468+
426469
#### Console UI
427470
428471
Launch the development server with the Console UI:

0 commit comments

Comments
 (0)