Skip to content

Commit 1e057bc

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14423-loadmany-consumer-census
2 parents 03785cf + 95464ed commit 1e057bc

41 files changed

Lines changed: 2905 additions & 203 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/cli-lint-conversion-notices.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ and their metadata stopped loading.
5252
| face | before | after |
5353
| --- | --- | --- |
5454
| console (`os lint`) | nothing | one `` line per notice: the path, `'from'``'to'`, the conversion id, and the protocol major it retires in |
55-
| `os lint --json` | no such key | `conversions`: the same structured notices, unconditionally present |
56-
| `os lint --json`, thrown / caught | no such key | what the run had computed — `[]` for a throw at load |
55+
| `os lint --json` (project lint) | no such key | `conversions`: the same structured notices, present on every exit of this mode |
56+
| `os lint --json` (project lint), thrown / caught | no such key | what the run had computed — `[]` for a throw at load |
57+
| `os lint --eval --json` | no such key | **still no such key** — out of scope here, see below |
5758

5859
The console wording is `compile.ts`'s, verbatim, so an author who runs two of
5960
the three commands over one tree is told the same thing in the same words. The
@@ -63,18 +64,30 @@ consumer reads all three authoring commands the same way.
6364

6465
## What a consumer should know
6566

66-
`conversions` is **always an array** on `os lint --json`, success or
67-
failure, so it can be read unconditionally. Each entry keeps its structured
68-
`conversionId`, `surface`, `from`, `to`, `path`, `toMajor` and `retiresIn`
69-
fields, so a CI job can gate on `retiresIn` without a second run.
67+
`conversions` is **always an array** on the **project-lint** `--json`
68+
payloads — the report exit and the caught-error exit, success or failure — so
69+
a consumer of plain `os lint --json` can read it unconditionally. Each entry
70+
keeps its structured `conversionId`, `surface`, `from`, `to`, `path`,
71+
`toMajor` and `retiresIn` fields, so a CI job can gate on `retiresIn` without
72+
a second run.
73+
74+
**`os lint --eval --json` does not carry the key**, and this change did not
75+
add it there. `--eval` scores a generation corpus instead of loading the
76+
project, so it never reaches the conversion layer; both of its JSON exits —
77+
the eval report, and the `--generator` load failure — publish no `conversions`.
78+
That is the whole exception: `lint.ts` has four `--json` exits, the two
79+
project-lint ones carry the key and the two `--eval` ones do not. A consumer
80+
that runs both modes must guard the key on the `--eval` path (or branch on the
81+
mode it asked for) — `payload.conversions.length` is a `TypeError` there.
7082

7183
`conversions: []` does **not** mean "this tree converts nothing" on the
7284
caught-error payload — it means the run stopped before the conversion layer
7385
ran. A config that fails to load reports `[]` by construction. Read the
7486
`error` key to tell the two apart.
7587

76-
⛔ A consumer asserting an exact key set on `os lint --json` must add
77-
`conversions` to it. No existing key changed: `total`, `errors`, `warnings`
88+
⛔ A consumer asserting an exact key set on the project-lint
89+
`os lint --json` payload must add `conversions` to it; the `--eval` key sets
90+
are unchanged. No existing key changed: `total`, `errors`, `warnings`
7891
and `suggestions` count exactly what they counted before, and exit codes are
7992
untouched (errors still exit 1).
8093

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os generate migration` now gives a `multiple: true` field a JSON column, in both formats. One authored field used to produce two incompatible answers from one config in one run: `Field.lookup({ reference: 'account', multiple: true })` emitted `account?: string[]` from `os generate types` and a scalar `VARCHAR(36)` / `table.uuid('account')` column from the two migration generators, because `multiple` appeared exactly four times in `generate.ts` and all four were on the TypeScript side — `fieldTypeToSql` did not even take the parameter. Nothing warned: the scaffold looks right, the generated TypeScript IS right, and only the column is wrong, so the first symptom was a write of an array into a scalar column. That is the `#field-zoo` failure one layer out — there the DDL switch and `isJsonField` had drifted into two lists inside the driver; here the platform and the *generated* DDL were the two lists.
6+
7+
The authority is the driver's, and it is the flag alone. `SqlDriver.createColumn` short-circuits on `field.multiple` **above** its own `switch (type)`; `isJsonField` is `JSON_COLUMN_TYPES.has(type) || !!field.multiple`; and `fieldHasColumn` opens with `if (field?.multiple) return true` under the comment "Mirrors `SqlDriver.createColumn` exactly … including `multiple` (a JSON column)". Three statements of one rule: a flagged field is a JSON column whatever its element type would have been. Both generators now answer it the same way and in the same place — before the type is consulted at all.
8+
9+
Deliberately **not** the spec's `isMultiValueField`. That predicate is the ADR-0104 D1 *value* contract ("is the persisted value an array") and gates on `MULTI_CAPABLE_TYPES`, so asking it here would answer `VARCHAR` for a `text` field flagged `multiple: true` while the driver gives that same field a JSON column — the identical drift one notch narrower. `FieldSchema` does not refuse the combination either (`multiple` is a plain `z.boolean()` on every field; only `radio` + `multiple` is refused by name), and the generators sit downstream of validation. The two questions have two different owners: the value shape is the spec's, the column is the driver's.
10+
11+
Nothing about the existing per-type vocabularies changes. The scalar answers — including the five that are separately disputed — are byte-for-byte what they were, and a new pin asserts that as a scope fence rather than leaving it to a reading of the diff. `generate-multiple-json-column.pin.test.ts` drives all three generators on one config and pins the agreement across every member of the spec's `MULTI_CAPABLE_TYPES` plus a type outside it, so the type-blindness of the rule is an assertion rather than a comment; it also reads the driver's two statements of the rule, so moving them there fails here.

