Skip to content

Commit 9dd0474

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-6djfn0
2 parents bcb84eb + f00e8d5 commit 9dd0474

75 files changed

Lines changed: 2968 additions & 1736 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/25777-demo-staff.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-auth': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/cli': minor
6+
---
7+
8+
feat: `demoStaff` — an application declares the demo accounts its seed data is dealt to, and the development boot creates them
9+
10+
Clause-②: yes
11+
Metadata-protocol: added
12+
13+
A stack may now declare `demoStaff: [{ name, email, position?, permissionSets? }]`. On a local development boot (`os dev`), `@objectstack/plugin-auth` creates each entry right after the development admin, through the same sign-up pipeline, with the development seed password (`os dev --admin-password`, default `admin123`); assigns the declared position and permission sets in the Default Organization (an account the membership policy left outside it is reported and gets none); and the application's seed then runs, so a seed row naming one of these people (`owner_id: 'Legal Counsel 1'`) resolves on the first boot. Idempotent: an address that already has an account is left exactly as it is, and only its missing assignments are added.
14+
15+
Development only. `os build` leaves the key out of the artifact (`objectstack.json`), so a deployment booted from one has nothing to provision. Every other boot creates nothing and says why, with an ADR-0112 code: a boot that is not `NODE_ENV=development`, or a migrate-and-exit run (`PERMISSION_DENIED`, logged at `info` — the intended outcome there); and, on a development boot, logged at `warn` because the developer loses the accounts: a walled tenancy posture or an auth base URL that is not a loopback address (`PERMISSION_DENIED`), a database where nothing but a demo account could sign in, so a demo account would be promoted to platform admin (`PRECONDITION_REQUIRED`), or a credential store that could not be read (`SERVICE_UNAVAILABLE`).
16+
17+
`defineStack` refuses an entry that names a position or permission set the stack does not declare (a platform set such as `admin_full_access` is never one), and two entries with one name or one address (`STACK_CROSS_REFERENCE_INVALID`).
18+
19+
`@objectstack/runtime`: when the boot will create demo accounts, `AppPlugin` runs its inline seed at `kernel:bootstrapped` instead of inside `start()`. The seed source is declared as before, so the settlement tally and `app:seeded` behave as they do for a seed that overran its budget.
20+
21+
What to do: nothing. A stack that declares no `demoStaff` boots, seeds and builds exactly as before. An application that creates its demo accounts with its own script can declare them here and delete the script.

‎.claude/skills/pm-dispatch/references/platform-readings.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@
387387
- 本车道强制多是过宽的回忆:`dispatch-gates.mjs --tier PATH` 逐路径现推,路径线是下限非放行。
388388
- 该脚本只答自己那棵树:姊妹仓路径回 absent from this tree,姊妹仓的档位与条款②只能手推。
389389
- `os-verify-lock.sh` 只住 objectstack:objectui 无它,跑本仓副本读的是容器级锁,非该仓深度。
390-
- required checks 的名单是每仓事实,objectstack 七个:
391-
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Build Core` ·
392-
`Governed Surface Queue Guard` · `Lint UI` · `Build & E2E UI`。
390+
- required checks 的名单是每仓事实,objectstack 八个:
391+
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Build Core` · `Lint UI` ·
392+
`Governed Surface Queue Guard` · `Type Check UI` · `Build & E2E UI`。
393393
- ⭐ 本表的 `mustName` 声明 `statesTheSet` ⇒ 必须逐字等于注册表,加注册行不跟改本表即红。
394394
- ⇒ 门绿即名单逐字齐全的读数;⛔ 但数量词未钉:实测 七→六 照绿,只能手跟改。
395395
- ⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在包级 typecheck 绿时红。

