Skip to content

Commit 96a92ca

Browse files
committed
Merge origin/main into claude/project-thread-x004t9
Claude-Session: https://claude.ai/code/session_01QciHBvFJXnhZqXmdqpDtpz Co-authored-by: Claude <noreply@anthropic.com>
2 parents 0871a48 + abb9046 commit 96a92ca

99 files changed

Lines changed: 4442 additions & 1268 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+
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/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/lint.yml‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -966,13 +966,11 @@ jobs:
966966
# repo-root `sdui.manifest.json` is absent or unreadable, so nothing in
967967
# the production path ever says "the gate lost its whitelist" — this step
968968
# is the thing that notices. Offline record checks only (existence, shape,
969-
# sha256 vs the provenance record, and with no pin each registry
970-
# package's source digest vs its record): the regenerate-and-compare half
971-
# runs where the registry is built, as ci.yml's Build & E2E UI step "The
972-
# committed SDUI manifest is the one this tree generates" (#26137), on
973-
# pull_request and on the merge queue's combined tree. Unfiltered:
974-
# filtering it to the artefact would go dormant on exactly the PR that
975-
# moves the artefact's inputs and not the artefact.
969+
# sha256 vs the provenance record): freshness is decided where the
970+
# registry is built, by ci.yml's Build & E2E UI step "The committed SDUI
971+
# manifest is the one this tree generates" (#26137), on pull_request and
972+
# on the merge queue's combined tree. Unfiltered: it builds nothing and
973+
# runs in seconds, and an absent artefact is the failure it exists for.
976974
# Direct invocation per the GATE INVOCATION IDIOM at the top of this file.
977975
- name: SDUI manifest is present, intact and fresh at the objectui pin
978976
id: sdui-manifest-is-present-intact-and-fresh

‎AGENTS.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ The Studio/Console UI lives in this workspace under `ui/` (`@object-ui/*`; the C
159159
and `ui/AGENTS.md` adds that tree's own rules to this file's. `pnpm console:build` builds the Console and copies its
160160
dist into `packages/console/` (`packages/console/build-dist.sh`). ⛔ Never hand-edit `packages/console/dist/`.
161161

162-
**Moving the pin has a second half: regenerate the committed manifest** — `pnpm objectui:build`, then
163-
`node scripts/gen-sdui-manifest-node.mjs`, which reads objectui's BUILT tree at the pin (never an npm install, whose
164-
version string names an older commit) and takes no arguments; `scripts/check-sdui-manifest.mjs` reds until you do.
165-
`objectui:bump` and `objectui:refresh` print the step, and the console build ships that same tracked file in
166-
`packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`.
162+
**A registry change has a second half: regenerate the committed manifest** with
163+
`node scripts/gen-sdui-manifest-node.mjs`, which builds the registry under `ui/`, enumerates it from `ui/apps/console`
164+
(never an npm install) and takes no arguments; commit what it writes. CI's `Build & E2E UI` runs it too and fails
165+
when the committed `sdui.manifest.json` differs, the merge queue's combined tree included, and the console build
166+
ships that same tracked file in `packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`.
167167

168168
**Fast iteration on `ui/` src (no rebuild loop):** run the Console dev server —
169169
`pnpm --filter @object-ui/console dev` (Vite on **:5180**, HMR). Its `/api` proxy targets

