Skip to content

Commit 8a65474

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22677-flow-cel-record-entrance
2 parents 641f940 + 762db99 commit 8a65474

34 files changed

Lines changed: 2476 additions & 230 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the list-view sort and search, form-predicate path, bulk-dispatch, component-type and preset-comparand findings print one verdict line, and `os explain <rule-id>` carries their reasoning
6+
7+
Clause-②: no
8+
9+
- **Shorter verdicts.** Each finding of these 12 rule ids now prints a `message` of one verdict sentence. Every finding the rules' own test suites fire is at most 193 characters, and the runtime publish gate's suites at most 137; before, the longest of each ran from 287 to 927 characters. The ids:
10+
- list-view `sort` (`objects[].listViews`, `views[]` lists, list overlays and ViewItem records): `sort-field-unknown`, `sort-field-unsortable`, `sort-field-unprovisioned`;
11+
- `searchableFields` (the object's own set, list views, and a react page's `<ListView searchableFields>`): `searchable-field-unknown`, `searchable-field-unsearchable`, `searchable-field-unprovisioned`;
12+
- metadata-form `visibleWhen` predicates (`views[]` forms bound to a schema): `predicate-path-unresolved`, `predicate-path-unrooted`, `predicate-rhs-path-shaped`;
13+
- list-view bulk wiring: `action-dispatch-contract-mismatch`;
14+
- page component types (`pages[]`): `component-type-unknown`;
15+
- filter comparands: `filter-preset-comparand`.
16+
17+
A verdict no longer repeats what the finding's `where` already names: the view that wires an action (`action-dispatch-contract-mismatch`). The unprovisioned-anchor ids print the same one-clause cause the other converted anchor rules print (`'owner_id' is an injected column with no storage on external object 'x'`), and the two virtual-entry ids (`sort-field-unsortable`, `searchable-field-unsearchable`) state the storage fact in one shared wording. `searchable-field-unsearchable` quotes at most three names of an object's declared set, then `(and N more)`. A retired component type (`user:profile`, `element:filter`, `element:form`, `ai:chat_window`) is quoted to the head of its prescription in `RETIRED_PAGE_COMPONENT_TYPES` (`` `element:filter` was removed in @objectstack/spec 17 (ADR-0049) ``), and the verdict says the parse refuses the node by name; the whole prescription is the parse door's refusal of the same name, which `os validate` and `os build` print. `filter-preset-comparand` opens with the first sentence of the refusal the schema door shares (`"last_30_days" is a dashboard date-range PRESET name, not a filter value`), then names the operator and the preset's `{date-macro}` window. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
18+
- **`os explain <rule-id>` takes these 12 ids**, for example `os explain sort-field-unknown`. It prints the reasoning the verdicts no longer carry: why an unknown sort field breaks a view's first fetch and every load after it, and which list-view surfaces the sort and search rules walk and skip; what a `formula` field's lack of storage does to an ORDER BY and to a search; what an unprovisioned anchor is and what sorting or searching one measured; how a stale `searchableFields` entry narrows a search or falls through to the auto-default set, and how a list view's narrowing reaches the runtime as the `$searchFields` override; how a metadata-form predicate path is resolved against the edited schema, why a dead predicate fails open, and why the right side of `==` / `!=` is a literal; how the two bulk wirings call an action and why nothing refuses a mismatch at run time; which component types the vocabulary closes and where a retired type's prescription lives; and where a date-range preset name is understood and what each layer does with a bare one. Paragraphs shared across ids are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 12 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 12 entries.
19+
- **Where the new text prints.** On the CLI, all 12 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` and `os generate` run print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP), the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change for: the three `searchable-field-*` ids on a `view`, `object` or `flow` write; the three `sort-field-*` ids on a `view` or `flow` write; the three `predicate-*` ids on a `view` write; and `filter-preset-comparand` on a `dashboard`, `view`, `object`, `page`, `flow` or `report` write. Each issue's `hint` is unchanged.
20+
- **Never at the runtime gate:** `component-type-unknown`, which runs on the CLI doors only, and `action-dispatch-contract-mismatch`, whose rule runs at that door only for a `flow` write, whose snapshot carries no actions to judge, so both speak only on the CLI doors above.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate plan` / `apply` plan each object in the database it lives in, the `telemetry` sibling included (#22579)
6+
7+
A development `os serve` boot on a file-backed SQLite database, and any boot with `OS_TELEMETRY_DB=<path>`, keeps lifecycle-classed system data (audit, telemetry and event objects such as `sys_audit_log`, `sys_activity`, `sys_metadata_audit`, `sys_job_run`, `sys_notification`) in a sibling `telemetry` database. The one-shot boot `os migrate` runs never opened that sibling, so every one of those objects resolved to the primary database: after a development boot, `os migrate plan` listed each as a table to create, and `os migrate apply` created each in the primary — empty tables beside the ones the served boot uses.
8+
9+
The migrate boot now provisions the sibling exactly when the serving boot would, through the same helper and under the same rule: `--dev` or `NODE_ENV=development` on a file-backed SQLite primary, or `OS_TELEMETRY_DB=<path>`, and never with `OS_TELEMETRY_DB=0`. `plan` and `apply` diff and apply every object against the database it lives in, and print the sibling under the database line (`Telemetry database: …`); in `--json` it is the new `telemetryDatabase` field, present only when a sibling is planned. `apply`'s confirmation names both databases. A `plan` against a sibling that does not exist yet lists its tables to create and creates no file. `os migrate unmapped-columns` reads a lifecycle-classed object from the sibling, and names it as the database.
10+
11+
A deployment with no sibling — production without `OS_TELEMETRY_DB`, or `OS_TELEMETRY_DB=0` — is unchanged. Run the migration with the `NODE_ENV` the deployment is served with: without `NODE_ENV=development` the plan describes a production `os serve`. Tables an earlier `os migrate apply` created in the primary for these objects are not removed.
12+
13+
This supersedes the "Known limit" in this release's `os migrate plan` / `apply` composition entry: the migration boot now provisions the `telemetry` database.
14+
15+
`os serve` loads the project's `.env*` files through the same function `os migrate` does; which files it reads, and in which mode, is unchanged.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
An import row answers a sandboxed hook's refusal in the hook's own words, not the sandbox debug wrapper
6+
7+
Clause-②: no
8+
9+
When a hook body refused a row during `POST /api/v1/data/:object/import` (or the async `/import/jobs` job), the row's `error` read `hook 'NAME' threw: Error: SENTENCE`, while `POST /api/v1/data/:object` and `/createMany` answered the same refusal as `SENTENCE`. The import runner now reads the row's sentence the way those routes do: the hook's sentence, unchanged, with any `code` the body declared still on the row. A hook body that crashes (for example with a `TypeError`) is not a refusal, and its row reads as it did before.

‎content/docs/data-modeling/schema-design.mdx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,8 @@ send you to the same fix, so either message is greppable back to this section.
167167
`os validate` reports `searchable-field-unknown`:
168168

169169
```text
170-
searchableFields entry "project_id.name" is not a field on object "task". The
171-
declaration is stale: searching it can never match, and the engine silently
172-
drops it — leaving a narrower search than declared, or the auto-default set once
173-
every entry is dropped.
170+
searchableFields entry "project_id.name" is not a field on object "task", so the
171+
engine drops it from the search.
174172
175173
hint: 'search' scans this object's own columns, so a related record's column
176174
cannot be a search target — expand the relation and search the related object,

‎content/docs/deployment/cli.mdx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,24 @@ Under `--json` the same answer is `database` plus `databaseSource`, whose `kind`
887887
`flag`, `process-env`, `env-file` (with its `variable` and `file`),
888888
`config-datasource` (with its `datasource`) or `default`.
889889
890+
The deployment's datasources are the serving boot's too. Where `os serve` keeps
891+
lifecycle-classed system data (audit, telemetry and event objects) in the `telemetry`
892+
sibling database — a development boot (`--dev`, or `NODE_ENV=development`) on a
893+
file-backed SQLite database, or any boot with `OS_TELEMETRY_DB=<path>` — `plan` and
894+
`apply` open that sibling too, and plan and apply those objects in it, never in the
895+
primary:
896+
897+
```text
898+
ℹ Database: data/app.db (OS_DATABASE_URL from .env)
899+
ℹ Telemetry database: data/app.telemetry.db (audit, telemetry and event objects — where os serve keeps them; OS_TELEMETRY_DB=0 turns it off)
900+
```
901+
902+
Under `--json` that is `telemetryDatabase`, present only when a sibling is planned.
903+
Run the migration with the `NODE_ENV` the deployment is served with: a plan run
904+
without `NODE_ENV=development` describes a production `os serve`, which keeps those
905+
objects in the primary unless `OS_TELEMETRY_DB` names a file. A `plan` against a
906+
sibling that does not exist yet lists its tables to create and leaves no file behind.
907+
890908
#### Nothing is applied before you confirm
891909
892910
Both commands boot your app to read its metadata. That boot writes no row and no

‎packages/cli/src/commands/migrate/apply.ts‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {
1919
bootSchemaStack,
2020
describeDatabaseSource,
21+
describeTelemetryDatabase,
2122
renderPlan,
2223
renderPendingSchemaWork,
2324
summarize,
@@ -202,7 +203,13 @@ export default class MigrateApply extends Command {
202203
// [#22581] The database this run reconciles and who named it, on every
203204
// payload below — the in-sync and refusal paths included, since "nothing
204205
// to apply" is only as good as the database it was read from.
205-
const target = { database: stack.dbLabel, databaseSource: stack.dbSource };
206+
// [#22579] …and the `telemetry` sibling lifecycle-classed objects are
207+
// reconciled in, when the serving boot keeps one.
208+
const target = {
209+
database: stack.dbLabel,
210+
databaseSource: stack.dbSource,
211+
...(stack.telemetryDatabase !== null ? { telemetryDatabase: stack.telemetryDatabase } : {}),
212+
};
206213

207214
// What the object set was composed from (#12938) — printed BEFORE the
208215
// in-sync early return below, not with the plan. "Already in sync" over a
@@ -212,11 +219,14 @@ export default class MigrateApply extends Command {
212219
// above it for the same reason.
213220
if (!flags.json) {
214221
printInfo(`Database: ${chalk.white(stack.dbLabel)} ${chalk.dim(`(${describeDatabaseSource(stack.dbSource)})`)}`);
222+
if (stack.telemetryDatabase !== null) printInfo(describeTelemetryDatabase(stack.telemetryDatabase));
215223
for (const note of stack.composition.notes) console.log(chalk.dim(` ${note}`));
216224
console.log('');
217225
}
218226

219-
const drift = await stack.driver.detectManagedDrift();
227+
// [#22579] Over every datasource this run reconciles: the primary and the
228+
// `telemetry` sibling, each entry applied where it was found.
229+
const drift = await stack.detectManagedDrift();
220230
const grouped = groupByCategory(drift);
221231
// Additive work the boot sync was held back from doing. Not drift — it
222232
// is what `initObjects` does on its own — but it IS a change to the
@@ -422,7 +432,10 @@ export default class MigrateApply extends Command {
422432
printWarning('Confirmation required. Re-run with --yes to apply, or use "os migrate plan" to preview.');
423433
return;
424434
}
425-
const ok = await confirm(chalk.bold(`\nApply ${totalIntended} change(s) to ${stack.dbLabel}? [y/N] `));
435+
const targets = stack.telemetryDatabase !== null
436+
? `${stack.dbLabel} and its telemetry database ${stack.telemetryDatabase}`
437+
: stack.dbLabel;
438+
const ok = await confirm(chalk.bold(`\nApply ${totalIntended} change(s) to ${targets}? [y/N] `));
426439
if (!ok) { printInfo('Aborted — no changes made.'); return; }
427440
}
428441

@@ -431,7 +444,7 @@ export default class MigrateApply extends Command {
431444
// just-created table matches metadata by construction, so the two sets
432445
// never overlap.
433446
const created = await stack.flushSchemaDdl();
434-
const { applied, skipped } = await stack.driver.applyMigrationEntries(drift, { allowDestructive });
447+
const { applied, skipped } = await stack.applyMigrationEntries(drift, { allowDestructive });
435448

436449
if (flags.json) {
437450
await emitJson({

‎packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ function runCli(argv: string[], dbFile: string): Promise<Run> {
179179
// The fixed key `serve-process.ts` hands its children, so no boot
180180
// mints and persists a crypto key into this runner's home directory.
181181
OS_SECRET_KEY: '0e2e'.repeat(16),
182+
// [#22579] The deployment the controls model keeps every object in
183+
// ONE database: their served-shape boots are in-process stacks, which
184+
// provision no `telemetry` sibling. The source entry pins
185+
// `NODE_ENV=development`, under which the commands' boot mirrors a
186+
// development `os serve` — one that keeps lifecycle-classed objects
187+
// (`sys_audit_log`, `sys_activity`) in a sibling — so the fixture
188+
// declares its own topology, as a deployment with no sibling does.
189+
OS_TELEMETRY_DB: '0',
182190
}),
183191
stdio: ['ignore', 'pipe', 'pipe'],
184192
},

‎packages/cli/src/commands/migrate/plan.boot-parity.integration.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ import Database from 'better-sqlite3';
4646
* and no object declares it. A new raw-DDL table fails this pin and has to be
4747
* named here with the same reason.
4848
*
49-
* Both sides run with `OS_TELEMETRY_DB=0`: a development boot's `telemetry`
50-
* sibling (ADR-0057 §3.6) is a second database the one-shot boot does not
51-
* provision — a known, separate gap — and the pin compares object sets on one.
49+
* Both sides run with `OS_TELEMETRY_DB=0`, so the pin compares object sets on
50+
* one database. A development boot's `telemetry` sibling (ADR-0057 §3.6) — a
51+
* second database both boots provision — is
52+
* `plan.telemetry-sibling.integration.test.ts`'s (#22579).
5253
*/
5354

5455
const HERE = dirname(fileURLToPath(import.meta.url));

0 commit comments

Comments
 (0)