‎.github/workflows/ci.yml‎

Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,24 @@ jobs:
137137
contents: read
138138
pull-requests: read
139139
outputs:
140-
# On merge_group — and on the `schedule` / `workflow_dispatch` full runs
141-
# (#16467) — everything counts as changed: dorny/paths-filter resolves a
142-
# diff only on `pull_request` and `push`, and each of these three events
143-
# is a place a skipped job can never be the right answer (the queue build
144-
# is the last validation before main; the hourly run IS the full battery).
145-
# A skipped step's output is the empty string (falsy), so `|| 'true'`
146-
# supplies their value without touching PR/push behavior.
147-
docs: ${{ steps.changes.outputs.docs || 'true' }}
148-
core: ${{ steps.changes.outputs.core || 'true' }}
140+
# On the `schedule` / `workflow_dispatch` full runs (#16467) everything
141+
# counts as changed: dorny/paths-filter is skipped there (no diff to
142+
# resolve), a skipped step's output is the empty string (falsy), and
143+
# `|| 'true'` supplies the value without touching PR/push behavior.
144+
#
145+
# On merge_group the step RUNS (the diff is the queue base_sha against
146+
# the group head, see the step below), but only the three ui outputs
147+
# read it. `docs`, `core` and `crosspkg` stay 'true' there through the
148+
# `github.event_name != 'merge_group' &&` guard: the queue build is the
149+
# last validation before main, and those jobs run on every entry, as
150+
# they did before the step ran on this event. On any other event the
151+
# guard is true and the expression reads exactly as `<output> ||
152+
# 'true'` did.
153+
docs: ${{ github.event_name != 'merge_group' && steps.changes.outputs.docs || 'true' }}
154+
core: ${{ github.event_name != 'merge_group' && steps.changes.outputs.core || 'true' }}
149155
# Gates the `test` job ONLY, ORed with `core` — see the `crosspkg`
150156
# filter below and the `test` job's `if:` (#9829, #10015).
151-
crosspkg: ${{ steps.changes.outputs.crosspkg || 'true' }}
157+
crosspkg: ${{ github.event_name != 'merge_group' && steps.changes.outputs.crosspkg || 'true' }}
152158
# The objectui tree under `ui/` (objectstack#25677). Three outputs, read
153159
# together by the four `* UI` jobs at the bottom of this file:
154160
# ui — a path under ui/, or a root file the ui jobs build
@@ -158,13 +164,21 @@ jobs:
158164
# UI) and built (Build & E2E UI) against the new shape in
159165
# the PR that moves it;
160166
# ui_tree — `ui/package.json` exists in this checkout.
167+
# On merge_group `ui` and `ui_consumers` come from the queue diff
168+
# (objectstack#25867): an entry whose merged diff against
169+
# `merge_group.base_sha` touches no ui path and none of the seven
170+
# packages skips the four ui jobs, exactly as the same diff does on its
171+
# PR head; an entry that touches one runs them. If the step fails (the
172+
# base cannot be fetched, the diff errors), it is `continue-on-error`
173+
# there, its outputs are empty and `|| 'true'` runs every ui job.
174+
#
161175
# `ui_tree` is what keeps the ui jobs SKIPPED, not failed, on a tree that
162176
# does not carry ui/ yet: `ui` and `ui_consumers` fall back to 'true' on
163-
# merge_group / schedule / dispatch (half 1 below), and on push to main a
164-
# spec change sets `ui_consumers`, so neither path output alone can tell a
165-
# job there is nothing to build. Its step runs on every event, so the
166-
# `|| 'true'` fallback is reached only when this job died before it — and
167-
# then THE FILTER CONTRACT's "run everything" is the intended reading.
177+
# schedule / dispatch (half 1 below), and on push to main a spec change
178+
# sets `ui_consumers`, so neither path output alone can tell a job there
179+
# is nothing to build. Its step runs on every event, so the `|| 'true'`
180+
# fallback is reached only when this job died before it — and then THE
181+
# FILTER CONTRACT's "run everything" is the intended reading.
168182
ui: ${{ steps.changes.outputs.ui || 'true' }}
169183
ui_consumers: ${{ steps.changes.outputs.ui_consumers || 'true' }}
170184
ui_tree: ${{ steps.ui-tree.outputs.present || 'true' }}
@@ -216,13 +230,15 @@ jobs:
216230
echo 'ui/package.json is absent: every ui job skips on this run.'
217231
fi
218232
219-
# ⭐ SPELLED AS AN ALLOW-LIST, not as `!= 'merge_group'` (#16467). This
220-
# action reads a diff, and the only two events that hand it one it can
221-
# resolve unaided are `pull_request` (base vs head) and `push`
222-
# (`event.before` vs `after`). `merge_group` was the first event with
223-
# neither, and the exclusion was written as its name; `schedule` and
224-
# `workflow_dispatch` are the second and third, and under the old
225-
# spelling they would have RUN the action with no resolvable base.
233+
# ⭐ SPELLED AS AN ALLOW-LIST, never as a deny-list (#16467). This
234+
# action reads a diff, and only three events hand it one it can resolve
235+
# unaided: `pull_request` (base vs head), `push` (`event.before` vs
236+
# `after`) and `merge_group` (the payload's `base_sha` vs `head_sha`; at
237+
# the pinned v4.0.3, src/main.ts reads both when `base` / `ref` are
238+
# unset, and src/git.ts fetches the base sha at depth 1 and diffs the two
239+
# trees, `base..head`). `schedule` and `workflow_dispatch` hand it
240+
# neither, and under a deny-list spelling they would RUN the action with
241+
# no resolvable base.
226242
#
227243
# That failure direction is the dangerous one: every filter output would
228244
# come back `false`, `|| 'true'` supplies nothing (the output exists and
@@ -231,9 +247,16 @@ jobs:
231247
# of nothing at all. An allow-list makes each new event fail toward
232248
# THE FILTER CONTRACT's half 1 instead: the step is skipped, its outputs
233249
# are the empty string, `|| 'true'` fills them in, and everything runs.
250+
#
251+
# `merge_group` joined the list for the ui outputs alone (#25867; the
252+
# three core-side outputs above ignore the step on that event). Its
253+
# `continue-on-error` is the fail-open half there: a queue build whose
254+
# base cannot be fetched reads as "run every ui job", never as a red
255+
# `filter` job. On pull_request and push a failure stays red, as before.
234256
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
235257
id: changes
236-
if: github.event_name == 'pull_request' || github.event_name == 'push'
258+
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group'
259+
continue-on-error: ${{ github.event_name == 'merge_group' }}
237260
with:
238261
filters: |
239262
docs:
@@ -3292,11 +3315,16 @@ jobs:
32923315
# vitest root, so the suite must run with cwd = ui/ (it does, below); run
32933316
# from this repo's root every key misses and every file weighs the median.
32943317
#
3295-
# Every event runs the whole suite. objectui's affected-test selector
3296-
# (ui/scripts/select-affected-tests.mjs) reads `git diff --name-only`, which
3297-
# prints paths relative to the GIT root — `ui/packages/...` here — against a
3298-
# package graph spelled relative to the objectui root, so it is not wired
3299-
# until objectui's repo-root lookups are migrated (sub-issue 2 of the epic).
3318+
# ── Test UI: the two speeds (#25868) ────────────────────────────────────
3319+
# `push`, `schedule` and `workflow_dispatch` run the whole suite: the full
3320+
# battery. `pull_request` and `merge_group` run the files
3321+
# ui/scripts/select-affected-tests.mjs selects -- the ui packages the change
3322+
# sits in and their dependents, plus the tests the package graph cannot
3323+
# place, which run on every change -- or the whole suite when the change
3324+
# reaches something every ui test runs through, outside ui/ included (a
3325+
# non-documentation root file, `patches/`, one of the seven packages the
3326+
# `ui_consumers` filter names). The selector's header states the rules and
3327+
# what it does not see; the full battery on `main` is the backstop.
33003328
test-ui:
33013329
name: Test UI (${{ matrix.shard }}/16)
33023330
needs: filter
@@ -3313,6 +3341,10 @@ jobs:
33133341
steps:
33143342
- name: Checkout repository
33153343
uses: actions/checkout@v7
3344+
with:
3345+
# The select step below diffs against the base commit, which needs
3346+
# the base's history in the clone.
3347+
fetch-depth: 0
33163348

