Skip to content

Commit 6ed56d2

Browse files
committed
Merge origin/main into claude/issue-16094-liveness-dead-warns
# Conflicts: # packages/lint/src/lint-liveness-properties.test.ts
2 parents 64e0275 + fed0db8 commit 6ed56d2

56 files changed

Lines changed: 2674 additions & 295 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.

‎.changeset/19518-picklist-kind.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Clause-②: yes (widening)
1111

1212
- **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused.
1313
- **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source.
14-
- **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. This release does not resolve the reference. Until the runtime does, a picklist-bound field is served without options, and the liveness ledger grades the key `planned` and warns an author who writes it.
14+
- **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release.
1515
- **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package.
1616
- **Translation.** `TranslationData` gains `picklists.<name>.{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them.
1717
- **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES).
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/metadata': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(objectql): the runtime resolves a field's `picklist` onto its served options, validates writes against the resolved list, and merges `picklistExtensions` additively
8+
9+
Clause-②: no
10+
11+
- **Load.** `defineStack({ picklists })` and `defineStack({ picklistExtensions })` now register, from a manifest and from a nested plugin, through the same registration seam as every other collection. The compiled-artifact door registers `picklists` as `picklist` items, so `GET /meta/picklist` serves them on an artifact boot.
12+
- **Merge.** A picklist's options are its own, followed by the options every `picklistExtensions` entry adds. A value the list already carries is refused with `422 INVALID_METADATA`, which names both declarations, whichever of the two registered first. The later declaration never replaces the earlier one. A package that registers again replaces its own extension. Uninstalling a package removes the values it added.
13+
- **Serve.** A field with `picklist: 'NAME'` is served with the resolved options written onto it and `picklist` kept (`PicklistServedFieldSchema`), on every object read, including objects stored in `sys_metadata`. The list's translations (`picklists.NAME.options.VALUE`) relabel those options per request locale. An option marked `default: true` in the list fills an omitted field on insert, as an inline option does, and the import template reads it the same way.
14+
- **Unknown name.** A packaged field that names a picklist no loaded package declares fails the boot at `kernel:ready` with `INVALID_METADATA`, and so does a `picklistExtensions` entry that extends such a list. The error names every such field or extension and the package that declared it. After boot, an artifact registered through the `manifest` service is checked before any of it registers. A field whose list does not resolve is served with no options and accepts no value.
15+
- **Write validation.** The write door judges a picklist-bound field against the resolved options, and its refusal names the picklist. The wire code stays `invalid_option`. The validation message catalog gains three message keys for this (`invalid_option_picklist`, `invalid_option_value_picklist`, `invalid_option_picklist_unresolved`) in en, zh-CN, ja-JP and es-ES. They change the message text only, never the wire.
16+
- **Writing the served body back.** The served body carries `picklist` and `options` together. Writing it back through the metadata door is still refused, with the prescription to drop `options`, as `FieldSchema` declares. Nothing strips it on the write side.
17+
- **Ledger.** `field.picklist`, the `picklist` kind's rows and `translation.picklists` are `live`. `field.picklist` no longer carries `authorWarn`, so `os lint` / `os validate` stop warning an author who writes it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): a book tree's synthetic *Uncategorized* group holds only the unplaced docs of the book's own packages, per ADR-0046 §6.4
6+
7+
Clause-②: no
8+
9+
- `resolveBookTree` used to put every unclaimed doc it was handed into the book's *Uncategorized* group. `GET /api/v1/meta/book/:name/tree` resolves over every doc in the environment, so a book's tree listed every other package's ungrouped docs there. The docs portal never showed those docs in the book.
10+
- The group now holds a doc only when it belongs to one of the book's packages: the package that ships the book, or a package a group names with `package`. A doc with no stamped package still counts as the book's. A doc of another package stays reachable through its own package's book.
11+
- The implicit per-package book that the tree route serves for a package id catches no other package's docs either.
12+
- Unchanged: a doc whose `group` names one of the book's groups joins that group from any package. A book that declares no package keeps every unclaimed doc in *Uncategorized*. The docs a book claims, and so every doc's audience, do not change.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `ObjectSchema.fields` refuses `constructor` and `prototype` at the offending key (`fields.constructor`, `fields.prototype`), not at the `fields` slot
6+
7+
Clause-②: no
8+
9+
A field map carrying a key named `constructor` or `prototype` is refused, as before. The
10+
refusal used to be reported at the path `fields`, which names no field, so a form reading the
11+
structured issues of a refused save could not point at the field that caused it. It is now
12+
reported at that key, like the `__proto__` refusal (`fields.__proto__`) and the key grammar's
13+
`invalid_key` refusal (`fields.Bad Name`) already are. A document carrying both names gets two
14+
issues, one at each key, where it used to get one at the slot.
15+
16+
Nothing else moves. The same keys are refused and the same documents are accepted. The issue
17+
code (`custom`) and the message are unchanged. The published JSON Schema states the same ban:
18+
it is now written as one `propertyNames` clause per name inside `allOf` instead of one clause
19+
naming both, which accepts and refuses exactly the same documents.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): an object's embedded `listViews` are served in the reader's language, from the `_views` keys `os i18n extract` already writes
6+
7+
Clause-②: no
8+
9+
- `GET /api/v1/meta/object` and `GET /api/v1/meta/object/:name` now translate each view in an object's `listViews`: its `label`, its `description`, and the copy of its `bulkActionDefs` (label, confirm prompt, confirm button, and each param's label, help and placeholder). They read the keys `os i18n extract` writes for those views, `objects.<object>._views.<view>.*`, where `<view>` is the view's key under `listViews`. For example, `sys_account`'s `mine` view is now served as `我的链接` to a `zh-CN` reader. It used to be served as the authored `My Links`.
10+
- A view with no translation for the requested locale keeps its authored text. An object with no `_views` entries is served unchanged.
11+
- A string that was changed after the package shipped still wins over the packaged translation, the same rule a served view document and a dashboard follow. The string is compared with the same view in the packaged object. If it differs, the changed string is served in every locale. A view the packaged object does not declare keeps all of its own strings.
12+
- `translateObject` (and `translateMetadataDocument('object', …)`) in `@objectstack/spec/system` does the translating, so any caller of those functions gets the same result.

‎.gitattributes‎

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -86,38 +86,53 @@
8686
# merge-and-regenerate round. The cure is the one the header above records for
8787
# the three hottest artifacts; each gate sums its shards when it reads them.
8888
#
89-
# The elevation census page joined at #13646 — a generated `file:line` anchor
90-
# table whose correct merged values are on NEITHER side of a conflict (measured on
91-
# #13625: five conflicted anchors resolved to 4408/5771/6019/6382/6575 against
92-
# branch 4407/5770/… and main 4284/5647/…), so no text merge and no hand merge can
93-
# reach them. ⚠️ It is routed as the FILE and NOT as `content/docs/permissions/**`:
94-
# unlike `content/docs/references/**` above, which is generated whole, that
95-
# directory is 22 hand-written prose pages around one generated one, and the glob
96-
# would defer the prose to OURS. See NOT_DRIVER_MANAGED for that entry.
89+
# The elevation census page joined at #13646, when it was a generated `file:line`
90+
# anchor table whose correct merged values were on NEITHER side of a conflict
91+
# (measured on #13625: five conflicted anchors resolved to 4408/5771/6019/6382/6575
92+
# against branch 4407/5770/… and main 4284/5647/…), so no text merge and no hand
93+
# merge could reach them. ⚠️ It is routed as the FILE and NOT as
94+
# `content/docs/permissions/**`: unlike `content/docs/references/**` above, which
95+
# is generated whole, that directory is 22 hand-written prose pages around one
96+
# generated one, and the glob would defer the prose to OURS. See
97+
# NOT_DRIVER_MANAGED for that entry.
9798
#
9899
# This is also the row where the header's own warning is answered rather than
99100
# accepted: `scripts/check-system-context-census.mjs` still reddens on every PR from
100-
# the required `Lint & Repo Gates` job — it RE-DERIVES the census from the tree, so
101-
# it catches the stale anchors a merge leaves behind even when nothing conflicted,
102-
# which is the majority case (#13625: 18 anchors stale, 5 marked). The driver removes
103-
# hand-merge rounds; it is never the only signal.
101+
# the required `Lint & Repo Gates` job — it RE-DERIVES the census from the tree.
102+
# While the page carried line anchors, that is what caught the stale anchors a merge
103+
# left behind even when nothing conflicted, which was the majority case (#13625: 18
104+
# anchors stale, 5 marked). The driver removes hand-merge rounds; it is never the
105+
# only signal.
104106
#
105107
# ⚠️ #14064 CORRECTED the sentence that used to open that paragraph — "deferring is
106-
# safe here BECAUSE the census gate re-derives". The gate re-derives the census and
108+
# safe here BECAUSE the census gate re-derives". The gate re-derived the census and
107109
# the anchors. It re-derives no PROSE, because prose is derived from nothing, and
108110
# this page is the one routed path that carries both. So the argument was true and
109111
# its domain was half the risk surface: the driver drops a side WHOLE, and on this
110-
# page that side can carry hand-written paragraphs the gate is constitutionally
111-
# unable to miss. Measured, not reasoned — deleting a 3-line anchor-free paragraph
112-
# and running ten doc-family gates returned ten exit 0 over deleted documentation.
113-
#
114-
# Routing this file is still RIGHT (the correct anchors are on neither side; nothing
115-
# above changes). What #14064 added is the missing half: the row now declares
116-
# `mixed: 'line-anchors'` in scripts/regen-artifacts.mjs, and the driver refuses to
117-
# defer SILENTLY when the incoming side carries anything but anchor numbers — it
118-
# text-merges instead, and conflicts loudly if that cannot be done. The cheap case
119-
# stays cheap: of the last 25 main commits to this page, 24 changed nothing but
120-
# anchor numbers.
112+
# page that side can carry hand-written paragraphs whose loss the gate is
113+
# constitutionally unable to notice. Measured, not reasoned — deleting a 3-line
114+
# anchor-free paragraph and running ten doc-family gates returned ten exit 0 over
115+
# deleted documentation.
116+
#
117+
# #14064 kept the routing (the correct anchors were on neither side) and added the
118+
# missing half: the row declares `mixed: 'line-anchors'` in
119+
# scripts/regen-artifacts.mjs, and the driver refuses to defer SILENTLY when the
120+
# incoming side carries anything but anchor numbers — it text-merges instead, and
121+
# conflicts loudly if that cannot be done. The cheap case stayed cheap: of the last
122+
# 25 main commits to this page when that was measured, 24 changed nothing but anchor
123+
# numbers.
124+
#
125+
# Today (since #15921) the page carries `path#symbol` anchors and no line numbers,
126+
# so the `line-anchors` comparator is the IDENTITY on it (measured on #16612:
127+
# `extractLineAnchors(page).length === 0`, `blankAnchorLineNumbers(page) === page`).
128+
# Every real edit therefore takes the text-merge branch, or conflicts loudly, by
129+
# design; the renumbering case is gone because there is nothing left to renumber.
130+
# What the census gate catches after a merge is now a symbol anchor that no longer
131+
# resolves, a population that no longer matches, or a declared count that no longer
132+
# equals the census. ⛔ The `mixed` field stays: it is what routes this row through
133+
# the lossless check at all, and deleting it sends the row to the unconditional
134+
# deferral, the silent deletion #14064 closed (reproduced on #16612). The row's
135+
# comment in scripts/regen-artifacts.mjs carries the measurements.
121136

122137
#
123138
# #13731 enumerated this file's blind spot and closed it: `check:merge-driver` now

‎.github/workflows/ci.yml‎

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,8 +1632,8 @@ jobs:
16321632
restore-keys: |
16331633
${{ runner.os }}-pnpm-store-v3-
16341634
1635-
# Shard-scoped key: the turbo test hash differs per shard (pass-through
1636-
# args are part of the task hash). Restore-only on PRs — see the Restore
1635+
# Shard-scoped key: the turbo test hash differs per shard (the task-declared
1636+
# env `OS_TEST_SHARD` is part of the task hash). Restore-only on PRs — see the Restore
16371637
# Turbo cache comment in the test job; the save step at the end of this
16381638
# job seeds from main only. The job-level catch-all (same as Test Core's)
16391639
# is what keeps a NEW shard number warm before main has ever saved it:
@@ -1653,13 +1653,16 @@ jobs:
16531653
run: pnpm install --frozen-lockfile
16541654

16551655
# ⛔ THE DOGFOOD PACKAGE'S DEPENDENCY CLOSURE IS BUILT HERE, IN A RUN
1656-
# THAT CARRIES NO PASSTHROUGH, so that the sharded run in the next step
1657-
# can be `--only` (#16395's shape, applied to this job by #16886).
1658-
#
1659-
# Turbo folds a run-level passthrough into the hash of EVERY task in the
1660-
# run, not only the task that receives it -- and `-- --shard=k/3` is the
1661-
# whole reason each shard gets its own invocation at all (the next step's
1662-
# comment says what those args are for). Measured on turbo 2.10.10,
1656+
# THAT CARRIES NO PASSTHROUGH. (#20820: the next step no longer needs this
1657+
# for correctness -- it carries its shard in the task env `OS_TEST_SHARD`,
1658+
# so it plans and replays this same closure itself, with no `--only`. The
1659+
# step stays as its own stall-guard site, and the closure replays there.
1660+
# The measurement below is the history of the `-- --shard=k/3` shape.)
1661+
#
1662+
# (History.) Turbo folds a run-level passthrough into the hash of EVERY
1663+
# task in the run, not only the task that receives it -- and the old leg's
1664+
# `-- --shard=k/3` was the whole reason each shard got its own invocation
1665+
# (the next step's comment says what carries the slice now). Measured on turbo 2.10.10,
16631666
# `--filter=@objectstack/dogfood`, `turbo run test ... --dry=json`
16641667
# (67 tasks: 66 `build` + 1 `test`):
16651668
#
@@ -1704,11 +1707,16 @@ jobs:
17041707
# Boots real example apps in-process (in-memory SQLite) and exercises them
17051708
# through the real HTTP + service stack — catches runtime regressions that
17061709
# build / unit tests / spec-liveness pass over (e.g. the #2018 tz-bucketing
1707-
# break, which was green on every static gate). The `--` args reach the
1708-
# package's `vitest run` and are hashed into the turbo task, so each
1709-
# shard caches its test leg independently -- and under `--only` that is
1710-
# the ONLY task the passthrough is hashed into; the build closure it used
1711-
# to scatter is built once, shard-independently, by the step above.
1710+
# break, which was green on every static gate). The shard slice travels in
1711+
# the task-declared env `OS_TEST_SHARD` (#20820, #19278's carrier):
1712+
# `turbo.json` lists it in `@objectstack/dogfood#test`'s `env`, so it is
1713+
# hashed into THAT task only, and `packages/qa/dogfood/vitest.config.ts`
1714+
# hands it to vitest's `shard`. There is no `--` passthrough and no
1715+
# `--only`: the run plans the `^build` closure with the same hashes as the
1716+
# step above (they replay), and the test task's hash therefore moves when
1717+
# an upstream package changes -- the old `--only` + `-- --shard=k/3` shape
1718+
# dropped the closure from the test's hash and a main-seeded cache entry
1719+
# could replay a shard green across an upstream-only change.
17121720
# run-with-stall-guard: same wiring as Test Core (see the comment there;
17131721
# #4250/#4314) — it tees to dogfood.log itself for the completeness guard
17141722
# below, propagates the suite's real exit status (no `| tee` + pipefail),
@@ -1728,17 +1736,14 @@ jobs:
17281736
# globalPassThroughEnv entry or turbo strips it — see the script header.
17291737
export VITEST_MAX_WORKERS="$(node scripts/vitest-worker-cap.mjs)"
17301738
mkdir -p "$RUNNER_TEMP/stall-reports"
1731-
# `--only` (#16395's flag, #16886's site): the step above already
1732-
# built this package's dependency closure in a passthrough-free run,
1733-
# so this run must schedule the ONE task the passthrough is for.
1734-
# Without it turbo re-hashes the whole `^build` closure under
1735-
# `--shard=k/3` and rebuilds it, once per shard -- the step comment
1736-
# above carries the measurement. ⚠ That build step is load-bearing
1737-
# for this flag: with the closure unbuilt, this run fails LOUDLY
1738-
# here (imports resolve to a missing dist), never as a silent green.
1739+
# The slice is the ENV below, not a passthrough: see the step comment.
1740+
# The build step above still builds the closure once, so the closure's
1741+
# `build` tasks REPLAY here (identical hashes: nothing run-level is
1742+
# hashed into them any more) and only `dogfood#test` executes.
1743+
export OS_TEST_SHARD="${{ matrix.shard }}/3"
17391744
node scripts/run-with-stall-guard.mjs --log "$RUNNER_TEMP/dogfood.log" --stall-minutes 10 \
17401745
--report-dir "$RUNNER_TEMP/stall-reports" -- \
1741-
pnpm turbo run test --filter=@objectstack/dogfood --only --log-order=stream -- --shard=${{ matrix.shard }}/3
1746+
pnpm turbo run test --filter=@objectstack/dogfood --log-order=stream
17421747
17431748
# Dogfood boots real apps in-process, so a native/OOM abort is likelier
17441749
# here than in the unit suites — and a shard that dies silently looks like

0 commit comments

Comments
 (0)