Skip to content

Commit 74b7c06

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22937-core-row-definition-mappers
2 parents 9e86437 + eede467 commit 74b7c06

40 files changed

Lines changed: 2938 additions & 1027 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'create-objectstack': minor
3+
---
4+
5+
The blank starter ships a test story: `pnpm test` boots the scaffolded app's own stack with `bootStack` from `@objectstack/verify` and asserts one business fact through its in-process handle
6+
7+
Clause-②: no
8+
9+
A project scaffolded with `npx create-objectstack` used to start with no `test` script and no way to assert its own behaviour, so the first test an author wrote was a hand-assembled kernel or a faked `ctx`. The blank template now carries:
10+
11+
- **`note.test.ts`**, one starter test. It boots the app the project declares (its objects, its plugins, the services it requires) on the real engine, in-process, with an in-memory database and no server, and asserts that a note without a title is refused with the engine's own `VALIDATION_FAILED` error naming `title`, beside the same note with a title being saved. It addresses the object as `Note.name`, so it follows the namespace scaffolding writes.
12+
- **`vitest.config.ts`**: a 60 s hook timeout for the boot, vitest's console capture switched off, so a log line the stack writes while a worker shuts down cannot fail a green run, and `OS_REGISTRY_LOG: 'warn'`, which drops the engine's one-line-per-registered-item boot log.
13+
- **`package.json`**: a `test` script (`vitest run`), and `@objectstack/verify` (at the project's `@objectstack/*` range) and `vitest` as devDependencies. The vitest range is `~4.0.0`, the one `objectstack init -t plugin` and `objectstack create plugin` declare, and it is generated from that shared policy rather than written by hand, so the three scaffolders cannot drift apart. `~4.0.0` is the range that installs on npm 10, the npm Node 22 bundles; `^4.0.0` does not.
14+
- **`tsconfig.json`** includes every root `*.ts` (`"*.ts"`, as `objectstack init` writes it), so `pnpm typecheck` checks the starter test and the vitest config.
15+
- **`README.md`** lists `pnpm test` beside `validate`, `lint` and `typecheck`.
16+
17+
The project's CI workflow is unchanged: its steps are the script set every scaffolder's project must declare, and `objectstack init -t app` declares no `test` script.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
fix(objectql)!: a validation rule that reads its master through a master-detail field asks the caller's read door for the header first — a header the caller cannot read is unavailable to the rule as a header that exists nowhere is (#22892)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, export or stored shape is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite. A rule that loaded before keeps its authored shape. What narrows is one runtime read: the related read of a `script` / `cross_field` rule that reads one hop through a `master_detail` field (`record.<field>.<column>`) now asks the caller's read door for the header before its elevated read, so a header the caller cannot read is answered as a header that exists nowhere. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only or type-surface-only). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
A detail write answers a header the caller cannot read exactly as a header that exists nowhere: its `parent` binding and its reference check ask the caller's read door first (the companion entry for #22519). A validation rule on the detail that reads the header through the master-detail field (`record.invoice.status == 'locked'`) still read the header elevated, and rules run before the reference check, so that rule could still answer by what an unreadable header holds.
14+
15+
**FROM.** For a non-system caller who may write the detail but cannot read its header, a rule reading the header through the master-detail field judged the header's stored values. Measured with a rule refusing a line under a locked invoice: a header id that exists nowhere was refused with `_record` `rule_violation` ("the related record was not found"); an unreadable open header passed the rule and was refused `reference_not_found`; an unreadable locked header was refused with the rule's own message. Three answers, one bit of the header's state.
16+
17+
**TO.** Through a `master_detail` field the rule's related read takes the write's two reads:
18+
19+
1. The caller's read door, under the caller's own context. A header it does not return (hidden by row scope, a refused read of the header object, or a failed read) is unavailable to the rule exactly as a header that exists nowhere is: the rule cannot be evaluated and the write is refused `400 VALIDATION_FAILED` at `_record` (`rule_violation`, "the related record was not found"), whatever the header holds.
20+
2. The elevated, tenant-kept read of the columns the rule names, only for the headers step 1 returned. A header the caller can read is still judged on its STORED values, a column field-level security hides or masks for the caller included.
21+
22+
**Bindings.**
23+
24+
- Every write door that runs the rule: `insert` (single and batch), `insertMany`, update by id (a repoint included), the predicate (bulk) update, and the `validate()` preview.
25+
- System callers (`isSystem`: seed replay, boot, package install) ask no read door and are judged exactly as before.
26+
- A rule reading through a plain `lookup` is unchanged.
27+
- No new error code and no status change: the answer is the one a header that exists nowhere already got.
28+
- Cost: one id-projected read under the caller's context per master-detail field a rule reads through, once per write batch.
29+
30+
**Fix, if a write relied on the old answer:** give the writer read access to the header (an object permission or sharing rule on the master), or declare the detail `controlled_by_parent` so the header's access governs it.
31+
32+
**Not measured.** How many deployed detail objects carry a rule reading their master was not measured: the rule only tightens, so it holds whatever that answer is.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): on a `kernel-resolver` host, a dispatcher door resolves a service on the request's own environment kernel before the host kernel. `HttpDispatcher.resolveService` used to ask the host kernel's scoped lookup first, and the host kernel's singleton `protocol` / `metadata` / `objectql` answered it whatever the scope. So `POST /api/v1/actions/:object/:action` loaded its subject row through a protocol that registers none of the environment's objects, and it answered `404 RECORD_NOT_FOUND` for a row the caller can read. Hosts with no request kernel of their own (single-environment, SharedProjectPlugin) resolve exactly as before, and an id that names nothing is still refused with 404.
6+
7+
Clause-②: no

