Skip to content

Commit cd14a59

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16322-analytics-daterange-closed-vocabulary-drivers
2 parents 67e46cc + 419facd commit cd14a59

23 files changed

Lines changed: 3234 additions & 146 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
docs(metadata): the `@objectstack/metadata/errors` header records what the leaf entry actually loads — 4 packages, 83 modules, 2.2 MB — instead of leaving the reader to infer it is light (#15346)
6+
7+
`packages/metadata/src/errors.ts` is the leaf subpath `@objectstack/metadata/errors`, and its header explains why the subpath exists: the root entry pulls the manager, every loader and the YAML/filesystem machinery behind them, and "a consumer that wants a 40-line predicate should not have to load any of that". Measured, that promise half holds — and the header did not say which half.
8+
9+
**The half that holds, and stays:** MEASURED 2026-09-08 on `origin/main` `8c1515e847`, with `scripts/check-lean-entry-closure.mjs`'s exported `measure()`/`byPackage()` (one fresh child per published condition; module set collected through `module.registerHooks` unioned with `require.cache`). Both published conditions load exactly four packages, and the manager, every loader, `chokidar`, `glob`, `js-yaml` and `readdirp` are absent from all of them. That is the claim the header actually wrote, and it is accurate.
10+
11+
**The half that had drifted:** the general conclusion a reader takes away. `import` loads 83 modules / 2,201,709 bytes; `require` loads 83 / 2,355,405. Since the maintainer's 2026-08-30 ruling the file re-exports from `@objectstack/types`, which builds to one bundled module importing **both** `@objectstack/spec/api` (1,401,253 B) and `@objectstack/spec/security` (195,367 B) at module top, as values — `api` is the dominant edge at 7x `security`, and the two spec entries are independent of each other.
12+
13+
The header now separates the two costs the way the finding did, because conflating them is the available mistake: **in-repo the marginal contribution is 143 bytes** — the only current consumer of the subpath, `@objectstack/metadata-protocol`, declares `@objectstack/spec` itself and pays for that closure anyway — while the megabytes are what an **out-of-repo** consumer pays, which nothing here can measure. The figures are labelled PROVENANCE, dated and tree-pinned, in the manner the neighbouring gate keeps its own: nothing derives from them, nothing compares against them, and the header says to re-take them rather than quote them. They rot fast — the same measurement four days earlier at `6e67b86c0` found the same 83 modules but 2,487,842 bytes, 286,133 more, without this file changing at all.
14+
15+
**Why `patch` and not `skip-changeset`, measured rather than assumed.** `@objectstack/metadata` is released (17.3.0) and `errors.ts` is a shipped source file, so "a comment publishes nothing" needed checking rather than asserting. Building the package before and after the edit: `dist/errors.js`, `dist/errors.cjs`, `dist/errors.d.ts` and `dist/errors.d.cts` are **byte-identical** (esbuild strips the comment, and there is no `sourcesContent`), but `dist/errors.js.map` and `dist/errors.cjs.map` **change** — the export statement moved from source line 65 to line 113, so the `mappings` VLQ moves with it (`;AAgEA,…``;AAgHA,…`), at identical file size. `npm pack --dry-run` lists both maps in the tarball. So this diff does publish bytes from a released package, which is exactly what `skip-changeset` is not for. Nothing executable and no type changes: a consumer's stack traces resolve to the right source line, and that is the whole of it.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`ai/solution-blueprint.zod.ts` publishes its own sentence again, instead of a list of the symbols it happens to export.
6+
7+
The file always carried a real module header — ADR-0033 §4 plan-first authoring, and how the `apply_blueprint` tool expands each entry into a proper metadata body. But only a blank line separated that header from `const SNAKE_CASE`, and TSDoc's own attachment rule says a block belongs to the declaration it immediately precedes. The header-zone selector reads that rule back, so the header counted as the regex constant's documentation and was disqualified as the module's. Both generators then fell through to their export-list fallback, and the row published into the `objectstack-ai` skill index read:
8+
9+
```
10+
- `…/ai/solution-blueprint.zod.ts` — Exports: BlueprintConditionSchema, BlueprintSummaryOperationsSchema, …
11+
```
12+
13+
A true statement about the file that says nothing about its subject — on the one row whose job is to send an agent to this source for exact field shapes.
14+
15+
`SNAKE_CASE` now carries the one-line doc it always deserved. A comment is not a declaration, so the preamble ends there and the header becomes the module's own block. The published row and the public reference page both open on it:
16+
17+
```
18+
- `…/ai/solution-blueprint.zod.ts` — Solution Blueprint Schema (ADR-0033 §4 — plan-first authoring)
19+
```
20+
21+
The selector is untouched. Under its own rule it was deciding correctly, and a census of every source under `packages/spec/src` found this file to be the only one of its kind: 19 shipped `*.zod.ts` sources have a header-zone block sitting against a declaration, and in the other 18 that block genuinely documents the symbol it sits against (`Transport Protocol Enum` against `TransportProtocol`, `Shared history for this file` against `AGENT_HISTORY`). Only here did a module header sit against a constant it says nothing about.
22+
23+
Neither generator can see this class — each compares its artifact against itself, and each reproduced the selector faithfully, so a generator-only check passes on the defect. A pin now asserts the content of the published row directly.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): the `field.valueDomain` liveness note stops claiming the settings door is "unchanged until then"
6+
7+
The `valueDomain` row of the published `liveness/field.json` ledger ended on a sentence written
8+
while the re-point was still in the future:
9+
10+
> The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in
11+
> its own follow-up card and is unchanged until then.
12+
13+
Both halves of the 2026-09-02 ruling have since landed — the settings half (#15434) and the engine
14+
half (#15316) — and the engine half rewrote this note wholesale while carrying that sentence
15+
forward verbatim. "Unchanged until then" therefore described a state that no longer existed: the
16+
door it names had already re-pointed, one commit earlier.
17+
18+
The sentence now says what is true of that door, read off its source rather than off a PR title:
19+
its second copy of all three definitions is deleted, `firstRejectedDomainMember` asks
20+
`isValueDomainMember` — the same call `record-validator.ts` makes — and what remains on that side
21+
is the door's own business (which declarations it agrees to enforce, how a multi-value carrier is
22+
walked, the fragments the env-override log line needs). A re-added local table reddens
23+
`value-domains.shared-predicate.pin.test.ts`.
24+
25+
Ledger-note text only. The row's `status` is untouched — it tracks the engine write path, and
26+
`liveness/state-counts.md` is derived by `gen:liveness-counts` from the row states, none of which
27+
move here (`check:liveness` reports the counts file current).

.github/workflows/ci.yml

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,55 @@ on:
3838
# branch-protection-required check MUST carry this trigger, or queue builds
3939
# wait forever on a check that never reports.
4040
merge_group:
41+
# ── THE HOURLY FULL RUN (#16467) ─────────────────────────────────────────
42+
# `push` above now computes the Test Core package set with `--affected`
43+
# against `github.event.before`, so a merge no longer re-tests the whole
44+
# workspace it just tested in the queue. Something still has to run the FULL
45+
# battery on `main`, on a clock, or "main is green" stops being a statement
46+
# about the workspace and becomes a statement about the last diff.
47+
#
48+
# This is that run. Minute 0 rather than an offset minute ON PURPOSE, and
49+
# against the sibling convention in test-nightly-tiers.yml: the reading this
50+
# feeds — `scripts/ci/select-shard-timings-run.mjs` — wants one complete run
51+
# per hour far more than it wants a short queue wait, and a run that starts
52+
# a few minutes late is still the hour's run. What it must NOT do is collide
53+
# with the next hour's, which the concurrency group below decides.
54+
#
55+
# ⛔ This is a NEW TRIGGER KEY, not a widening of `push:`. The ⛔ above still
56+
# binds: `push:` stays filtered to `main`.
57+
schedule:
58+
- cron: '0 * * * *'
59+
# The same full battery, on demand: an operator who has just landed a fix for
60+
# a red hourly run should not have to wait up to an hour to see it answered.
61+
# Treated as `full` by the selection script for the same reason `schedule` is.
62+
workflow_dispatch:
4163

4264
# Superseded runs on the same PR/branch waste runners and delay feedback;
4365
# cancel them. Push runs to main group by commit ref as well, so an in-flight
4466
# main run is cancelled only by a newer main push.
67+
#
68+
# ⭐ `github.event_name` IS LOAD-BEARING IN THIS KEY, and it is the whole
69+
# reason the hourly run above can finish (#16467). On a `schedule` event
70+
# `github.event.pull_request.number` is empty and `github.ref` is
71+
# `refs/heads/main` — BYTE-IDENTICAL to what a push to `main` produces. Without
72+
# the event name in the key the hourly run and the next merge share one group,
73+
# `cancel-in-progress: true` applies, and the next merge kills the hourly run.
74+
# That is not a hypothetical: this card's own measurement is that 36 of the
75+
# last 60 push runs on `main` were already cancelled that way, at a merge
76+
# cadence that would censor most hours.
77+
#
78+
# ⛔ Do not "simplify" this back to two segments. `scripts/ci/scheduled-full-
79+
# run.mjs --check-concurrency` evaluates this expression against a push-shaped
80+
# and a schedule-shaped context and reds when the two groups are equal, so the
81+
# collision cannot come back silently.
82+
#
83+
# `cancel-in-progress` stays `true` for every event including `schedule`: two
84+
# hourly runs overlapping means one of them is over an hour old, and an hour-old
85+
# measurement of `main` is strictly the worse of the two. Test Core's wall clock
86+
# is well inside the cadence (~39 shard-minutes across six shards), so this is
87+
# the exceptional path, not the normal one.
4588
concurrency:
46-
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
89+
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
4790
cancel-in-progress: true
4891

4992
jobs:
@@ -54,11 +97,13 @@ jobs:
5497
contents: read
5598
pull-requests: read
5699
outputs:
57-
# On merge_group, everything counts as changed: dorny/paths-filter has no
58-
# merge_group support, and the queue build is the last validation before
59-
# main — the one place a skipped job can never be the right answer. A
60-
# skipped step's output is the empty string (falsy), so `|| 'true'`
61-
# supplies the merge-group value without touching PR/push behavior.
100+
# On merge_group — and on the `schedule` / `workflow_dispatch` full runs
101+
# (#16467) — everything counts as changed: dorny/paths-filter resolves a
102+
# diff only on `pull_request` and `push`, and each of these three events
103+
# is a place a skipped job can never be the right answer (the queue build
104+
# is the last validation before main; the hourly run IS the full battery).
105+
# A skipped step's output is the empty string (falsy), so `|| 'true'`
106+
# supplies their value without touching PR/push behavior.
62107
docs: ${{ steps.changes.outputs.docs || 'true' }}
63108
core: ${{ steps.changes.outputs.core || 'true' }}
64109
console: ${{ steps.changes.outputs.console || 'true' }}
@@ -100,9 +145,24 @@ jobs:
100145
- name: Checkout repository
101146
uses: actions/checkout@v7
102147

148+
# ⭐ SPELLED AS AN ALLOW-LIST, not as `!= 'merge_group'` (#16467). This
149+
# action reads a diff, and the only two events that hand it one it can
150+
# resolve unaided are `pull_request` (base vs head) and `push`
151+
# (`event.before` vs `after`). `merge_group` was the first event with
152+
# neither, and the exclusion was written as its name; `schedule` and
153+
# `workflow_dispatch` are the second and third, and under the old
154+
# spelling they would have RUN the action with no resolvable base.
155+
#
156+
# That failure direction is the dangerous one: every filter output would
157+
# come back `false`, `|| 'true'` supplies nothing (the output exists and
158+
# says `false`), and every downstream `!= 'false'` guard SKIPS — so the
159+
# hourly full run would have been an entirely green, entirely empty run
160+
# of nothing at all. An allow-list makes each new event fail toward
161+
# THE FILTER CONTRACT's half 1 instead: the step is skipped, its outputs
162+
# are the empty string, `|| 'true'` fills them in, and everything runs.
103163
- uses: dorny/paths-filter@v4
104164
id: changes
105-
if: github.event_name != 'merge_group'
165+
if: github.event_name == 'pull_request' || github.event_name == 'push'
106166
with:
107167
filters: |
108168
docs:
@@ -473,13 +533,21 @@ jobs:
473533
# on this shard" exit below, every shard still attests,
474534
# and Test Core is an honest green. It is NOT the #10057
475535
# case, which is pull_request-only (the script says why).
476-
# push unchanged: the FULL list.
536+
# push affected set against `github.event.before`, the commit
537+
# `main` was on before this merge landed (#16467). A zero
538+
# sha -- a first push, or a force-push that rewrote
539+
# history -- falls back to the FULL list, loudly.
540+
# schedule the FULL list: the hourly run IS the full battery, and
541+
# workflow_ the on-demand rerun of it. Neither carries a diff base,
542+
# dispatch and neither should: they are the run that says whether
543+
# the whole workspace is green on `main`.
477544
- name: Compute this shard's package set
478545
env:
479546
OS_SHARD_EVENT_NAME: ${{ github.event_name }}
480547
OS_SHARD_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
481548
OS_SHARD_PR_PINNED_BASE_SHA: ${{ github.event.pull_request.base.sha }}
482549
OS_SHARD_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
550+
OS_SHARD_PUSH_BEFORE_SHA: ${{ github.event.before }}
483551
run: |
484552
bash scripts/ci/select-shard-packages.sh
485553
node scripts/partition-test-shards.mjs "$RUNNER_TEMP/turbo-ls.json" \

.github/workflows/lint.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ on:
2525
# queue builds or the queue stalls. This workflow has no PR-only steps, so
2626
# the trigger alone is enough.
2727
merge_group:
28+
# ── THE HOURLY FULL RUN (#16467), the half this file owns ────────────────
29+
# The card's ruling is that the hourly run covers BOTH required-context
30+
# files, not just ci.yml, and the reason is measured rather than symmetric:
31+
# the push-on-`main` run of THIS workflow is the only post-merge full-battery
32+
# run of the families `scripts/ci/select-gate-families.sh` scopes away on
33+
# merge groups (the PM dispatch-gates self-test, both ratchets, the
34+
# verify-lock self-test, the comment-mask corpus). A scoped family that goes
35+
# red on `main` after a queue build skipped it had, until this trigger, no
36+
# run that would notice and no filer that would say so.
37+
#
38+
# The selector already treats every event that is neither `merge_group` nor
39+
# `pull_request` as "run every family", so this trigger alone restores the
40+
# full battery here — no step in this file changes.
41+
#
42+
# ⛔ A NEW TRIGGER KEY, not a widening of `push:`; see ci.yml's `on:` block.
43+
schedule:
44+
- cron: '0 * * * *'
45+
workflow_dispatch:
2846

2947
# ── MEASURED 2026-08-25 (#12211) — a NEGATIVE result, recorded so it is not
3048
# re-measured. After the stale-ledger outage the queue's check set was measured
@@ -68,8 +86,14 @@ on:
6886
# Same policy as ci.yml: superseded runs on the same PR/branch waste runners
6987
# and delay feedback; cancel them. Push runs to main group by commit ref, so an
7088
# in-flight main run is cancelled only by a newer main push.
89+
#
90+
# ⭐ `github.event_name` is in the key for the reason ci.yml's concurrency block
91+
# states in full (#16467): on a `schedule` event the remaining two segments are
92+
# byte-identical to a push to `main`, so without it the next merge cancels the
93+
# hourly full run. `scripts/ci/scheduled-full-run.mjs --check-concurrency`
94+
# evaluates THIS expression too and reds if the two groups ever collapse again.
7195
concurrency:
72-
group: lint-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
96+
group: lint-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
7397
cancel-in-progress: true
7498

7599
# ─────────────────────────────────────────────────────────────────────────────
@@ -3402,6 +3426,35 @@ jobs:
34023426
- name: Test Core package selection self-test
34033427
run: pnpm check:select-shard-packages
34043428

3429+
# The hourly full run's own contract (#16467). Two halves, one script:
3430+
#
3431+
# --self-test the card's IDENTITY, DE-DUP and BODY, driven
3432+
# offline. The filer is a `workflow_run` workflow, so
3433+
# it can only ever run from the default branch and
3434+
# NOTHING on a pull request can exercise it — the
3435+
# same position merge-queue-triage.yml is in. A
3436+
# de-dup rule tested only by the live workflow gets
3437+
# its first real test on the night it files its
3438+
# second duplicate, and this one fires hourly.
3439+
# --check-concurrency
3440+
# evaluates ci.yml's and THIS file's
3441+
# `concurrency.group` expressions against a
3442+
# push-shaped and a schedule-shaped context and reds
3443+
# when the two collapse onto one group. They did
3444+
# until #16467: on a `schedule` event the other two
3445+
# segments are byte-identical to a push to `main`, so
3446+
# `cancel-in-progress` let the next merge kill the
3447+
# hourly run. It also refuses the two degenerate ways
3448+
# to make them differ — a run-unique key, and a
3449+
# constant one — since both switch cancellation off
3450+
# or on for every event in the file.
3451+
#
3452+
# Offline, no network, ~0.1 s.
3453+
- name: Hourly full run self-test and concurrency gate
3454+
run: |
3455+
node scripts/ci/scheduled-full-run.mjs --self-test
3456+
node scripts/ci/scheduled-full-run.mjs --check-concurrency
3457+
34053458
# Gate-family selection self-test (#16496). The "Select the gate families
34063459
# this run pays for" step at the top of this job is a thin call into
34073460
# scripts/ci/select-gate-families.sh, and its `merge_group` branch cannot

0 commit comments

Comments
 (0)