.changeset/hook-run-as-inherit.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/lint": patch
5+
---
6+
7+
feat(hooks): `runAs` on a hook — `'system' | 'user' | 'inherit'`, default `'inherit'`
8+
9+
A hook's `ctx.api` runs with the context of the write that fired it, so a column
10+
an app wants **computed and never hand-written** could not be expressed: author
11+
`editable: false` for the persona and the direct `PATCH` is refused — and so is
12+
the hook that maintains the column, by the same field-level check. The guard and
13+
the legitimate writer were the same door. The only elevation a hook had was the
14+
in-process `ctx.api.sudo()`, which is not marshalled into the sandbox (a
15+
`TypeError` once a build lowers the handler into a body) and which rides the L3
16+
bundle path that is being retired.
17+
18+
`HookSchema` now accepts `runAs`:
19+
20+
| value | the hook's `ctx.api` data operations run as |
21+
| --- | --- |
22+
| `'inherit'` (default) | the context of the triggering write — exactly the behaviour every hook has today |
23+
| `'system'` | elevated: a full-access, RLS-bypassing system principal |
24+
| `'user'` | the triggering user; a hook whose trigger resolved no user has its data operations **refused** (`HOOK_UNSCOPED_DATA_ACCESS`) rather than run unscoped |
25+
26+
`'system'` and `'user'` mean here exactly what they mean on `flow.runAs` — same
27+
word, same semantics. `'inherit'` is the hook-only third value, because only a
28+
hook has a context to inherit; a flow establishes its identity from nothing,
29+
which is why its default is `'user'` and this one's is `'inherit'`. Nothing on
30+
`FlowSchema` changes.
31+
32+
**Purely additive: no migration, no behaviour change for any existing hook.**
33+
The default reproduces today's behaviour by handing the engine-built `ctx.api`
34+
through unchanged, and an absent key parses to it.
35+
36+
Scope, deliberately narrow: `ctx.api` data operations only. `condition`
37+
evaluation, the `readonly` strip applied to the hook's own `ctx.input` payload,
38+
`ctx.session` and `async` semantics all keep reading the triggering operation's
39+
context, and declaring `runAs: 'system'` does not elevate the write that fired
40+
the hook.
41+
42+
Elevation is authorization, not anonymity: a `runAs: 'system'` write still
43+
carries the triggering user, so `created_by` / `updated_by` and the audit row
44+
still name the operator.
45+
46+
Honoured on both execution surfaces — the in-process `handler` and the
47+
sandboxed `body`.
48+
49+
Authoring notes:
50+
51+
- `sudo`, `elevate`, `elevated` and `isSystem` are refused with a prescription
52+
naming `runAs`, and `run_as` is answered as a rename.
53+
- `@objectstack/lint`'s gating `hook-api-update-readonly-field` rule now skips a
54+
hook that declares `runAs: 'system'` — the static `readonly` strip skips a
55+
system context, so the write it exists to catch does not happen — and its
56+
hints name the knob. The `readonlyWhen` warning is unchanged: a system context
57+
does not waive a conditional lock.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): the three recovery doors run the ADR-0094 mutation projector
6+
7+
`rollbackMetaItem`, both limbs of `revertCommit`, and `deleteMetaItem`'s
8+
legacy raw-engine exit restored the metadata row and the in-memory registry
9+
but never called the awaited ADR-0094 mutation projector — so a derived
10+
read-model (e.g. `permission` -> `sys_permission_set`) stayed on the
11+
rolled-back-FROM state until an unrelated save/publish/delete on the same
12+
name, or a boot reconciliation, re-derived it. `saveMetaItem`,
13+
`runPublishSideEffects`, and `deleteMetaItem`'s repository branch already ran
14+
this hook; the three recovery doors were the same gap the prior card closed
15+
for the mutation-event choke point, one call site over.
16+
17+
All four sites now call `runMutationProjector`, awaited BEFORE the existing
18+
fire-and-forget `emitMetadataMutation` call — the order `saveMetaItem`'s own
19+
comment establishes: `rollbackMetaItem` and `revertCommit`'s restore limb
20+
project `state: 'active'` with the restored body; `revertCommit`'s
21+
soft-remove limb and `deleteMetaItem`'s legacy exit project
22+
`state: 'deleted'`, the same call `deleteMetaItem`'s repository branch
23+
already makes. `deleteMetaItem`'s legacy exit now also carries
24+
`projectionApplied` on its success return, the same optional key its
25+
repository-branch sibling has declared since ADR-0094 shipped.
26+
27+
Internal only: `runMutationProjector` is a private, best-effort,
28+
already-registered hook (never thrown, logged on failure) — no published
29+
schema or wire shape moves. ADR-0094 D2's door enumeration is amended in a
30+
companion `docs/adr/**` PR.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(platform-objects): the zh-CN `dashboard` metadata-form subtree says what its source says
6+
7+
Five leaves of `metadataForms.dashboard` in `zh-CN.metadata-forms.generated.ts`
8+
described the Studio dashboard property panel in terms the source form
9+
(`packages/spec/src/ui/dashboard.form.ts`) does not use, each one naming a
10+
concept the source omits while dropping one it names:
11+
12+
| leaf | was | now |
13+
| --- | --- | --- |
14+
| `sections.layout.description` | 「栅格与响应式」 | 「栅格尺寸与刷新间隔」 |
15+
| `sections.basics.description` | 「名称与图标」 | 「仪表板标识与描述」 |
16+
| `sections.widgets.description` | 「图表、指标、列表等」 | 「放置在栅格上的卡片与图表」 |
17+
| `sections.filters.description` | 「全局筛选与日期范围」 | 「应用到所有组件的默认筛选与全局筛选」 |
18+
| `fields.header.helpText` | 「标题、操作按钮与筛选」 | 「仪表板页眉配置(title、subtitle、actions)」 |
19+
20+
Two of them sent a zh-CN author looking for a control that does not exist.
21+
`layout` promised a responsive-breakpoint input in a section holding `columns`,
22+
`gap`, `refreshInterval` and `header`, and left the refresh cadence unnamed.
23+
`basics` promised an icon input; that section holds `name`, `label` and
24+
`description`, and the dashboard schema declares no icon key at all — the only
25+
`icon` under `dashboard.zod.ts` is per header action. The other three swapped
26+
which concepts the section covers: widgets is cards and charts on the grid, not
27+
metrics and lists; filters is default plus global filters across widgets, not
28+
the date range (that is the separate `dateRange` field); and the header carries
29+
a subtitle, not filters. The replacements reuse terms this bundle already
30+
establishes — `grid` → 「栅格」, `widgets` → 「组件」, `identity` → 「标识」, and
31+
the literal key list rendered as `(title、subtitle、actions)` the way
32+
`page.fields.layout.helpText` renders `(header、main、sidebar、footer)`.
33+
34+
The source strings have not moved since the form was created: all five have been
35+
byte-identical across that file's only two commits. zh-CN's values were carried
36+
in from the pre-consolidation `metadata-translations/zh-CN.ts` overlay and never
37+
reconciled against the extracted English, while es-ES and ja-JP were translated
38+
from it and are correct at all five. So this is a translation correction, not a
39+
re-sync after a source edit — nothing else in the bundle, the source form, or
40+
the recorded source-hash companion changes.

0 commit comments

Comments
 (0)