‎CONTRIBUTING.md‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ pull request.
1010
alike.** It holds naming, the Zod-first and contract-first rules, the documentation
1111
guardrails and the changeset rules. This file does not restate them. Where a rule
1212
matters, it points you to AGENTS.md, and if the two ever disagree, AGENTS.md wins.
13+
[Human Minimum](#human-minimum) lists the rules a first pull request most often breaks,
14+
each with its pointer.
1315

1416
## 📋 Table of Contents
1517

1618
- [Code of Conduct](#code-of-conduct)
1719
- [Getting Started](#getting-started)
1820
- [Build and Test Cost](#build-and-test-cost)
1921
- [Making a Change](#making-a-change)
22+
- [Human Minimum](#human-minimum)
2023
- [Where Things Live](#where-things-live)
2124
- [Package Directory](#package-directory)
2225
- [Pull Request Process](#pull-request-process)
@@ -121,7 +124,8 @@ Other examples: `pnpm dev:crm`, `pnpm dev:todo`. Docs site: `pnpm docs:dev`.
121124
`git worktree` instead of switching branches in a shared tree. AGENTS.md
122125
(Prime Directive #11 and *Multi-agent working discipline*) explains why, and what
123126
a worktree does not isolate.
124-
3. **Make the change, following AGENTS.md.** Read its *Prime Directives* before a
127+
3. **Make the change, following AGENTS.md.** Start with the
128+
[Human Minimum](#human-minimum). Read its *Prime Directives* before a
125129
structural change, and its *Context Routing* table for the rules of the path you
126130
are editing.
127131
4. **Test what your change reaches**, not only the file you edited:
@@ -138,8 +142,50 @@ Other examples: `pnpm dev:crm`, `pnpm dev:todo`. Docs site: `pnpm docs:dev`.
138142
5. **Add a changeset when the change publishes.** Run `pnpm changeset` and commit the
139143
`.changeset/*.md` file it writes. A bug fix in a released package takes a `patch`
140144
changeset. AGENTS.md's *Post-Task Checklist* (step 3) states the whole rule: which
141-
bump a change takes, the `Clause-②` declaration, and the migration a breaking
142-
change must carry.
145+
bump a change takes, the `Clause-②` and `Metadata-protocol` declarations (what they
146+
mean: [Human Minimum](#human-minimum), item 5), and the migration a breaking change
147+
must carry.
148+
149+
## Human Minimum
150+
151+
The rules a first pull request most often breaks. Each item names the rule in one
152+
sentence and links the AGENTS.md text that states it; that text is the rule, so read it
153+
before you rely on the summary. The
154+
[pull request template](./.github/PULL_REQUEST_TEMPLATE.md) asks you to confirm the
155+
same list.
156+
157+
1. **Schemas start as Zod, and `packages/spec` holds no runtime logic.** AGENTS.md,
158+
[Prime Directives 1 and 2](./AGENTS.md#prime-directives).
159+
2. **Each kind of name has one casing**: config keys, machine names, error codes,
160+
metadata type names, REST paths and `OS_` environment variables. AGENTS.md,
161+
[Prime Directives 3 and 9](./AGENTS.md#prime-directives).
162+
3. **No formatter: `pnpm lint` is the only style authority, and Prettier is never run on
163+
a file you touch.** AGENTS.md, [Build & Test](./AGENTS.md#build--test).
164+
4. **One issue, one branch off an up-to-date `main`, one pull request; in a checkout that
165+
coding agents also use, one `git worktree` per task.** AGENTS.md,
166+
[Prime Directive 11](./AGENTS.md#prime-directives) and
167+
[Multi-agent working discipline](./AGENTS.md#multi-agent-working-discipline) item 2.
168+
5. **A change that publishes carries a changeset, and the PR body and the changeset body
169+
each declare a `Clause-②:` line and a `Metadata-protocol:` line.** AGENTS.md,
170+
[Post-Task Checklist](./AGENTS.md#post-task-checklist) step 3, which has the exact
171+
values and what each one requires.
172+
- `Clause-②` answers one question: does this change widen or narrow what a published
173+
package accepts or exposes (its accept set or public surface)? `Clause-②: no` when
174+
it does neither; `Clause-②: yes` when it does.
175+
- `Metadata-protocol` answers another: what changed in what a metadata author can
176+
write? `added`, `narrowed`, `semantic`, or `none` for everything else.
177+
- Each is a line of its own, starting with the key, so the `Check Changeset` gate can
178+
read it.
179+
6. **Touched `packages/spec`? Regenerate and commit its generated artifacts before you
180+
push.** AGENTS.md,
181+
[Touched `packages/spec`?](./AGENTS.md#touched-packagesspec-regenerate-its-artifacts-before-pushing).
182+
7. **Generated and release-owned files are never hand-edited in your pull request**
183+
(`content/docs/references/`, `packages/*/CHANGELOG.md`, `content/docs/releases/`),
184+
and only the maintainer cuts a release. AGENTS.md,
185+
[Documentation Guardrails](./AGENTS.md#documentation-guardrails) and
186+
[Prime Directive 15](./AGENTS.md#prime-directives).
187+
8. **Issue and pull request titles, bodies and comments are written in English.**
188+
AGENTS.md, [Communication](./AGENTS.md#communication).
143189

144190
## Where Things Live
145191

@@ -302,7 +348,8 @@ new page needs before it shows up in the navigation.
302348

303349
Push your branch to your fork and open a pull request against `main`. Its first line
304350
names the issue it fixes (`Fixes #1234`). The body says what changed, why, and how you
305-
verified it.
351+
verified it. The [pull request template](./.github/PULL_REQUEST_TEMPLATE.md) lays out
352+
that body, the two declaration lines and the [Human Minimum](#human-minimum) checklist.
306353

307354
### Review Process
308355

‎content/docs/data-modeling/seed-data.mdx‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,47 @@ duplicates on every replay boot ([#3434](https://github.com/objectstack-ai/objec
440440

441441
---
442442

443+
## File Fields — `seedFile(path)`
444+
445+
A `file`, `image`, `avatar`, `video` or `audio` field stores the id of a stored
446+
file, and a seed has no upload to make one. Name the file by the path your
447+
package ships it at instead, and the platform stores it for the record:
448+
449+
```typescript
450+
import { defineSeed, seedFile } from '@objectstack/spec/data';
451+
452+
export const contractVersionsSeed = defineSeed(ContractVersion, {
453+
externalId: 'code',
454+
records: [
455+
{ code: 'NDA-001-V1', file: seedFile('fixtures/nda-v1.pdf') },
456+
// `name` is what a download is saved as; `contentType` for an extension
457+
// the build does not know.
458+
{ code: 'MSA-001-V1', file: seedFile('fixtures/msa-v1', { name: 'MSA v1.pdf', contentType: 'application/pdf' }) },
459+
// A `multiple: true` file field takes an array.
460+
{ code: 'SOW-001-V1', file: seedFile('fixtures/sow.pdf'), scans: [seedFile('fixtures/p1.png'), seedFile('fixtures/p2.png')] },
461+
],
462+
});
463+
```
464+
465+
- **The path** is relative to the directory holding `objectstack.config.ts` and
466+
stays inside it — `/` separators, no leading `/`, no `.` or `..` segment.
467+
- **`os build`** reads each referenced file and bundles its bytes into the
468+
compiled artifact, so the seed carries them wherever the artifact goes
469+
(`os dev`, `os serve`, a published package). A path that names no file fails
470+
the build, and `os validate` refuses it too. `os serve` booting the config
471+
itself reads the files the same way.
472+
- **The seed load** stores each file and writes its id into the field; the
473+
record owns the file exactly as if it had been uploaded into that field. Every
474+
row gets a file of its own, even when two rows name the same path. A replay
475+
keeps a file whose bytes did not change, so restarting does not store it
476+
again; changing the file and rebuilding replaces it.
477+
- **Refused, by row, loudly**: a reference whose bytes the build did not bundle
478+
(a dataset written by hand without its `files`), bytes that do not match what
479+
the build recorded, and a file field holding an id-shaped string that names no
480+
stored file — write `seedFile(...)` there instead.
481+
482+
---
483+
443484
## Dynamic Values (CEL)
444485

445486
Any field value may be a **CEL expression** evaluated at install time against a
@@ -639,6 +680,9 @@ function defineSeed<
639680
records: Array<SeedRecord<TObj['fields']>>; // declared fields + injectable system columns
640681
}
641682
): Seed
683+
684+
// A file the package ships, for a file-class field (see "File Fields").
685+
function seedFile(path: string, options?: { name?: string; contentType?: string }): { $file: string; name?: string; contentType?: string }
642686
```
643687

644688
`SeedRecord` keys are the object's declared fields (a `lookup` / `master_detail`

‎content/docs/getting-started/quick-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Categories that have no section here at all are named under
2222
[Categories Without a Section](#categories-without-a-section) — that curation is stated,
2323
not left implicit.
2424

25-
## Data Protocol (15 of 30 schemas)
25+
## Data Protocol (15 of 31 schemas)
2626

2727
Core business logic and data modeling schemas.
2828

0 commit comments

Comments
 (0)