33173349
- name: Setup Node.js
33183350
uses: actions/setup-node@v7
@@ -3363,13 +3395,85 @@ jobs:
33633395
--filter=@objectstack/sdui-parser... \
33643396
--filter=@objectstack/types...
33653397
3398+
# Fails OPEN: a selector that exits non-zero, prints no verdict, or has
3399+
# no base to diff against means the whole suite, never a skip. The exit
3400+
# status is captured beside the output, not through a command
3401+
# substitution. A pull request diffs from merge-base(origin/<base ref>,
3402+
# HEAD), never the payload's `base.sha`, which is frozen when the PR is
3403+
# opened: everything `main` gained since would be charged to this PR,
3404+
# and a lockfile change among it would make every long-open PR a full
3405+
# run (Test Core's "WHERE THE AFFECTED DIFF STARTS" above is the same
3406+
# fact). A merge group diffs from its `base_sha`. Every shard computes
3407+
# the same selection from the same checkout, so `--shard` partitions
3408+
# one set.
3409+
- name: Select the tests this change affects
3410+
id: select
3411+
env:
3412+
EVENT: ${{ github.event_name }}
3413+
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
3414+
QUEUE_BASE: ${{ github.event.merge_group.base_sha }}
3415+
run: |
3416+
base=''
3417+
case "$EVENT" in
3418+
pull_request)
3419+
if [ -n "$PR_BASE_REF" ]; then
3420+
base=$(git merge-base "refs/remotes/origin/$PR_BASE_REF" HEAD) || base=''
3421+
fi
3422+
;;
3423+
merge_group) base="$QUEUE_BASE" ;;
3424+
*)
3425+
echo 'mode=full' >> "$GITHUB_OUTPUT"
3426+
echo "A $EVENT run is the full battery: every ui test file, sixteen shards."
3427+
exit 0
3428+
;;
3429+
esac
3430+
status=0
3431+
if [ -z "$base" ]; then
3432+
echo "::warning::no diff base on this $EVENT event; running the full suite rather than guessing."
3433+
status=2
3434+
else
3435+
echo "Diff base: $base"
3436+
node ui/scripts/select-affected-tests.mjs --base "$base" --out "$RUNNER_TEMP/affected-tests.txt" \
3437+
> "$RUNNER_TEMP/select.out" || status=$?
3438+
cat "$RUNNER_TEMP/select.out"
3439+
fi
3440+
verdict=''
3441+
if [ "$status" -eq 0 ]; then
3442+
verdict=$(tail -n 1 "$RUNNER_TEMP/select.out")
3443+
fi
3444+
case "$verdict" in
3445+
mode=affected | mode=full) ;;
3446+
*)
3447+
echo "::warning::the affected-test selector reached no verdict (exit $status); running the full suite rather than skipping silently."
3448+
verdict='mode=full'
3449+
;;
3450+
esac
3451+
echo "$verdict" >> "$GITHUB_OUTPUT"
3452+
3453+
# `$MODE` is the VALUE of the select step's output `mode`: its
3454+
# `mode=affected` line is GitHub's `key=value` form, so what arrives here
3455+
# is `affected` or `full`, never the line itself.
3456+
# ui/scripts/__tests__/select-affected-tests-wiring.test.ts runs both
3457+
# steps as written, so neither end can be renamed alone. The filters are
3458+
# paths relative to ui/, the vitest root, passed as positionals with no
3459+
# `--`, the only spelling ui/scripts/vitest-invocation-guard.mjs admits.
3460+
#
33663461
# objectui's full-battery runs also pass its per-file timings reporter
33673462
# and upload the result for the weekly dataset refresh. Both belong to
33683463
# that refresh workflow, which is in the governance-workflow audit
33693464
# (sub-issue 4 of the epic); until it is ported the committed dataset
33703465
# is what the sequencer reads.
33713466
- name: Run the ui tests (shard ${{ matrix.shard }}/16)
3372-
run: pnpm --dir ui run test --shard=${{ matrix.shard }}/16 --reporter=default --reporter=github-actions
3467+
env:
3468+
MODE: ${{ steps.select.outputs.mode }}
3469+
run: |
3470+
if [ "$MODE" = 'affected' ]; then
3471+
mapfile -t filters < "$RUNNER_TEMP/affected-tests.txt"
3472+
echo "Affected speed: ${#filters[@]} vitest filter(s), shard ${{ matrix.shard }} of 16."
3473+
pnpm --dir ui exec vitest run --shard=${{ matrix.shard }}/16 --reporter=default --reporter=github-actions "${filters[@]}"
3474+
else
3475+
pnpm --dir ui run test --shard=${{ matrix.shard }}/16 --reporter=default --reporter=github-actions
3476+
fi
33733477
33743478
- name: Save Turbo cache (main only)
33753479
if: always() && github.event_name == 'push' && matrix.shard == 1

