Skip to content

Commit 2dfbe0b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-g33h31
# Conflicts: # scripts/sdui-manifest.sources/components.json # scripts/sdui-manifest.sources/plugin-list.json
2 parents 80dc2dc + abb9046 commit 2dfbe0b

136 files changed

Lines changed: 5659 additions & 2059 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/25779-seed-files.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/service-storage': minor
5+
'@objectstack/runtime': minor
6+
'@objectstack/cli': minor
7+
---
8+
9+
feat(spec,metadata-protocol,service-storage,runtime,cli)!: a seed record can carry a file the package ships, `seedFile(path)`, and a file field's id that names no stored file is refused
10+
11+
Clause-②: yes (narrowing)
12+
Metadata-protocol: narrowed
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is removed, renamed or re-shaped. The narrowing is a seed load refusing a value it used to write: an id-shaped string in a file field that names no stored file. There is nothing for `objectstack migrate meta` to rewrite and no tombstone; the fix is the author's choice of the file the field should hold. -->
15+
16+
A `file`, `image`, `avatar`, `video` or `audio` field stores the id of a stored file, and only an upload made one, so a seed could not fill a required file field at all. A seed record now names the file by the path the package ships it at:
17+
18+
```ts
19+
import { defineSeed, seedFile } from '@objectstack/spec/data';
20+
21+
defineSeed(ContractVersion, {
22+
externalId: 'code',
23+
records: [
24+
{ code: 'NDA-001-V1', file: seedFile('fixtures/nda-v1.pdf') },
25+
{ code: 'NDA-002-V1', file: seedFile('fixtures/nda-v2', { name: 'NDA v2.pdf', contentType: 'application/pdf' }) },
26+
],
27+
});
28+
```
29+
30+
- **The path** is relative to the directory holding `objectstack.config.ts`, uses `/`, and stays inside it: an absolute path, a drive letter, a `\`, or an empty, `.` or `..` segment is refused at parse. A `multiple: true` file field takes an array of `seedFile(...)` values. `defineSeed` refuses `seedFile(...)` in a field that does not store a file.
31+
- **`os build` / `os compile`** reads each referenced file and writes its bytes into the dataset's new `files` map (`{ contentType, size, sha256, data }`, base64), so the compiled artifact carries them to every boot that loads it: `os dev`, `os serve`, a published package, an artifact URL. A path that names no file, or a file whose type is neither named with `contentType` nor known by its extension, fails the build, and `os validate` refuses the same paths. `os serve` / `os dev` booting a config rather than an artifact reads the files the same way, so both boots seed the same files.
32+
- **The seed loader** stores the bytes through the new `field-files` service (`FIELD_FILE_SERVICE`, `IFieldFileService`, registered by `@objectstack/service-storage`), writes the file's id into the field, and makes the written record the file's owner, the (object, record, field) ownership an upload gets. Each row gets a file of its own. A replay that finds the row holding a file stored from the same path with the same bytes, name and type keeps it, so a restart does not store a new file for an unchanged fixture. A file stays `pending` until its row lands, so a file whose row is refused is reclaimed by the pending-upload retention.
33+
- **Refused, by row, before the row is written**: a reference whose bytes the dataset does not carry, bytes that do not match their recorded size and SHA-256, a load with no `field-files` service, a field mixing `seedFile(...)` with other values, and several `seedFile(...)` values in a one-file field. Each is counted, reported in the load's errors and logged at `error`.
34+
- **The inline boot seed** of an app whose datasets carry seed files runs at `kernel:bootstrapped` (as one with `demoStaff` already does), once the storage service has read its settings, so the files land in the store the deployment serves from.
35+
- `SeedLoaderService` takes an optional fourth argument, `{ getFieldFiles }`; every platform caller passes the kernel's `field-files` service.
36+
37+
**BREAKING** (a seed-load narrowing). A seed record whose file field holds an id-shaped string (letters, digits, `_` and `-`, at most 64 characters) that names no stored file in the row's organization is now refused, by row, with an error naming the field and the value. Before, it was written as given, and the field then read as holding no file. An external URL or an inline value is not an id and is written as before; so is any value on a deployment without `@objectstack/service-storage`.
38+
39+
**What to do.** If a seed load now reports `… holds '<value>', which names no stored file`, replace the value with the file the field should hold, `seedFile('<path in the package>')`, or remove it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
refactor(app-shell): the Console `ObjectView`'s list toolbar affordances move into a sibling hook
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The environment list's entitlements, the object's list actions and their localized toolbar copies, and the create and import buttons' toolbar-scope predicates move verbatim out of `ObjectViewInner` in `views/ObjectView.tsx` into `useObjectViewToolbarAffordances` in `objectViewToolbarAffordances.ts` beside it. The object-level verdicts (`resolveAffordance`) stay in the component and are passed in with every other input. No logic, name or rendered output changes, and the package entry is unchanged.
11+
12+
What to do: nothing.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/plugin-detail': minor
3+
---
4+
5+
fix(plugin-detail): `record:related_list` with `actions: []` no longer draws the built-in New
6+
7+
Clause-②: no
8+
Metadata-protocol: semantic
9+
10+
- **Before.** `actions: []` on a `record:related_list` hid the list's authored and default actions, but the built-in **New** stayed whenever the host offered create. The only way to drop it was the child object's `userActions.create`, which removes New from every list of that object. A page could not say that one list's children must be created through a process (a renewal through its flow, a version through upload), and the generic create form, with the relationship prefilled, bypassed that process.
11+
- **Now.** An authored `actions: []` is the author's choice of no actions on that list, New included. It holds per list and does not depend on the child object's `userActions.create`. Every list that does not author `[]` keeps the host's verdict.
12+
- **Unchanged.** A list with no `actions`, or with a non-empty `actions` list, draws New exactly as before. The row affordances (View, Edit, Delete) and the `add` (link existing) button are not governed by this key. No key is added and none is removed.
13+
- **What to do.** Nothing for a list that should keep New. A page that authored `actions: []` and still wants New on that list deletes the key.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the permission editor tells the delegated-admin companion list whether it edits under a package, instead of the list guessing from its presentation
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **`@object-ui/app-shell`:** `PermissionMatrixEditPage` now passes `packageBound={!!packageId}` to `PermissionAdvancedFacets` at both of its mounts (Setup's stacked page and the Studio Access pillar's tabs). The facet's `packageBound` prop defaults to `false` and no longer reads its value from `only`, which picks a presentation, not a binding. What an author sees does not change today: the Studio Access pillar always edits under a package and Setup's permission page never does. A host that sets one without the other now gets the face that matches its binding: under a package the companion grants are listed unmarked with the package note, otherwise they are marked with the one-click grant.
11+
12+
What to do: nothing.

‎.github/PULL_REQUEST_TEMPLATE.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Fixes #
2+
3+
## What changes
4+
5+
## Why
6+
7+
## Verification
8+
9+
Clause-②: yes | no
10+
Metadata-protocol: added | narrowed | semantic | none
11+
12+
_Keep one value on each of the two lines above, each line starting with its key.
13+
[Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum)
14+
item 5 says what they mean._
15+
16+
## Human Minimum
17+
18+
Each item is one rule from
19+
[CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum),
20+
which links the AGENTS.md text that states it.
21+
22+
- [ ] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
23+
- [ ] 2. Every name I added uses the casing for its kind.
24+
- [ ] 3. No formatter ran; `pnpm lint` passes.
25+
- [ ] 4. One issue, one branch off an up-to-date `main`, one pull request.
26+
- [ ] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
27+
- [ ] 6. `packages/spec` touched: its generated artifacts are regenerated and committed.
28+
- [ ] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`.
29+
- [ ] 8. Title, body and comments are in English.
30+
- [ ] The *Before Submitting* list in CONTRIBUTING.md is done.