‎.gitattributes‎

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

145110
#
146111
# #13731 enumerated this file's blind spot and closed it: `check:merge-driver` now
@@ -182,7 +147,6 @@ packages/spec/declaration-map/** merge=os-regen
182147
packages/spec/api-surface-signatures.json merge=os-regen
183148
docs/audits/2026-07-unknown-key-strictness-ledger.counts/** merge=os-regen
184149
content/docs/references/** merge=os-regen
185-
content/docs/permissions/system-context.mdx merge=os-regen
186150
skills/*/references/_index.md merge=os-regen
187151
skills/objectstack-ui/references/react-blocks.md merge=os-regen
188152
scripts/platform-object-tenancy-census.json merge=os-regen

‎.github/workflows/ci.yml‎

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,17 +1066,27 @@ jobs:
10661066
# second red on wall time against `timeout-minutes`. Both constants, their
10671067
# measured basis and a self-test battery each live in the script.
10681068
#
1069+
# THE RED IS A `schedule` RED ALONE (C2, both timing steps, ruling
1070+
# https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511):
1071+
# the dataset's weights are measured on the hourly `schedule` runs, so
1072+
# only those exit 1 on DRIFT. On `merge_group`, `pull_request`, `push`
1073+
# and `workflow_dispatch` the step prints the same verdict, raises the
1074+
# DRIFT as a `::warning::` annotation and exits 0. The event reaches the
1075+
# script as `--event "$GITHUB_EVENT_NAME"`, which it requires; the rule
1076+
# and its self-test live there (TIMING_GRADED_EVENT, timingGateMode).
1077+
#
10691078
# EXECUTED WINDOWS ONLY. Measurements come from samplesFromSummary() in
10701079
# measure-test-shard-timings.mjs, which drops cache HITs and failed
10711080
# suites, and the prediction is summed over the same executed packages.
10721081
# A replay's near-zero window would read as a shard far FASTER than
10731082
# predicted and vouch for a rotted dataset, so a shard whose packages all
10741083
# replayed is NOT MEASURED, never "fast".
10751084
#
1076-
# NO `if:` and NO `continue-on-error` (the red is the point), and ABOVE
1077-
# the attestation pair for the #6082 reason documented on the upload
1078-
# above: a failed unguarded step skips that pair, so a drift red also
1079-
# withholds this shard's attestation, the fail-closed direction. A shard
1085+
# NO `if:` and NO `continue-on-error` (the red is the point, and the
1086+
# `schedule`-only red is the script's `--event`, so neither is needed),
1087+
# and ABOVE the attestation pair for the #6082 reason documented on the
1088+
# upload above: a failed unguarded step skips that pair, so a drift red
1089+
# also withholds this shard's attestation, the fail-closed direction. A shard
10801090
# with no packages runs no turbo and writes no summary; that is NOT
10811091
# MEASURED here rather than a usage error from the script.
10821092
#
@@ -1109,7 +1119,7 @@ jobs:
11091119
exit 0
11101120
fi
11111121
node scripts/partition-test-shards.mjs --check-drift "${SUMMARIES[@]}" \
1112-
--label "Test Core (${{ matrix.shard }}/6)"
1122+
--label "Test Core (${{ matrix.shard }}/6)" --event "$GITHUB_EVENT_NAME"
11131123
11141124
# Runs even when the suite failed — that is when it earns its keep. It
11151125
# answers TWO questions about a red suite, and needs both to be able to
@@ -1486,12 +1496,12 @@ jobs:
14861496
# ── The per-package suite-duration ratchet (#16468) ──────────────────
14871497
#
14881498
# A GATE, unlike the two reporting steps above: NO `continue-on-error`,
1489-
# so a package whose suite ran past its ceiling reds `Test Core`, naming
1490-
# the package and its slowest test files. The ceiling is the package's
1491-
# slowest executed run in the refresh window x 1.25, generated into
1492-
# scripts/test-shard-timings.json (`ceilings`) by the refresh lane and
1493-
# held by every later refresh; it rises only by a ruled entry in the
1494-
# script's RULED_CEILING_RAISES. A `provisional` package prints "no
1499+
# so on a `schedule` run a package whose suite ran past its ceiling reds
1500+
# `Test Core`, naming the package and its slowest test files. The
1501+
# ceiling is the package's slowest executed run in the refresh window
1502+
# x 1.25, generated into scripts/test-shard-timings.json (`ceilings`) by
1503+
# the refresh lane and held by every later refresh; it rises only by a
1504+
# ruled entry in the script's RULED_CEILING_RAISES. A `provisional` package prints "no
14951505
# ceiling: provisional" and is not red; a package absent from the dataset
14961506
# prints "no ceiling: new package" and is not red until the first refresh
14971507
# after it lands gives it a ceiling, while a package the dataset knows
@@ -1505,6 +1515,20 @@ jobs:
15051515
# samplesFromSummary, and every vitest file line); a capture that did not
15061516
# arrive leaves its packages ungraded and says so, never red.
15071517
#
1518+
# EXIT-GRADED ON `schedule` ALONE (C2, both timing steps, ruling
1519+
# https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511):
1520+
# the ceilings come from the hourly `schedule` runs, so only those red
1521+
# this job on a package over its ceiling. On `merge_group`,
1522+
# `pull_request`, `push` and `workflow_dispatch` the step prints the same
1523+
# table, raises each package over its ceiling as a `::warning::`
1524+
# annotation and exits 0; a real slowdown merged to `main` reds the next
1525+
# hourly run. A package whose ceiling is under the absolute floor (20s,
1526+
# CEILING_FLOOR_SECONDS) prints "below floor" and is never red. The
1527+
# event reaches the script as `--event "$GITHUB_EVENT_NAME"`, which it
1528+
# requires; a refusal (a malformed table or raise record) is red on every
1529+
# event. The rule, the floor's reading and their self-tests live in the
1530+
# script.
1531+
#
15081532
# NOT MEASURED, exit 0, while the dataset carries no `ceilings`: they are
15091533
# generated only where the run-summary artifacts are downloadable (the
15101534
# refresh lane), so the first refresh after this lands writes them, and
@@ -1515,7 +1539,7 @@ jobs:
15151539
# captured, so a red shard's packages are graded too.
15161540
- name: Check the per-package suite-duration ceilings
15171541
if: always()
1518-
run: node scripts/check-test-suite-ceilings.mjs --captures "$RUNNER_TEMP/test-timings" --label 'Test Core'
1542+
run: node scripts/check-test-suite-ceilings.mjs --captures "$RUNNER_TEMP/test-timings" --label 'Test Core' --event "$GITHUB_EVENT_NAME"
15191543

15201544

15211545
# ── Temporal conformance against live, non-UTC servers (ADR-0053 D-A3) ─────

0 commit comments

Comments
 (0)