‎AGENTS.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,11 @@ Even inside your own worktree, operate defensively:
523523
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
524524
every poll then misreads "not on `main` yet" as "queued". Always read *two* things:
525525
the queue branch **and** `origin/main`. And **the queue enforces only the required
526-
set** — seven contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
527-
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI` and `Build & E2E UI`. A
528-
check outside those seven is advisory and rides through, and an advisory red that lands rides
529-
`main`'s merge ref into every later PR until stanched. A required context is matched by
530-
check-run name, so a rename detaches its gate silently — treat those seven names as contract.
526+
set** — eight contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
527+
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI` and
528+
`Build & E2E UI`. A check outside those eight is advisory and rides through, and an advisory red
529+
that lands rides `main`'s merge ref into every later PR until stanched. A required context is
530+
matched by check-run name, so a rename detaches its gate silently — treat those eight names as contract.
531531
**Two speeds** (ruling record 6120820653): PR and queue runs carry the fast set, 10 minutes of
532532
wall per queue entry; `main`'s hourly and push runs carry the full battery — dogfood, e2e,
533533
`Temporal Conformance (live PG + MySQL)` — read by the publish lane before

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Plugin architecture, manifests, and kernel runtime.
8585
| **[Metadata Loader](/docs/references/kernel/metadata-loader)** | `metadata-loader.zod.ts` | MetadataLoaderContract | Metadata loading |
8686
| **[Package Registry](/docs/references/kernel/package-registry)** | `package-registry.zod.ts` | InstalledPackage, InstallPackageRequest | Package resolution |
8787

88-
## System Protocol (16 of 36 schemas)
88+
## System Protocol (16 of 37 schemas)
8989

9090
Runtime environment, logging, jobs, caching, and observability.
9191

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,11 @@ schema page is the authority until one lands.
233233
at all (`viewItems` is `z.never()`, the machine-assembled channel for
234234
runtime-assembled manifests; `runtimeModule` is written by `objectstack build`);
235235
`manifest`, `i18n`, `api`, `server`, `appName`, `email`, `sms`, `requires`,
236-
`tiers`, `plugins`, `devPlugins`, `devHint` and `devLogins` configure the stack
237-
rather than declare metadata items (`email` and `sms` carry no credentials —
238-
those stay in the environment or in Settings); and
236+
`tiers`, `plugins`, `devPlugins`, `devHint`, `devLogins` and `demoStaff` configure
237+
the stack rather than declare metadata items (`email` and `sms` carry no
238+
credentials — those stay in the environment or in Settings; `demoStaff` names the
239+
demo accounts a local `os dev` boot creates, and `os build` leaves it out of
240+
`objectstack.json`); and
239241
`functions` and `onEnable` are code the runtime calls, not metadata it stores.
240242

241243
## How you verify

0 commit comments

Comments
 (0)