‎.github/workflows/check-links.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,56 @@ name: Check Links
2020
# "the anchors are fine". The fragment half is owned by `pnpm check:doc-anchors`
2121
# in `lint.yml`, which is REQUIRED where this lane is advisory; `lychee.toml`'s
2222
# `include_fragments` comment records why the enum is not the fix.
23+
#
24+
# `paths:` is what the two checks read: lychee's input globs in `LYCHEE_ARGS`
25+
# below (taken as whole `content/` and `docs/` trees, so an image or page a
26+
# link points at inside them counts too), its config, and the ADR step's
27+
# `docs/adr/**` and script. Measured on the last 49 landings of `main`
28+
# (6390fe52): 10 touched one of these, so the other 39 would not have run it.
29+
# ⚠️ The filter is blind to the TARGET side outside those trees: a PR that
30+
# only renames or deletes a file a doc links to (a `packages/*` directory
31+
# from CONTRIBUTING.md, `LICENSE` from LICENSING.md) skips this lane, and the
32+
# break shows on the next PR that touches a doc, or on a `workflow_dispatch`
33+
# run. Keep this list equal to `LYCHEE_ARGS`. ⛔ Promoting the lane into the
34+
# required set removes this filter in the same change: a required check
35+
# skipped by `paths:` never reports, the #6121 stall above.
2336
on:
2437
workflow_dispatch:
2538
pull_request:
2639
branches:
2740
- main
41+
paths:
42+
- 'content/**'
43+
- 'docs/**'
44+
- 'README.md'
45+
- 'ARCHITECTURE.md'
46+
- 'CHANGELOG.md'
47+
- 'RELEASE_NOTES.md'
48+
- 'CONTRIBUTING.md'
49+
- 'ROADMAP.md'
50+
- 'LICENSING.md'
51+
- 'CODE_OF_CONDUCT.md'
52+
- 'lychee.toml'
53+
- '.lycheeignore'
54+
- 'scripts/check-adr-links.mjs'
55+
- 'scripts/invoked-as.mjs'
56+
- '.github/workflows/check-links.yml'
57+
58+
# A newer run for the same pull request supersedes this one, so the older run is
59+
# cancelled. The group key is ci.yml's shape; `cancel-in-progress` is true only on
60+
# `pull_request`, so no other event's run is ever cancelled.
61+
concurrency:
62+
group: check-links-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.after || github.ref }}
63+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
2864

