Skip to content

Commit 4c837a5

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18677-validate-per-package-authoring-pass
2 parents ceb9774 + bc2ec80 commit 4c837a5

19 files changed

Lines changed: 2208 additions & 61 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/formula': minor
3+
---
4+
5+
`EvalContext` no longer declares `api?: { exists, count, lookup }` — the kernel query API behind `os.exists` / `os.count` / `os.lookup`, which `buildScope()` never bound (#18318).
6+
7+
**BREAKING** for a TypeScript consumer: an `EvalContext` literal that carries `api` stops compiling. The level stays `minor` because the launch window refuses `major` outright — while it is open, breaking-ness is carried by this banner and by the ADR-0087 disposition at the foot of this changeset, not by the bump.
8+
9+
The member's docblock said it was "implemented opportunistically by call sites that have a query engine", and no call site ever could: `ctx.api` was read **zero** times in this package — control in the same sweep, `ctx.user`, three reads in `stdlib.ts` — so the three functions reached no evaluation scope however completely a caller populated the member. An author who wrote a predicate to the declaration got `runtime: found no matching overload for 'dyn.lookup(string, dyn)'` instead, and because an unevaluable predicate refuses the write it guards, a validation rule authored that way locked **every** write on its object. The harm came from the declaration existing, not from the implementation missing, so it is removed rather than implemented — with the reason written at the deletion site, and with no shim, alias or reserved spelling left behind.
10+
11+
**Migration — `api: { … }` → delete the property.** There is no replacement key and nothing to re-point: every implementation ever passed there was discarded before evaluation, so removing the property changes no result your predicates produce. TypeScript is where you will hear about it: an `EvalContext` literal carrying `api` now fails to compile, which is the whole of the break. Reading a related record's field from inside a predicate remains unexpressible in any spelling — that capability is tracked as its own card, relationship traversal (`record.crm_account.type`), and deliberately not as `os.lookup` queries; no schedule is implied by this removal.
12+
13+
<!-- adr-0087: not-required (runtime-interface-only packages/formula/src/types.ts#EvalContext) `EvalContext` is a published runtime TypeScript interface with no metadata surface — no Zod schema, no `packages/spec` declaration, no stored representation — so `objectstack migrate meta` has nothing to rewrite and the ledger has no channel to carry. The compiler at the consumer's own call site reaches every affected caller instead. Nothing is re-pointed either: `buildScope()` never read `ctx.api`, so the whole remedy is deleting a property that was already inert. -->
14+
15+
Clause-②: yes
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/plugin-auth': patch
4+
'@objectstack/organizations': patch
5+
---
6+
7+
Build freshness: these three packages now write the repo's build-input content
8+
stamp as the last step of their own build, and are checked for freshness (not
9+
merely existence) by `check:dev-prereqs`.
10+
11+
What changes for a consumer: each tarball now carries two extra inert metadata
12+
files inside `dist/` — `.build-input-hash` and `.build-input-hash-dts`, the same
13+
pair `@objectstack/spec` has always shipped. Nothing is imported, executed or
14+
resolved from them, no export moves and no runtime behaviour changes.
15+
16+
Why: a sibling checkout that links these packages by `link:` compiles against
17+
their `dist/`, so a dist built from an older tree surfaces as a type error
18+
naming an import nobody touched, with the symbol present in `src/` the whole
19+
time. A HEAD-versus-pin comparison is silent through that; a content stamp
20+
written by the build itself is not.

‎.github/workflows/lint.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4938,6 +4938,26 @@ jobs:
49384938
- name: Stall-guard budget invariant
49394939
run: pnpm check:stall-guard-budget
49404940

4941+
# The `Rerun Safety` verdict table (#18590) — self-test half only, and the
4942+
# asymmetry is the same one the step below explains: the measurement half
4943+
# takes two log paths and two exit codes that exist only inside that
4944+
# nightly job, so there is nothing here for a bare invocation to read.
4945+
#
4946+
# ⭐ It is wired HERE rather than left in the nightly deliberately.
4947+
# `rerun-safety-nightly.yml` declares `schedule` and `workflow_dispatch`
4948+
# and no PR-time event, so a gate family reachable only from it would be
4949+
# this tree's first SCHEDULED-ONLY family — which `check:pm-dispatch-gates`
4950+
# refuses by name, on the ground that a board sweep must not be routed into
4951+
# a per-PR gate list.
4952+
#
4953+
# WHAT REDDENS IT: its own script's parsing rules and verdict table. Those
4954+
# are matching rules, so their production verdict is identical before and
4955+
# after they break (see `check-self-test-wired.mjs`) — this step is the
4956+
# only instrument watching them. Reads nothing but its own fixtures; no
4957+
# build, no network, no token, sub-second.
4958+
- name: Rerun-safety verdict table self-test
4959+
run: pnpm check:rerun-safety-verdict
4960+
49414961
# The third member of the stall-guard family, and the only one that was
49424962
# never wired (#13246). `measure-stall-guard-headroom.mjs` re-derives the
49434963
# one term the budget gate above cannot read statically -- `p + s`, the

‎.github/workflows/rerun-safety-nightly.yml‎

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@
2020
# to fail loudly like any other; what this gate is really asserting is that the
2121
# SECOND pass agrees with the first.
2222
#
23+
# ## Why pass 1 no longer ends the job (#18590)
24+
#
25+
# That last sentence was true of the design and false of the implementation. A
26+
# red `pass 1` ENDED THE JOB, which skipped `pass 2` — so on every night the
27+
# suite was red on main, the one thing this workflow exists to assert was not
28+
# asserted. The job went red anyway, and its red is indistinguishable from the
29+
# red it exists to report.
30+
#
31+
# It ran that way for a 17-night streak ending 2026-08-31: thirteen of those
32+
# nights died in pass 1, under FOUR different failing packages and two different
33+
# failure kinds, with the killer moving from `verify` to `cli` as each was
34+
# fixed. Only four of the seventeen nights ever reached pass 2. "Keep pass 1
35+
# green" is not a reachable end state on a trunk that goes red like any other,
36+
# so the measurement is no longer conditional on it: both passes always run,
37+
# and `scripts/rerun-safety-verdict.mjs` compares their failing task sets and
38+
# says, in one line, whether the rerun property was MEASURED and what it said.
39+
#
40+
# ⛔ Nothing here is weakened. A red pass 1 still fails this job. What changed
41+
# is only that a broken trunk now costs the instrument its BASELINE instead of
42+
# its READING.
43+
#
2344
# ## Why `--force`
2445
#
2546
# Turbo would replay cached results on the second pass and report green without
@@ -44,7 +65,19 @@ jobs:
4465
# while the PR read as "still running" rather than broken. Since #4314 the
4566
# primary stall detector is run-with-stall-guard on each pass (labeled red
4667
# after 15 min of frozen output); this is the pure backstop.
47-
timeout-minutes: 120
68+
#
69+
# ⚠️ Raised 120 -> 170 (#18590). 120 had stopped being a backstop and become
70+
# a second way for this instrument to go dark: the last four GREEN two-pass
71+
# runs on main took 74, 89, 103 and **115** minutes, so a healthy run was
72+
# within five minutes of being killed by its own bound — and #18590 makes
73+
# pass 2 run on the nights pass 1 is red too, which is exactly when a
74+
# 115-minute run is most likely. ⛔ This is NOT a stall budget and must never
75+
# be read as one: run-with-stall-guard still declares a stall after 15
76+
# minutes of frozen output on EITHER pass, so a genuine hang is still caught
77+
# in 15 minutes and never at this number. Raising it can therefore hide no
78+
# hang; all it stops is a slow-but-live run being reported as a failure of
79+
# the thing under test.
80+
timeout-minutes: 170
4881
steps:
4982
- name: Checkout repository
5083
uses: actions/checkout@v7
@@ -77,13 +110,35 @@ jobs:
77110
- name: Build packages
78111
run: pnpm build
79112

80-
# Deliberately NOT `continue-on-error`. If pass 1 is red the suite is
81-
# simply broken on main and that is worth failing on; this gate adds pass
82-
# 2, it does not replace the ordinary signal. The stall guard (#4250,
83-
# #4314) bounds a frozen-output hang at 15 min instead of letting a
84-
# nightly nobody watches sit until the 120-min timeout; on a stall it
85-
# exits 75 and its banner in the log names the last output line.
86-
# --log-order=stream is mandatory under the guard (see the Test Core
113+
# ⭐ Pass 1 RECORDS its exit code instead of ending the job with it
114+
# (#18590), and that change is what makes this workflow an instrument
115+
# again rather than a light that has been on for seventeen nights.
116+
#
117+
# It used to be spelled "deliberately NOT `continue-on-error`", on the
118+
# reasoning that a red pass 1 means the suite is broken on main and that
119+
# is worth failing on. The reasoning still holds and the job still fails
120+
# — see the verdict step, which owns the exit status now. What did NOT
121+
# hold is the consequence nobody wrote down: a step that ends the job also
122+
# skips `pass 2`, so on every night pass 1 was red the ONE measurement
123+
# this workflow exists to take was not taken at all. The job went red
124+
# either way, and that red is indistinguishable from the red it exists to
125+
# report — "rerun safety is failing" and "rerun safety was never measured"
126+
# printed the same colour.
127+
#
128+
# Measured over the 17-night red streak that ended 2026-08-31 (#18590):
129+
# thirteen nights died here in pass 1, and the killer MOVED — six nights
130+
# `@objectstack/verify#test` (an AssertionError, since fixed on main),
131+
# then four nights `@objectstack/cli#test` (5000ms vitest timeouts), with
132+
# `plugin-auth` and earlier `cli` nights in between. The remaining four
133+
# nights reached pass 2 and are the only rerun readings the streak
134+
# contains. So "keep pass 1 green" was never a reachable end state on a
135+
# trunk that is red from time to time like any other — and every night it
136+
# was not reached cost the reading.
137+
#
138+
# The stall guard (#4250, #4314) is unchanged: it bounds a frozen-output
139+
# hang at 15 min instead of letting a nightly nobody watches sit to the
140+
# job timeout; on a stall it exits 75 and its banner names the last output
141+
# line. --log-order=stream is mandatory under the guard (see the Test Core
87142
# comment in ci.yml): grouped log order flushes a task's output only when
88143
# the task ends, so a solo tail task longer than the budget reads as
89144
# frozen while healthy. The guard refuses a turbo run without it.
@@ -96,8 +151,16 @@ jobs:
96151
# vitest's own "use the default" signal. Needs turbo.json's
97152
# globalPassThroughEnv entry or turbo strips it — see the script header.
98153
export VITEST_MAX_WORKERS="$(node scripts/vitest-worker-cap.mjs)"
154+
status=0
99155
node scripts/run-with-stall-guard.mjs --log "$RUNNER_TEMP/rerun-pass1.log" --stall-minutes 15 -- \
100-
pnpm turbo run test --concurrency=4 --force --log-order=stream
156+
pnpm turbo run test --concurrency=4 --force --log-order=stream || status=$?
157+
echo "PASS1_EXIT=$status" >> "$GITHUB_ENV"
158+
if [ "$status" -ne 0 ]; then
159+
echo "::warning::Pass 1 exited $status — the suite is red on main. Recorded as the"
160+
echo "::warning::BASELINE and NOT failed here: pass 2 runs anyway so the rerun"
161+
echo "::warning::property still gets measured tonight. The verdict step owns the"
162+
echo "::warning::job's exit status and will still fail this job."
163+
fi
101164
102165
# Informational only. A stray `.objectstack/` is how the #4065 class shows
103166
# up on disk, so printing what pass 1 left behind turns a pass-2 failure
@@ -113,11 +176,19 @@ jobs:
113176
echo "Files inside them:"
114177
find . -path ./node_modules -prune -o -type d -name '.objectstack' -exec find {} -type f \; || true
115178
116-
# A pass-2 red normally means rerun-UNSAFE — but a stall (guard exit 75)
117-
# says nothing about working-tree pollution, and the #4065 error text
118-
# would send whoever triages it hunting for state leaks that aren't
119-
# there. Keep the two verdicts apart.
179+
# ⭐ `if: always()` is the other half of #18590 — pass 2 is the assertion,
180+
# so it runs whatever pass 1 did. A red pass 1 now costs this instrument
181+
# its BASELINE, not its reading.
182+
#
183+
# This step no longer renders the verdict either. It cannot: with a red
184+
# baseline allowed, "pass 2 failed" is no longer the same fact as
185+
# "rerun-unsafe" — a task that was already failing in pass 1 and fails
186+
# again in pass 2 is ordinary breakage on main, and printing the #4065
187+
# text over it would send whoever triages it hunting for state leaks that
188+
# are not there. That comparison needs both passes' failing task sets, so
189+
# it belongs one step down, in the verdict.
120190
- name: Test suite — pass 2 (same working tree)
191+
if: always()
121192
run: |
122193
# Bound vitest's INNER worker pool (#11958). A no-op on a runner with
123194
# <= 5 cores (it only ever LOWERS vitest's own `cores - 1` default);
@@ -129,18 +200,28 @@ jobs:
129200
status=0
130201
node scripts/run-with-stall-guard.mjs --log "$RUNNER_TEMP/rerun-pass2.log" --stall-minutes 15 -- \
131202
pnpm turbo run test --concurrency=4 --force --log-order=stream || status=$?
132-
if [ "$status" -eq 0 ]; then
133-
exit 0
134-
elif [ "$status" -eq 75 ]; then
135-
echo "::error::Pass 2 STALLED — frozen output, killed by run-with-stall-guard"
136-
echo "::error::(see #4250). This is NOT a rerun-safety verdict: a stall says"
137-
echo "::error::nothing about working-tree pollution. Rerun the workflow; if it"
138-
echo "::error::stalls again at the same file, note the occurrence on #4250."
139-
else
140-
echo "::error::The suite passes once and fails on a second run in the same"
141-
echo "::error::working tree. Something under test writes state into the tree and"
142-
echo "::error::reads it back on the next run. Ordinary CI cannot see this — every"
143-
echo "::error::other job is a fresh clone, so every other job is always run #1."
144-
echo "::error::See the on-disk state reported above, and #4065 for the pattern."
145-
fi
146-
exit "$status"
203+
echo "PASS2_EXIT=$status" >> "$GITHUB_ENV"
204+
205+
# ⭐ THE VERDICT, and the job's exit status (#18590).
206+
#
207+
# It answers the question the exit code alone has never been able to
208+
# express — was the rerun property MEASURED tonight — and only then what
209+
# it measured. `scripts/rerun-safety-verdict.mjs` carries the table and a
210+
# `--self-test`; its header is the authority on detail.
211+
#
212+
# ⛔ This step does not make the job green. A red pass 1 still fails it,
213+
# exactly as before; what is new is that the failure now SAYS which of
214+
# "broken on main" and "rerun-unsafe" it is, and a broken-on-main night no
215+
# longer swallows the reading.
216+
# ⛔ The self-test is NOT invoked here. It runs at PR time as
217+
# `pnpm check:rerun-safety-verdict` in lint.yml, because this workflow
218+
# declares only `schedule` and `workflow_dispatch`: a gate family reachable
219+
# from here alone would be the tree's first SCHEDULED-ONLY family, which
220+
# `check:pm-dispatch-gates` refuses by name. That step's comment carries
221+
# the reasoning.
222+
- name: Rerun-safety verdict
223+
if: always()
224+
run: |
225+
node scripts/rerun-safety-verdict.mjs \
226+
--pass1-log "$RUNNER_TEMP/rerun-pass1.log" --pass1-exit "${PASS1_EXIT:-1}" \
227+
--pass2-log "$RUNNER_TEMP/rerun-pass2.log" --pass2-exit "${PASS2_EXIT:-1}"

0 commit comments

Comments
 (0)