2965
jobs:
3066
link-checker:
3167
name: Check Documentation Links
3268
runs-on: ubuntu-latest
69+
# Run wall over the 100 most recent completed runs to 2026-10-27: p99 0.8 min, max
70+
# 1.0. The bound covers the lychee retry path (two binary downloads and the 15 s wait)
71+
# with headroom.
72+
timeout-minutes: 10
3373
# Least privilege: the job only reads the tree and runs lychee offline.
3474
# There is no issue-filing step, and `--offline` makes zero network
3575
# requests, so neither `issues: write` nor a GITHUB_TOKEN is needed.

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4148,12 +4148,12 @@ jobs:
41484148
# 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s
41494149
# for the whole run when nothing moved). It serialises through the
41504150
# @objectstack/sdui-parser the step above built, so a parser edit moves
4151-
# this verdict too, which no committed digest does. On merge_group it runs
4152-
# on the queue's combined tree, the one run that sees two entries' effect
4153-
# on the manifest together. Only the artefact is compared: the records the
4154-
# generator rewrites beside it are held by `check-sdui-manifest` in
4155-
# lint.yml. The Console build below reads the registry through its src/
4156-
# aliases, so the dist this step builds does not reach it.
4151+
# this verdict too. On merge_group it runs on the queue's combined tree,
4152+
# the one run that sees two entries' effect on the manifest together.
4153+
# Only the artefact is compared: the record the generator rewrites beside
4154+
# it is held by `check-sdui-manifest` in lint.yml. The Console build below
4155+
# reads the registry through its src/ aliases, so the dist this step
4156+
# builds does not reach it.
41574157
- name: The committed SDUI manifest is the one this tree generates
41584158
run: |
41594159
node scripts/gen-sdui-manifest-node.mjs

‎.github/workflows/codeql.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
analyze:
2828
name: Analyze
2929
runs-on: ubuntu-latest
30+
# Run wall of the 22 analyses that completed (success) in the last 100 runs,
31+
# 2026-10-24..26: p99 59.0 min, max 63.1. The bound is that p99 plus about half again.
32+
timeout-minutes: 90
3033
permissions:
3134
actions: read
3235
contents: read

‎.github/workflows/cross-repo-issue-closer.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ jobs:
124124
name: Close issues referenced in other repositories
125125
if: github.event.pull_request.merged == true
126126
runs-on: ubuntu-latest
127+
# Run wall over the 100 most recent completed runs to 2026-10-27: p99 0.2 min, max
128+
# 0.3.
129+
timeout-minutes: 5
127130
steps:
128131
- name: Close (or report) cross-repo closing keywords
129132
uses: actions/github-script@v9

‎.github/workflows/docker-publish.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ jobs:
3636
publish:
3737
name: Build & push ghcr.io/objectstack-ai/objectstack
3838
runs-on: ubuntu-latest
39+
# Job wall, all four readings on record: 3.5 and 3.5 min (the two dispatch runs), 4.3
40+
# min (18.1.0, Release run 40078805767, attempt 2) and 1.4 min (attempt 1, red). Four
41+
# readings make p99 the max, 4.3; the bound is wide because the image is built for two
42+
# platforms, arm64 under QEMU, and a cold layer cache is unmeasured.
43+
timeout-minutes: 30
3944
permissions:
4045
contents: read
4146
packages: write

0 commit comments

Comments
 (0)