diff --git a/.changeset/console-088e0f705520.md b/.changeset/console-088e0f705520.md new file mode 100644 index 00000000000..d9bfb230f31 --- /dev/null +++ b/.changeset/console-088e0f705520.md @@ -0,0 +1,15 @@ +--- +"@objectstack/console": minor +--- + +Console (objectui) refreshed to `088e0f705520`. Frontend changes in this range: + +Derived from the changesets objectui declared over the range — 3 releasing of 3 changesets added across 3 non-merge commits. + +- **minor** — A record-form field can name one item of a metadata registry by its machine name: a `text` field that declares `widget: 'position-ref'` lists the position registry, and one that d… (objectui `088e0f705`) +- **patch** — A position approver is read through the spec's registry binding, `{ source: 'registry', type }` (objectstack#22710): its readable label and its designer picker come from the posit… (objectui `83b9fade2`) +- **patch** — A rich-text field no longer draws hint lines above its editor (objectui#12110, ruled B). The `markdown`, `html` and `richtext` editors used to show "Format: markdown" (or "Format:… (objectui `2498a0753`) + +Clause-②: no + +objectui range: `e5311165a8d6...088e0f705520` diff --git a/.changeset/objectui-pin-citations-088e0f705520.md b/.changeset/objectui-pin-citations-088e0f705520.md new file mode 100644 index 00000000000..559bffcd42b --- /dev/null +++ b/.changeset/objectui-pin-citations-088e0f705520.md @@ -0,0 +1,19 @@ +--- +'@objectstack/spec': patch +--- + +The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `088e0f705520`. + +Clause-②: no + +Every anchor was mapped through the objectui diff `e5311165a8d6..088e0f705520`: 54 paths over 3 commits (objectui#12110's rich-text hint lines, objectui#12174 and objectui#12175). Files that three records cite changed on the hop, all for objectui#12175, and every cited line in them held or moved with its text byte-identical: + +- `form.tsx` (+3, registering the `position-ref` / `permission-ref` widget hints): `spanLadderFor` held at `:275-302`, and its one call site moved `:3326` -> `:3329` with the call unchanged. So `FormField.span: 'full'` still takes the whole row at every multi-column tier. +- `data-objectstack/src/index.ts` (+33 at `:5335`, the adapter's `listMetadataItems`): this lands below both reads the curated-public-blocks record cites, so `serializeOrderBy` `869-895` and the `translateFilterArray` arm `5066-5077` are unmoved. +- The `en` / `zh` / `de` packs (`fields.registryRef` keys, +4 each): the calendar record's `calendar.configRequired` strings moved by +4 and did not change. + +Every other record cites only files that are byte-identical across the hop (`git diff --quiet`, checked file by file), and each gains a hop sentence that says so. + +The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces every `e5311165a8d6` number. The corpus is now 8519 tracked files, and every retired name each entry counts still reads 0. The lit controls moved with the corpus, for example `objectstack` from 18355 to 18400 and `useState` from 2659 to 2661. + +No key, default, enum member or export moves. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 382ed879727..8bba0be0dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1066,6 +1066,15 @@ jobs: # second red on wall time against `timeout-minutes`. Both constants, their # measured basis and a self-test battery each live in the script. # + # THE RED IS A `schedule` RED ALONE (C2, both timing steps, ruling + # https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511): + # the dataset's weights are measured on the hourly `schedule` runs, so + # only those exit 1 on DRIFT. On `merge_group`, `pull_request`, `push` + # and `workflow_dispatch` the step prints the same verdict, raises the + # DRIFT as a `::warning::` annotation and exits 0. The event reaches the + # script as `--event "$GITHUB_EVENT_NAME"`, which it requires; the rule + # and its self-test live there (TIMING_GRADED_EVENT, timingGateMode). + # # EXECUTED WINDOWS ONLY. Measurements come from samplesFromSummary() in # measure-test-shard-timings.mjs, which drops cache HITs and failed # suites, and the prediction is summed over the same executed packages. @@ -1073,10 +1082,11 @@ jobs: # predicted and vouch for a rotted dataset, so a shard whose packages all # replayed is NOT MEASURED, never "fast". # - # NO `if:` and NO `continue-on-error` (the red is the point), and ABOVE - # the attestation pair for the #6082 reason documented on the upload - # above: a failed unguarded step skips that pair, so a drift red also - # withholds this shard's attestation, the fail-closed direction. A shard + # NO `if:` and NO `continue-on-error` (the red is the point, and the + # `schedule`-only red is the script's `--event`, so neither is needed), + # and ABOVE the attestation pair for the #6082 reason documented on the + # upload above: a failed unguarded step skips that pair, so a drift red + # also withholds this shard's attestation, the fail-closed direction. A shard # with no packages runs no turbo and writes no summary; that is NOT # MEASURED here rather than a usage error from the script. # @@ -1109,7 +1119,7 @@ jobs: exit 0 fi node scripts/partition-test-shards.mjs --check-drift "${SUMMARIES[@]}" \ - --label "Test Core (${{ matrix.shard }}/6)" + --label "Test Core (${{ matrix.shard }}/6)" --event "$GITHUB_EVENT_NAME" # Runs even when the suite failed — that is when it earns its keep. It # answers TWO questions about a red suite, and needs both to be able to @@ -1486,12 +1496,12 @@ jobs: # ── The per-package suite-duration ratchet (#16468) ────────────────── # # A GATE, unlike the two reporting steps above: NO `continue-on-error`, - # so a package whose suite ran past its ceiling reds `Test Core`, naming - # the package and its slowest test files. The ceiling is the package's - # slowest executed run in the refresh window x 1.25, generated into - # scripts/test-shard-timings.json (`ceilings`) by the refresh lane and - # held by every later refresh; it rises only by a ruled entry in the - # script's RULED_CEILING_RAISES. A `provisional` package prints "no + # so on a `schedule` run a package whose suite ran past its ceiling reds + # `Test Core`, naming the package and its slowest test files. The + # ceiling is the package's slowest executed run in the refresh window + # x 1.25, generated into scripts/test-shard-timings.json (`ceilings`) by + # the refresh lane and held by every later refresh; it rises only by a + # ruled entry in the script's RULED_CEILING_RAISES. A `provisional` package prints "no # ceiling: provisional" and is not red; a package absent from the dataset # prints "no ceiling: new package" and is not red until the first refresh # after it lands gives it a ceiling, while a package the dataset knows @@ -1505,6 +1515,20 @@ jobs: # samplesFromSummary, and every vitest file line); a capture that did not # arrive leaves its packages ungraded and says so, never red. # + # EXIT-GRADED ON `schedule` ALONE (C2, both timing steps, ruling + # https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511): + # the ceilings come from the hourly `schedule` runs, so only those red + # this job on a package over its ceiling. On `merge_group`, + # `pull_request`, `push` and `workflow_dispatch` the step prints the same + # table, raises each package over its ceiling as a `::warning::` + # annotation and exits 0; a real slowdown merged to `main` reds the next + # hourly run. A package whose ceiling is under the absolute floor (20s, + # CEILING_FLOOR_SECONDS) prints "below floor" and is never red. The + # event reaches the script as `--event "$GITHUB_EVENT_NAME"`, which it + # requires; a refusal (a malformed table or raise record) is red on every + # event. The rule, the floor's reading and their self-tests live in the + # script. + # # NOT MEASURED, exit 0, while the dataset carries no `ceilings`: they are # generated only where the run-summary artifacts are downloadable (the # refresh lane), so the first refresh after this lands writes them, and @@ -1515,7 +1539,7 @@ jobs: # captured, so a red shard's packages are graded too. - name: Check the per-package suite-duration ceilings if: always() - run: node scripts/check-test-suite-ceilings.mjs --captures "$RUNNER_TEMP/test-timings" --label 'Test Core' + run: node scripts/check-test-suite-ceilings.mjs --captures "$RUNNER_TEMP/test-timings" --label 'Test Core' --event "$GITHUB_EVENT_NAME" # ── Temporal conformance against live, non-UTC servers (ADR-0053 D-A3) ───── diff --git a/.objectui-sha b/.objectui-sha index 28c065313fd..51356616906 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa +088e0f705520e82bee1d65a5fa3c1f7a26672dde diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 0e9d89ff351..b76fc2ea119 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -213,7 +213,7 @@ Column footer summary configuration | **required** | `boolean` | optional | Required override | | **hidden** | `boolean` | optional | Hidden override | | **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. | -| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `e5311165a8d6`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | +| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `088e0f705520`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | | **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | | **language** | `string` | optional | Code editor language (for type=code) | | **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | @@ -343,7 +343,7 @@ Form-view select option — the object-field option shape minus the per-option ` | **required** | `boolean` | optional | Required override | | **hidden** | `boolean` | optional | Hidden override | | **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. | -| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `e5311165a8d6`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | +| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `088e0f705520`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | | **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | | **language** | `string` | optional | Code editor language (for type=code) | | **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | diff --git a/packages/sdui-parser/objectui-lockstep.json b/packages/sdui-parser/objectui-lockstep.json index b61677b2f45..e023aa1fbec 100644 --- a/packages/sdui-parser/objectui-lockstep.json +++ b/packages/sdui-parser/objectui-lockstep.json @@ -6,8 +6,8 @@ ], "objectui": { "repo": "https://github.com/objectstack-ai/objectui.git", - "rev": "e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa", - "revDate": "2026-10-12T01:48:21+00:00", + "rev": "088e0f705520e82bee1d65a5fa3c1f7a26672dde", + "revDate": "2026-10-12T03:19:24+00:00", "source": "packages/sdui-parser/src", "files": [ "packages/sdui-parser/src/body-dialect.ts", @@ -21,7 +21,7 @@ "packages/sdui-parser/src/validate.ts" ] }, - "recordedAgainstPin": "e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa", + "recordedAgainstPin": "088e0f705520e82bee1d65a5fa3c1f7a26672dde", "grammarRegion": { "file": "packages/sdui-parser/src/parse.ts", "delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */", diff --git a/packages/spec/src/data/api-methods-batch-conformance.test.ts b/packages/spec/src/data/api-methods-batch-conformance.test.ts index b520af02c0e..f421e7f549f 100644 --- a/packages/spec/src/data/api-methods-batch-conformance.test.ts +++ b/packages/spec/src/data/api-methods-batch-conformance.test.ts @@ -61,7 +61,10 @@ const SINGLE_RECORD_WRITE_ONLY: Record = { // `revoked` on ONE key. The multi-select surface this rule protects does not // exist for API keys, and the shape a future one would take does not need // `bulk` either — both read off the console build this release pins - // (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + // (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + // every objectui file this record cites is byte-identical across the hop from + // `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + // `e5311165a`, re-read there 2026-10-12: // in `packages/plugin-grid`, on the hop off `499799502`, `ObjectGrid.tsx` // changed again (27 insertions, 11 deletions: objectui#12110's authored // column label and objectui#12143's grouped-pager button names), none of it diff --git a/packages/spec/src/kernel/functional-completeness.ts b/packages/spec/src/kernel/functional-completeness.ts index 04240cf6316..04110e2718b 100644 --- a/packages/spec/src/kernel/functional-completeness.ts +++ b/packages/spec/src/kernel/functional-completeness.ts @@ -298,7 +298,15 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] { * English sentence. So the loss is total rather than partial — every * record, on every object — and the author is told at render time as well * as here. Both reads hold at the pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * on the hop off `e5311165a` `ObjectCalendar.tsx` and `ListView.tsx` are + * byte-identical (`git diff --quiet`), so `getCalendarConfig` `:311`, the + * `if (!calendarConfig)` arm `:1354` with its `tt(…)` lines `:1361-1362` and + * the `case 'calendar'` at `3813` did not move; and the `en` / `zh` / `de` + * packs changed (objectui#12175's `fields.registryRef` keys, +4 each, so + * `en.ts` `:945` -> `:949`, `zh.ts` `:721` -> `:725`, `de.ts` `:701` -> + * `:705`) but not their `calendar.configRequired` strings. At `e5311165a`, + * re-read there 2026-10-12: * on the hop off `499799502` `ObjectCalendar.tsx` and `ListView.tsx` are * byte-identical (`git diff --quiet`), so `getCalendarConfig` `:311`, the * `if (!calendarConfig)` arm `:1354` with its `tt(…)` lines `:1361-1362` and @@ -432,7 +440,10 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] { * This repo already records the same deletion one door over: the #13817 * check in `../ui/view.zod.ts` names objectui#7029 as its runtime half. * - `gantt` → NO fallback, and no silence [#19630]. Measured at the pin - * this repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * this repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -534,6 +545,9 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] { * requires. So the view does not draw a blank chart: it refuses, by name. * - `timeline` → date axis: NO fallback [#19630]; title: `titleField || 'name'`, * which still stands. Measured at the same pin (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -633,7 +647,10 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] { * block does render; the warning still fires there, because the block the * view TYPE names is the one that is missing. Unchanged by this row. * - `map` → NO fallback, and no silence [#19630]. Measured at the same - * pin (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * pin (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: diff --git a/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts index 90016600957..cce439482e5 100644 --- a/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts @@ -52,13 +52,13 @@ export const entry: SemanticMigration = { + 'the audience that does not parse. Measured on 884e8347d: the only in-repo readers are ' + 'packages/core/src/health-monitor.ts and packages/core/src/hot-reload.ts, both moved in ' + 'this same change; and the pinned objectui checkout — the pin this repo builds ' - + 'against, `.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa` — names ' + + 'against, `.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde` — names ' + 'neither def and neither key: all thirteen exports of plugin-lifecycle-advanced.zod.ts and ' - + 'the string debounceDelay each occur 0 times across its 8506 tracked files (0 across the 8441 at 499799502, the 8424 at 0df67f237, the 8351 at 20c6d351a, the 8281 at 47b1f0bb7, the 8234 at f0268ad78, the 7754 at a58626c88, the 7650 at 0abd4f9f8, the 7632 at 9dfaca654, the 7579 at 2e818d0b5, the 10267 at ab1879721, the 10071 at 89cad75d5, the 9912 at 31971ff1e, the 9800 at e420df310, the 9546 at db11afd49, the 9283 at dd3f7e1be, the ' + + 'the string debounceDelay each occur 0 times across its 8519 tracked files (0 across the 8506 at e5311165a, the 8441 at 499799502, the 8424 at 0df67f237, the 8351 at 20c6d351a, the 8281 at 47b1f0bb7, the 8234 at f0268ad78, the 7754 at a58626c88, the 7650 at 0abd4f9f8, the 7632 at 9dfaca654, the 7579 at 2e818d0b5, the 10267 at ab1879721, the 10071 at 89cad75d5, the 9912 at 31971ff1e, the 9800 at e420df310, the 9546 at db11afd49, the 9283 at dd3f7e1be, the ' + '8512 at f8a9d0fb0 and the 8303 at 62597c588 too), against lit ' + 'controls objectstack 12966 and @objectstack/spec 4997 on the same corpus at 87af769e9, ' + 'which re-count to 13125 and 5043 respectively at 62597c588, to 13347 and 5123 at ' - + 'f8a9d0fb0, to 13745 and 5466 at dd3f7e1be, to 14704 and 5545 at db11afd49, to 15352 and 6024 at e420df310, to 15691 and 6206 at 31971ff1e, to 16044 and 6461 at 89cad75d5, to 16377 and 6665 at ab1879721, to 17227 and 7134 at 2e818d0b5, to 17313 and 7186 at 9dfaca654, to 17390 and 7209 at 0abd4f9f8, to 17468 and 7246 at a58626c88, to 17956 and 7522 at f0268ad78, to 17980 and 7523 at 47b1f0bb7, to 18047 and 7545 at 20c6d351a, to 18248 and 7582 at 0df67f237, to 18271 and 7587 at 499799502 and to 18355 and 7599 at this pin (git grep -o -F, the method that reproduces ' + + 'f8a9d0fb0, to 13745 and 5466 at dd3f7e1be, to 14704 and 5545 at db11afd49, to 15352 and 6024 at e420df310, to 15691 and 6206 at 31971ff1e, to 16044 and 6461 at 89cad75d5, to 16377 and 6665 at ab1879721, to 17227 and 7134 at 2e818d0b5, to 17313 and 7186 at 9dfaca654, to 17390 and 7209 at 0abd4f9f8, to 17468 and 7246 at a58626c88, to 17956 and 7522 at f0268ad78, to 17980 and 7523 at 47b1f0bb7, to 18047 and 7545 at 20c6d351a, to 18248 and 7582 at 0df67f237, to 18271 and 7587 at 499799502, to 18355 and 7599 at e5311165a and to 18400 and 7603 at this pin (git grep -o -F, the method that reproduces ' + 'every earlier count).', acceptanceCriteria: 'Every producer and reader of a PluginHealthCheck spells intervalMs and timeoutMs, and every ' diff --git a/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts index 553e5d17a66..310b9ed41d7 100644 --- a/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts @@ -46,10 +46,8 @@ export const entry: SemanticMigration = { + 'spells timeout 0 times; outside the zod file and its test the only live occurrences are the ' + 'generated rows in content/docs/references/kernel/plugin-security-advanced.mdx, which this ' + 'rename regenerates. The pinned objectui checkout — this is the pin we build against, ' - + '`.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa`, re-read from this tree — ' - + 'spells resourceLimits.timeout 0 times across ' - + '8506 tracked files, against lit controls timeout 1706, RuntimeConfig 357 and resourceLimits ' - + '2 on the same corpus (0 across 8441, and 1704 / 352 / 2, at 499799502; 0 across 8424, and 1705 / 347 / 2, at 0df67f237; 0 across 8351, and 1694 / 337 / 2, at 20c6d351a; 0 across 8281, and 1674 / 337 / 2, at 47b1f0bb7; 0 across 8234, and 1658 / 337 / 2, at f0268ad78; 0 across 7754, and 1431 / 299 / 2, at a58626c88; 0 across 7650, and 1360 / 293 / 2, at 0abd4f9f8; 0 across 7632, and 1360 / 293 / 2, at 9dfaca654; 0 across 7579, and 1351 / 276 / 2, at 2e818d0b5; 0 across 10267, and 1348 / 273 / 2, at ab1879721; 0 across 10071, and 1331 / 273 / 2, at 89cad75d5; 0 across 9912, and 1303 / 273 / 2, at 31971ff1e; 0 across 9800, and 1293 / 273 / 2, at e420df310; 0 across 9546, and 1197 / 273 / 2, at db11afd49; 0 across 9283, and 1172 / 263 / 2, at dd3f7e1be; 0 across 8512, and 1096 / 245 / 2, at f8a9d0fb0; 0 across 8303, and ' + + '`.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde`, re-read from this tree — ' + + 'spells resourceLimits.timeout 0 times across 8519 tracked files, against lit controls timeout 1706, RuntimeConfig 357 and resourceLimits 2 on the same corpus (0 across 8506, and 1706 / 357 / 2, at e5311165a; 0 across 8441, and 1704 / 352 / 2, at 499799502; 0 across 8424, and 1705 / 347 / 2, at 0df67f237; 0 across 8351, and 1694 / 337 / 2, at 20c6d351a; 0 across 8281, and 1674 / 337 / 2, at 47b1f0bb7; 0 across 8234, and 1658 / 337 / 2, at f0268ad78; 0 across 7754, and 1431 / 299 / 2, at a58626c88; 0 across 7650, and 1360 / 293 / 2, at 0abd4f9f8; 0 across 7632, and 1360 / 293 / 2, at 9dfaca654; 0 across 7579, and 1351 / 276 / 2, at 2e818d0b5; 0 across 10267, and 1348 / 273 / 2, at ab1879721; 0 across 10071, and 1331 / 273 / 2, at 89cad75d5; 0 across 9912, and 1303 / 273 / 2, at 31971ff1e; 0 across 9800, and 1293 / 273 / 2, at e420df310; 0 across 9546, and 1197 / 273 / 2, at db11afd49; 0 across 9283, and 1172 / 263 / 2, at dd3f7e1be; 0 across 8512, and 1096 / 245 / 2, at f8a9d0fb0; 0 across 8303, and ' + '1086 / 240 / 2, at 62597c588); both resourceLimits hits are prose in packages/app-shell recording ' + 'that objectui\'s own AppShellRuntimeConfig shares not one key with the spec\'s ' + 'RuntimeConfig, so nothing there authors this key and no pin bump is owed. ADR-0087.', diff --git a/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts index f2433a5af67..2bb53edc831 100644 --- a/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts @@ -47,10 +47,9 @@ export const entry: SemanticMigration = { + 'no in-repo runtime reads any of the four — outside `packages/spec/src/system/logging.zod.ts` ' + 'and its test the only occurrences are the generated rows in ' + '`content/docs/references/system/logging.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa` — spells ' + + 'objectui checkout — `.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde` — spells ' + '`flushInterval` 0 times, `initialDelay` 0, `HttpDestinationConfig` 0 and `LoggingConfig` 0 ' - + 'across its 8506 tracked files, against lit controls `useState` 2659 and `timeout` 1706 on ' - + 'the same corpus (all four 0 across 8441, against 2648 and 1704, at 499799502, 0 across 8424, against 2645 and 1705, at 0df67f237, 0 across 8351, against 2630 and 1694, at 20c6d351a, 0 across 8281, against 2630 and 1674, at 47b1f0bb7, 0 across 8234, against 2622 and 1658, at f0268ad78, 0 across 7754, against 2491 and 1431, at a58626c88, 0 across 7650, against 2478 and 1360, at 0abd4f9f8, 0 across 7632, against 2477 and 1360, at 9dfaca654, 0 across 7579, against 2477 and 1351, at 2e818d0b5, 0 across 10267, against 2476 and 1348, at ab1879721, 0 across 10071, against 2470 and 1331, at 89cad75d5, 0 across 9912, against 2469 and 1303, at 31971ff1e, 0 across 9800, against 2464 and 1293, at e420df310, 0 across 9546, against 2449 and 1197, at db11afd49, 0 across 9283, against 2435 and 1172, at dd3f7e1be, 0 across 8512, ' + + 'across its 8519 tracked files, against lit controls `useState` 2661 and `timeout` 1706 on the same corpus (all four 0 across 8506, against 2659 and 1706, at e5311165a, 0 across 8441, against 2648 and 1704, at 499799502, 0 across 8424, against 2645 and 1705, at 0df67f237, 0 across 8351, against 2630 and 1694, at 20c6d351a, 0 across 8281, against 2630 and 1674, at 47b1f0bb7, 0 across 8234, against 2622 and 1658, at f0268ad78, 0 across 7754, against 2491 and 1431, at a58626c88, 0 across 7650, against 2478 and 1360, at 0abd4f9f8, 0 across 7632, against 2477 and 1360, at 9dfaca654, 0 across 7579, against 2477 and 1351, at 2e818d0b5, 0 across 10267, against 2476 and 1348, at ab1879721, 0 across 10071, against 2470 and 1331, at 89cad75d5, 0 across 9912, against 2469 and 1303, at 31971ff1e, 0 across 9800, against 2464 and 1293, at e420df310, 0 across 9546, against 2449 and 1197, at db11afd49, 0 across 9283, against 2435 and 1172, at dd3f7e1be, 0 across 8512, ' + 'against 2391 and 1096, at f8a9d0fb0, and 0 across ' + '8303, against 2389 and 1086, at 62597c588).', acceptanceCriteria: diff --git a/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts index e19824f8376..de4a6814314 100644 --- a/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts @@ -73,10 +73,8 @@ export const entry: SemanticMigration = { + 'against a lit control of 1195 defineStack occurrences on that same corpus at fc28c1d38 ' + '(1195 again at 9b62f54671); and the objectui ' + 'checkout this repo builds against — this is the pin, ' - + '`.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa`, re-read from this tree — ' - + 'spells all six metrics def names and both distinctive keys 0 times across 8506 tracked ' - + 'files at that sha, against lit controls window 4614, timeout 1706, period 251, ' - + 'interval 238 and metrics 456 on that same corpus and sha (0 across 8441, against 4604 / ' + + '`.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde`, re-read from this tree — ' + + 'spells all six metrics def names and both distinctive keys 0 times across 8519 tracked files at that sha, against lit controls window 4615, timeout 1706, period 251, interval 238 and metrics 456 on that same corpus and sha (0 across 8506, against 4614 / 1706 / 251 / 238 / 456, at e5311165a, 0 across 8441, against 4604 / ' + '1704 / 251 / 238 / 456, at 499799502, 0 across 8424, against 4596 / ' + '1705 / 251 / 233 / 456, at 0df67f237, 0 across 8351, against 4470 / ' + '1694 / 249 / 213 / 456, at 20c6d351a, 0 across 8281, against 4449 / ' diff --git a/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts index cad84f0151d..5c9495b59f9 100644 --- a/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts @@ -69,12 +69,10 @@ export const entry: SemanticMigration = { + 'dark control of 0; inside packages/spec the ' + 'only occurrences are tracing.zod.ts, its test, and the generated rows in ' + 'content/docs/references/system/tracing.mdx, which this rename regenerates. And the ' - + 'pinned objectui checkout — `.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa` — names none of it: all 37 exports of ' - + 'tracing.zod.ts and each of the four key names occur 0 times across the 8506 files ' - + 'tracked at that sha (the 519 Span and 57 SpanSchema hits are objectui\'s own HTML ' + + 'pinned objectui checkout — `.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde` — names none of it: all 37 exports of ' + + 'tracing.zod.ts and each of the four key names occur 0 times across the 8519 files tracked at that sha (the 519 Span and 57 SpanSchema hits are objectui\'s own HTML ' + 'text-span component, TextSpanSchema, an unrelated name, plus colSpan and prose), against ' - + 'two lit controls on that same corpus and sha: 18355 hits for the bare token objectstack, ' - + 'and 7599 for the package specifier @objectstack/spec (at 499799502: 0 across 8441, Span 517, ' + + 'two lit controls on that same corpus and sha: 18400 hits for the bare token objectstack, and 7603 for the package specifier @objectstack/spec (at e5311165a: 0 across 8506, Span 519, 18355 and 7599; at 499799502: 0 across 8441, Span 517, ' + '18271 and 7587; at 0df67f237: 0 across 8424, Span 517, ' + '18248 and 7582; at 20c6d351a: 0 across 8351, Span 517, ' + '18047 and 7545; at 47b1f0bb7: 0 across 8281, Span 517, ' diff --git a/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts index ba320e0e24d..405d751c56e 100644 --- a/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts @@ -31,9 +31,7 @@ export const entry: SemanticMigration = { + 'bd25e897dc: no in-repo runtime reads the key — outside `packages/spec/src/system/tenant.zod.ts` ' + 'and its test the only occurrences are the four generated rows in ' + '`content/docs/references/system/tenant.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa` — spells it 0 ' - + 'times across 8506 tracked files, against lit controls `TTL` 184 and `tenant` 1365 on the ' - + 'same corpus (0 across 8441, against 184 and 1362, at 499799502; 0 across 8424, against 184 and 1357, at 0df67f237; 0 across 8351, against 184 and 1340, at 20c6d351a; 0 across 8281, against 184 and 1338, at 47b1f0bb7; 0 across 8234, against 184 and 1338, at f0268ad78; 0 across 7754, against 184 and 1319, at a58626c88; 0 across 7650, against 182 and 1318, at 0abd4f9f8; 0 across 7632, against 182 and 1318, at 9dfaca654; 0 across 7579, against 182 and 1317, at 2e818d0b5; 0 across 10267, against 180 and 1238, at ab1879721; 0 across 10071, against 181 and 1237, at 89cad75d5; 0 across 9912, against 181 and 1237, at 31971ff1e; 0 across 9800, against 181 and 1235, at e420df310; 0 across 9546, against 181 and 1200, at db11afd49; 0 across 9283, against 181 and 1185, at dd3f7e1be; 0 across 8512, against 156 and 1034, at f8a9d0fb0; 0 across 8303, against 156 ' + + 'objectui checkout — `.objectui-sha` = `088e0f705520e82bee1d65a5fa3c1f7a26672dde` — spells it 0 times across 8519 tracked files, against lit controls `TTL` 184 and `tenant` 1365 on the same corpus (0 across 8506, against 184 and 1365, at e5311165a; 0 across 8441, against 184 and 1362, at 499799502; 0 across 8424, against 184 and 1357, at 0df67f237; 0 across 8351, against 184 and 1340, at 20c6d351a; 0 across 8281, against 184 and 1338, at 47b1f0bb7; 0 across 8234, against 184 and 1338, at f0268ad78; 0 across 7754, against 184 and 1319, at a58626c88; 0 across 7650, against 182 and 1318, at 0abd4f9f8; 0 across 7632, against 182 and 1318, at 9dfaca654; 0 across 7579, against 182 and 1317, at 2e818d0b5; 0 across 10267, against 180 and 1238, at ab1879721; 0 across 10071, against 181 and 1237, at 89cad75d5; 0 across 9912, against 181 and 1237, at 31971ff1e; 0 across 9800, against 181 and 1235, at e420df310; 0 across 9546, against 181 and 1200, at db11afd49; 0 across 9283, against 181 and 1185, at dd3f7e1be; 0 across 8512, against 156 and 1034, at f8a9d0fb0; 0 across 8303, against 156 ' + 'and 987, at 62597c588).', acceptanceCriteria: 'Every schema-level tenant isolation source spells `performance.schemaCacheTtlSeconds`; ' diff --git a/packages/spec/src/ui/action-outcome-messages.test.ts b/packages/spec/src/ui/action-outcome-messages.test.ts index fad5fa7af12..b565c638dce 100644 --- a/packages/spec/src/ui/action-outcome-messages.test.ts +++ b/packages/spec/src/ui/action-outcome-messages.test.ts @@ -12,6 +12,9 @@ * runner default) and the `${result.*}` INTERPOLATION are the console's — the * reader is objectstack-ai/objectui#11344, a later link of the same ruling, * carried from the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 0d610aa0e9e..c53e960c3b6 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -1364,6 +1364,9 @@ const actionObject = () => strictObject({ * * Liveness: the ledger row was `planned` until the console reader landed, * and it is `live` from the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index d8bd8f1e34d..5c7fed8f171 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -288,7 +288,10 @@ describe('PageAccordionProps variant — declared because the accordion renderer // same file's `ComponentRegistry.register('accordion', …)` publishes the key to // the Studio block designer at `:1222` (the `items` input, documented as // `[{ label, icon?, collapsed?, children }]`). Measured at the pin this repo -// builds against — `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// builds against — `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: @@ -460,7 +463,10 @@ describe('PageTabsProps items[].value / items[].count — declared because the t // same file's `ComponentRegistry.register('tabs', …)` publishes the key to the // Studio block designer at `:1007` (the `items` input, documented as // `[{ label, value?, icon?, count?, visibleWhen?, children }]`). Measured at -// the pin this repo builds against — `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// the pin this repo builds against — `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: @@ -3505,7 +3511,10 @@ describe('object-* block props schemas — declared, so the props gate has a sch // #16503 — the spec half of objectui#8172 (decision batch #68, 2026-09-07, // option A: the contract declares the capability that already ships, is // documented and is in use). Measured at the objectui pin this repo builds -// against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: @@ -3722,7 +3731,10 @@ describe('ObjectKanbanPropsSchema limit — the row cap four objectui faces alre // since retired that block, objectui#8257). Unlike `limit` above — a key four // objectui faces already implemented, so the spec was the half that was wrong // — `quickAdd` was FORWARDED and never read: at the pin this repo builds -// against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: @@ -3928,7 +3940,10 @@ describe('ObjectKanbanPropsSchema quickAdd is retired', () => { // #9881 and commit 60e0f900a recorded the accordion and tab items; these two close the set. // // The button record re-measured at the pin this repo builds against — -// `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: @@ -4117,7 +4132,10 @@ describe('ObjectMetricPropsSchema icon liveness', () => { // // The acceptance the card names, pinned: each row's KEY SET is the one the // renderer's read points support at the pin this repo builds against -// (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: +// (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: +// every objectui file this record cites is byte-identical across the hop from +// `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At +// `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At // `499799502`, re-read there 2026-10-11: diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 2530a86b209..357ca027cf7 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -840,7 +840,10 @@ export const PageTabsProps = strictObject({ * false candidate a component over). * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -1086,7 +1089,10 @@ export const PageCardProps = strictObject({ * declarations identical. * * Each clause is read off the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a8d6`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a8d6` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a8d6`, re-read there 2026-10-12: * across the hop from `4997995022f8` `record-details.tsx`, * `record-highlights.tsx`, `record-quick-actions.tsx` and the three * `permissions` files are byte-identical (`git diff --quiet`), so NO anchor in @@ -2490,7 +2496,10 @@ export const PageAccordionProps = strictObject({ * re-derive the same false candidate). * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -2927,7 +2936,10 @@ export const ElementButtonPropsSchema = lazySchema(() => strictObject({ * the button. * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -3405,7 +3417,16 @@ export const ElementTextInputPropsSchema = lazySchema(() => strictObject({ * had no row for — `action:button`, `action:group`, `action:menu`, * `action:icon`, `element:definition-list`, `element:repeater` * (`core/src/registry/public-blocks.ts:117-122` at the pin this repo builds - * against, `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * against, `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * `public-blocks.ts`, `auto-trigger.ts`, `static-params.ts`, `ui/button.tsx`, + * the four `action-*.tsx` renderers, `basic/data-list.tsx` and + * `previews/block-config.ts` are byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so their anchors held unmoved; and + * `data-objectstack/src/index.ts` changed (+33 at `:5335`, objectui#12175, + * objectui `088e0f705`: the adapter's `listMetadataItems`, which the + * registry-name pickers read) below both cited reads, which left + * `serializeOrderBy` `869-895` and the `translateFilterArray` arm `5066-5077` + * unmoved. At `e5311165a`, re-read there 2026-10-12: * `public-blocks.ts`, `auto-trigger.ts`, `static-params.ts`, `ui/button.tsx`, * the four `action-*.tsx` renderers, `basic/data-list.tsx` and * `previews/block-config.ts` are byte-identical across the hop from @@ -3986,7 +4007,10 @@ export type ActionIconPropsParsed = z.infer; * inventory. A member's `params` is its `ActionParam[]` input list: both * containers forward an array as `actionParams` (group `:346-348`, menu * `:270-272`, re-read at the pin this repo builds against, `.objectui-sha` = - * `e5311165a`, 2026-10-12: `action-group.tsx`, `action-menu.tsx` and + * `088e0f705`, 2026-10-12: `action-group.tsx`, `action-menu.tsx` and + * `static-params.ts` are byte-identical across the hop from `e5311165a` (`git + * diff --quiet`), so both held unmoved; at `e5311165a`, 2026-10-12: + * `action-group.tsx`, `action-menu.tsx` and * `static-params.ts` are byte-identical across the hop from `499799502` (`git * diff --quiet`), so both held unmoved; at `499799502`, 2026-10-11: * `action-group.tsx`, `action-menu.tsx` and @@ -4683,7 +4707,10 @@ const GridOperationsSchema = lazySchema(() => strictObject({ * control `schema.editable` in `ObjectGrid.tsx`. It was declared ahead of its * reader on purpose (the BUILD objectui#11068 chose), and its describe carried * the `[EXPERIMENTAL — not enforced]` marker that said so. Re-measured at the - * pin this repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * pin this repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * `ObjectGrid.tsx` changed across the hop from `499799502` (+27/-11: * objectui#12110, objectui `5330038f4`, and objectui#12143, objectui * `2381c07f3`) and `data-table.tsx` changed too (+12/-1: objectui#12124 and @@ -4815,7 +4842,10 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ * same members, and objectui's grid follows this declaration. * * ⚠️ That gap is closed at the pin this repo builds against (`.objectui-sha` - * = `e5311165a`, re-read there 2026-10-12: + * = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * `ObjectGrid.tsx` changed across the hop from `499799502` (+27/-11: * objectui#12110 and objectui#12143), every hunk above both reads (the last * at `:6592`, a pager button's name written in place), so both MOVED by 16 @@ -5223,7 +5253,10 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ * `ComponentPropsMap` key. This record said to drop the marker in the change * that lands the BUILD at the pin, and it is dropped here. * - * Read at the pin this repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * Read at the pin this repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * `ObjectGrid.tsx` changed across the hop from `499799502` (+27/-11: * objectui#12110 and objectui#12143) and `data-table.tsx` changed (+12/-1: * objectui#12124 and objectui#12145: seven lines of default translations @@ -5676,7 +5709,10 @@ export const ObjectMetricPropsSchema = lazySchema(() => strictObject({ * same record for the metric tile. * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -5939,7 +5975,10 @@ const ObjectKanbanLaneSchema = lazySchema(() => strictObject({ * DESIGNER's spelling with * zero read points (#7973 class) — aliased to the `groupBy` the board reads. * `limit` (#16503) was measured later, at the pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -6063,7 +6102,10 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * Row cap (#16503 — the spec half of objectui#8172; decision batch #68, * 2026-09-07, option A: the contract declares the capability that already * ships, is documented and is in use). Measured at the objectui pin this - * repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -6359,7 +6401,10 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * quick-add control and no block a document can name offers one either). * * Measured at the objectui pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -6584,6 +6629,9 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * element schemas). * * Measured at the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -6816,6 +6864,9 @@ export const ObjectCalendarPropsSchema = lazySchema(() => strictObject({ * `object-kanban`'s above. * * Measured at the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -8045,7 +8096,10 @@ const OBJECT_MAP_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ /** * `object-map` (objectui `plugin-map/src/ObjectMap.tsx` plus the registry shell * `plugin-map/src/index.tsx`, read at the pin this repo builds against — - * `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -8249,7 +8303,10 @@ export const ObjectMapPropsSchema = lazySchema(() => strictObject({ * Base query filter — the `ViewFilterRule` ARRAY form, the one filter * orthography every `filter` door in this map shares (ui#6206-B reaching the * `object-*` family: #15449, decision batch #55, option A). Measured at the - * pin this repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * pin this repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -8332,7 +8389,10 @@ export const ObjectMapPropsSchema = lazySchema(() => strictObject({ * Marker order — the `SortItem` ARRAY form, the one sort orthography every * DECLARED `sort` door on this platform carries (objectui#8221, decision batch * #77, option B). Measured at the pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -8537,7 +8597,10 @@ function objectGanttMarker() { /** * `object-gantt` (objectui `plugin-gantt/src/ObjectGantt.tsx` plus the registry * shell `plugin-gantt/src/index.tsx`, read at the pin this repo builds against - * — `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * — `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -8814,7 +8877,10 @@ export type ObjectGanttPropsParsed = z.infer; * they flatten `options.tree` (`ListView.tsx:4120-4144`, `case 'tree'`: the * product carries these keys, the EFFECTIVE `filter` objectui#10250 added, and * NO `tree` key). Both halves re-READ at the - * pin this repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * pin this repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -8965,7 +9031,10 @@ const OBJECT_TREE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ /** * `object-tree` (objectui `plugin-tree/src/ObjectTree.tsx` plus the registry * shell `plugin-tree/src/index.tsx`, read at the pin this repo builds against - * — `.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * — `.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -9167,7 +9236,10 @@ export const ObjectTreePropsSchema = lazySchema(() => strictObject({ * holds for them because each registers through `ElementDataSourceGate`, * which lowers the spec binding onto `objectName` before the renderer sees * the node. `plugin-tree/src/index.tsx` does NOT: at the pin this repo - * builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * re-COUNTED by the same method, 0 for the tree, 3 each for `plugin-gantt`, * `plugin-grid` and `plugin-calendar` and 4 for `plugin-map`, all five * shells byte-identical across the hop from `499799502` (`git diff @@ -9582,7 +9654,10 @@ function objectTimelineItemsFitVariant() { * `object-timeline` (objectui `plugin-timeline/src/ObjectTimeline.tsx`, the * presentational `plugin-timeline/src/renderer.tsx` it composes into, and the * registry shell `plugin-timeline/src/index.tsx` — all read at the pin this - * repo builds against (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * repo builds against (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At + * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At * `499799502`, re-read there 2026-10-11: @@ -9845,6 +9920,9 @@ export const ObjectTimelinePropsSchema = lazySchema(() => strictObject({ * first publishes a refusal for a key the renderer honours. * * Read points at the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -10214,6 +10292,9 @@ export const ComponentPropsMap = { // set from this map's keys), on the three-part evidence that vocabulary's // string-arm ledger asks of a type admitted without an enum member — all // measured at the pin this repo builds against (`.objectui-sha` = + // `088e0f705`, re-read there 2026-10-12: + // every objectui file this record cites is byte-identical across the hop from + // `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At // `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -10326,6 +10407,9 @@ export const ComponentPropsMap = { // authority for map and gantt while tree's rung-1 `data` read stayed // undeclared on every face. Key sets measured from the renderers' read // points at the pin this repo builds against (`.objectui-sha` = + // `088e0f705`, re-read there 2026-10-12: + // every objectui file this record cites is byte-identical across the hop from + // `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At // `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At @@ -10419,6 +10503,9 @@ export const ComponentPropsMap = { // it — so `object-timeline` was unjudged in both directions, a real key and // a typo riding through alike. Key set measured from the renderer's read // points at the pin this repo builds against (`.objectui-sha` = + // `088e0f705`, re-read there 2026-10-12: + // every objectui file this record cites is byte-identical across the hop from + // `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At // `e5311165a`, re-read there 2026-10-12: // every objectui file this record cites is byte-identical across the hop from // `499799502` (`git diff --quiet`), so every anchor held unmoved. At diff --git a/packages/spec/src/ui/dataset.zod.ts b/packages/spec/src/ui/dataset.zod.ts index d078c001db4..f93ad680771 100644 --- a/packages/spec/src/ui/dataset.zod.ts +++ b/packages/spec/src/ui/dataset.zod.ts @@ -283,6 +283,9 @@ export const DatasetMeasureSchema = lazySchema(() => strictObject({ * and this docblock and the `describe` beneath it both said so. * * Measured at the pin this repo builds against (`.objectui-sha` = + * `088e0f705`, re-read there 2026-10-12: + * every objectui file this record cites is byte-identical across the hop from + * `e5311165a` (`git diff --quiet`), so every anchor held unmoved. At * `e5311165a`, re-read there 2026-10-12: * every objectui file this record cites is byte-identical across the hop from * `499799502` (`git diff --quiet`), so every anchor held unmoved. At diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 93efd15707f..ae261004f41 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -3429,7 +3429,16 @@ const FormFieldBaseSchema = lazySchema(() => { * inside the `53ded82bf7...87af769e9` range, so the widest-tier-only * under-span this block used to record (#17328: one cell of two at * 720px) no longer reproduces at the pin this repo builds against - * (`.objectui-sha` = `e5311165a`, re-read there 2026-10-12: + * (`.objectui-sha` = `088e0f705`, re-read there 2026-10-12: + * `autoLayout.ts` and `fields`' `field-type-alias.ts` are byte-identical to + * `e5311165a` (`git diff --quiet`), so `resolveColSpan` `:154`, + * `WIDE_FIELD_TYPES` `:58-69` and the `repeater` -> `field:grid` mapping held + * unmoved, and `form.tsx` changed on this hop — 3 insertions: objectui#12175 + * (objectui `088e0f705`) registers `position-ref` / `permission-ref` among + * the widget hints — none of it inside `spanLadderFor`, which held at + * `:275-302` byte-identical, and its one call site MOVED `:3326` -> `:3329` + * with the call byte-identical, so it still emits the ladder. At + * `e5311165a`, re-read there 2026-10-12: * `autoLayout.ts` and `fields`' `field-type-alias.ts` are byte-identical to * `499799502` (`git diff --quiet`), so `resolveColSpan` `:154`, * `WIDE_FIELD_TYPES` `:58-69` and the `repeater` -> `field:grid` mapping held @@ -3543,7 +3552,7 @@ const FormFieldBaseSchema = lazySchema(() => { * had changed only in its registration's input list, objectui#9910's * `children` slot; at `62597c588` it was byte-identical to `87af769e9`). */ - span: z.enum(['auto', 'full']).default('auto').describe("Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `e5311165a8d6`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest."), + span: z.enum(['auto', 'full']).default('auto').describe("Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `088e0f705520`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest."), /** Custom widget override — only needed when auto-inference is insufficient */ widget: z.string().optional().describe('Custom widget/component name (overrides type-based inference)'), diff --git a/scripts/check-test-suite-ceilings.mjs b/scripts/check-test-suite-ceilings.mjs index 570a0ceaa04..cc855740c0d 100644 --- a/scripts/check-test-suite-ceilings.mjs +++ b/scripts/check-test-suite-ceilings.mjs @@ -2,11 +2,12 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * Per-package suite-duration ratchet (#16468): a run in which a package's test - * suite took longer than its measured ceiling is RED, naming the package and - * its slowest test files, and a ceiling rises only by a ruling. + * Per-package suite-duration ratchet (#16468): a `schedule` run in which a + * package's test suite took longer than its measured ceiling is RED, naming the + * package and its slowest test files; every other run reports the same reading + * as a warning; and a ceiling rises only by a ruling. * - * node scripts/check-test-suite-ceilings.mjs --captures [--label ] + * node scripts/check-test-suite-ceilings.mjs --captures --event [--label ] * node scripts/check-test-suite-ceilings.mjs --self-test * * ## Why it exists @@ -39,6 +40,52 @@ * - A ceiling rises only by a ruling: RULED_CEILING_RAISES, each entry naming * the ruling's comment URL. Nothing else in this file moves a ceiling. * + * ## Exit-graded on `schedule` alone; every other event reports (C2) + * + * Ruling (letter C2, for both timing steps, plus an absolute floor): + * https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511 + * "a timing gate exit-grades only on the run profile its baseline was measured + * on; everywhere else it reports." The ceilings come from the refresh window, + * which reads ci.yml's hourly `schedule` runs alone; those replay part of the + * workspace from the turbo cache, while a merge-queue or PR build executes far + * more of it on the same runners. Graded against that window, 9 of the 19 queue + * builds on the morning the table went live redded on packages no PR in them + * touched, and PR heads did too, while the hourly run read green. + * + * So `--event` is required, and the mode is TIMING_GRADED_EVENT's, shared with + * the drift step (timingGateMode in partition-test-shards.mjs): + * + * schedule exit-graded, as before: a package over its ceiling exits 1 + * with an `::error::` annotation naming it + * every other REPORT ONLY (merge_group, pull_request, push, + * event workflow_dispatch): the same table prints, each package over + * its ceiling is a `::warning::` annotation, and it exits 0 + * + * A real slowdown merged to `main` reds the next hourly run, which names the + * package and its slowest files. A refusal -- a malformed raise record or + * ceiling table, or a missing `--event` -- is not a timing reading, does not + * vary with runner load, and exits 1 on every event. + * + * ⚠ The hourly run grades only the packages it EXECUTES: a turbo cache replay + * carries no window, so it is never graded. A package the hourly run replays on + * every run (ci.yml's `timeout-minutes` note: "scheduled runs mostly replay the + * cache their commit's push run saved") is graded on any hourly run that does + * execute it and reported everywhere else; nothing here forces one to. + * + * ## The absolute floor: CEILING_FLOOR_SECONDS, 20 s + * + * Same ruling: "a package whose ceiling is below the floor is never red; it + * prints 'below floor'". The floor is read off the table's distribution by the + * `domain:devx` seat and is not below 20 s. The reading: on `1f312fdfa` the 72 + * ceilings run 3.40, 3.69, 4.24, 5.18, 6.39, 9.10 and 16.23 s, then 22.71 s + * and up, and 20 s sits in the widest gap of the low band (16.23 -> 22.71). The + * seven packages under it then were knowledge-ragflow, service-cache, + * observability, sdui-parser, knowledge-memory, embedder-openai and + * client-react. A 3.4 s ceiling crossed by a 4.2 s run is noise, not a + * regression. The floor is compared with the EFFECTIVE ceiling, so a ruled + * raise to the floor or above grades the package again; the formula, its + * x 1.25 and RULED_CEILING_RAISES are unchanged. + * * ## No table yet is NOT MEASURED, never red * * The ceilings can only be generated where the run-summary artifacts can be @@ -94,10 +141,11 @@ * ## Exit codes * * 0 OK, or NOT MEASURED (no table yet; nothing executed; no captures); a - * new package alone never makes it 1 - * 1 a package over its ceiling, or a refusal (a malformed ceiling table -- - * a package the dataset knows that lost its row among them -- or raise - * record) + * new package or one below the floor never makes it 1; and on any event + * but `schedule`, a package over its ceiling (reported as a warning) + * 1 a package over its ceiling on a `schedule` run, or a refusal on any + * event (a malformed ceiling table -- a package the dataset knows that + * lost its row among them -- or raise record, or no `--event`) */ import { readFileSync, readdirSync, statSync } from 'node:fs'; @@ -106,7 +154,12 @@ import { fileURLToPath } from 'node:url'; import process from 'node:process'; import { isEntrypoint } from './invoked-as.mjs'; -import { escapeWorkflowCommandMessage } from './partition-test-shards.mjs'; +import { + TIMING_GRADED_EVENT, + escapeWorkflowCommandMessage, + timingGateMode, + timingModeNote, +} from './partition-test-shards.mjs'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const DATASET_PATH = path.join(REPO_ROOT, 'scripts', 'test-shard-timings.json'); @@ -115,6 +168,13 @@ const DATASET_REL = 'scripts/test-shard-timings.json'; /** How many of a red package's slowest test files the verdict names. */ export const SLOWEST_FILES_NAMED = 5; +/** + * The absolute floor (C2 ruling): a package whose effective ceiling is below it + * is never red and prints "below floor". Its reading and ruling are in the + * header, "The absolute floor". + */ +export const CEILING_FLOOR_SECONDS = 20; + // ── The ruled raises ────────────────────────────────────────────────────── // // A ceiling rises ONLY here, and only on a ruling. One entry per package: @@ -241,8 +301,9 @@ export function readCeilingTable(dataset) { * Returns `{ table, rows, problems }`; each row is one executed package with a * `status`: * - * over graded, over its effective ceiling RED + * over graded, over its effective ceiling RED on `schedule`, a warning elsewhere * ok graded, within its effective ceiling + * below-floor effective ceiling under CEILING_FLOOR_SECONDS never red * new not in the dataset, and no ruled raise no ceiling, not red * provisional in the dataset's `provisional` list no ceiling, not red * uncapped `uncapped` with a reason (`carried`) no ceiling, not red @@ -297,6 +358,11 @@ export function gradeCeilings({ merged, dataset, raises = RULED_CEILING_RAISES } continue; } row.ceiling = Math.max(generated ?? 0, raise?.seconds ?? 0); + // The absolute floor: strict `<`, so a ceiling AT the floor is graded. + if (row.ceiling < CEILING_FLOOR_SECONDS) { + row.status = 'below-floor'; + continue; + } // Strict `>`: a run AT its ceiling is within it, like the drift bound. row.status = seconds > row.ceiling ? 'over' : 'ok'; } @@ -335,8 +401,13 @@ function ceilingText(row) { * The verdict a runner receives, rendered without printing it so the * self-test can read it: `out` is stdout (workflow-command annotations), * `err` the human log. + * + * `event` is the run's (`--event`, required). Off TIMING_GRADED_EVENT a + * package over its ceiling prints the same table, is a `::warning::` + * annotation, and exits 0 (C2); a refusal exits 1 on every event. */ -export function renderCeilingVerdict(report, label) { +export function renderCeilingVerdict(report, label, event) { + const mode = timingGateMode(event); const head = `suite-ceiling`; if (report.problems.length > 0) { return { @@ -378,6 +449,12 @@ export function renderCeilingVerdict(report, label) { return ` -- ${r.name}: ${r.seconds.toFixed(2)}s -- no ceiling: provisional`; case 'uncapped': return ` -- ${r.name}: ${r.seconds.toFixed(2)}s -- no ceiling: ${r.reason}`; + case 'below-floor': + return ( + ` -- ${r.name}: ${r.seconds.toFixed(2)}s against ${r.ceiling.toFixed(2)}s ` + + `(${(r.seconds / r.ceiling).toFixed(2)}x)${sliceNote(r)} -- below floor: a ceiling under ` + + `${CEILING_FLOOR_SECONDS}s is never red` + ); case 'partial': return ` -- ${r.name}: NOT MEASURED -- a part of its suite is missing from this run${sliceNote(r)}; a part is never graded as the whole`; case 'new': @@ -395,18 +472,22 @@ export function renderCeilingVerdict(report, label) { const basis = `ceilings from ${DATASET_REL}${report.table.measuredAt ? ` (refreshed ${report.table.measuredAt})` : ''}: ` + `each package's slowest executed run x ${report.table.headroom ?? '?'} when first set, held since`; + const belowFloor = report.rows.filter((r) => r.status === 'below-floor').length; + const floorNote = belowFloor ? `; ${belowFloor} below the ${CEILING_FLOOR_SECONDS}s floor, never red` : ''; + const note = `\n ${timingModeNote(event)}.`; if (red.length === 0) { const verdict = graded.length === 0 ? 'NOT MEASURED' : 'OK'; const summaryLine = graded.length === 0 - ? `${head}: NOT MEASURED -- ${label}: no executed package carried a ceiling to grade against (${report.rows.length} executed).` - : `${head}: OK -- ${label}: ${graded.length} package(s) graded, every one within its ceiling.`; + ? `${head}: NOT MEASURED -- ${label}: no executed package carried a ceiling at or above the ` + + `${CEILING_FLOOR_SECONDS}s floor to grade against (${report.rows.length} executed${floorNote}).` + : `${head}: OK -- ${label}: ${graded.length} package(s) graded, every one within its ceiling${floorNote}.`; return { verdict, exitCode: 0, out: [], - err: [`${summaryLine}\n ${basis}.${lines.length ? `\n${lines.join('\n')}` : ''}${stale}`], + err: [`${summaryLine}\n ${basis}.${lines.length ? `\n${lines.join('\n')}` : ''}${stale}${note}`], }; } @@ -417,25 +498,37 @@ export function renderCeilingVerdict(report, label) { `(${(r.seconds / r.ceiling).toFixed(2)}x; ${ceilingText(r)})${sliceNote(r)}. Its slowest test files:\n${filesText(r)}` ) .join('\n'); + const reading = (r) => + `${r.name}: ${r.seconds.toFixed(2)}s, over its ${r.ceiling.toFixed(2)}s ceiling. Slowest file: ` + + `${r.files[0] ? `${r.files[0].file} ${(r.files[0].ms / 1000).toFixed(2)}s` : '(none captured)'}.`; + const text = + `${head}: OVER -- ${label}: ${red.length} package(s) over their suite-duration ceiling.\n` + + `${detail}\n` + + ` ${basis}.\n` + + ' Make the suite faster, or take the raise to a ruling: a ceiling rises only by an entry in\n' + + ' RULED_CEILING_RAISES (scripts/check-test-suite-ceilings.mjs) naming the ruling comment. ⛔ Never\n' + + ` hand-edit \`ceilings\` in ${DATASET_REL} -- it is generated, and the next refresh holds what it finds.\n` + + ` Every executed package this run:\n${lines.join('\n')}${stale}${note}`; + if (mode === 'report') { + return { + verdict: 'OVER', + exitCode: 0, + out: red.map( + (r) => + `::warning title=Suite-duration ceiling::${escapeWorkflowCommandMessage( + `${reading(r)} Report only on a \`${event}\` run: this gate exit-grades on \`${TIMING_GRADED_EVENT}\` ` + + 'runs alone, the profile its ceilings were measured on, so a slowdown that is real reds the next ' + + 'hourly run on main.' + )}` + ), + err: [text], + }; + } return { verdict: 'OVER', exitCode: 1, - out: red.map( - (r) => - `::error title=Suite-duration ceiling::${escapeWorkflowCommandMessage( - `${r.name}: ${r.seconds.toFixed(2)}s, over its ${r.ceiling.toFixed(2)}s ceiling. Slowest file: ` + - `${r.files[0] ? `${r.files[0].file} ${(r.files[0].ms / 1000).toFixed(2)}s` : '(none captured)'}.` - )}` - ), - err: [ - `${head}: OVER -- ${label}: ${red.length} package(s) over their suite-duration ceiling.\n` + - `${detail}\n` + - ` ${basis}.\n` + - ' Make the suite faster, or take the raise to a ruling: a ceiling rises only by an entry in\n' + - ' RULED_CEILING_RAISES (scripts/check-test-suite-ceilings.mjs) naming the ruling comment. ⛔ Never\n' + - ` hand-edit \`ceilings\` in ${DATASET_REL} -- it is generated, and the next refresh holds what it finds.\n` + - ` Every executed package this run:\n${lines.join('\n')}${stale}`, - ], + out: red.map((r) => `::error title=Suite-duration ceiling::${escapeWorkflowCommandMessage(reading(r))}`), + err: [text], }; } @@ -541,11 +634,13 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'file-sliced packages are summed across shards': 6, 'a ruled raise lifts a ceiling, and only a well-formed one does': 8, 'a malformed ceiling table is refused, never read as a pass': 4, + 'the absolute floor: a ceiling under CEILING_FLOOR_SECONDS is never red, and prints "below floor"': 8, + 'off `schedule` the gate reports, never reds (C2)': 8, }); // Deleting an entry silences that battery's floor as effectively as zeroing // it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 7; +const SELF_TEST_BATTERY_FLOOR = 9; const UNATTRIBUTED_BATTERY = '(no battery open)'; @@ -594,9 +689,11 @@ function selfTest() { provenance: { measuredAt: '2026-10-12', ceilingHeadroom: 1.25 }, ...extra, }); - const run = (captures, ds = dataset(), raises = {}) => { + // `event` defaults to the graded one, so every battery above C2's reads the + // exit-graded verdict it was written against. + const run = (captures, ds = dataset(), raises = {}, event = TIMING_GRADED_EVENT) => { const report = gradeCeilings({ merged: foldCaptures(captures), dataset: ds, raises }); - return { report, ...renderCeilingVerdict(report, 'Test Core') }; + return { report, ...renderCeilingVerdict(report, 'Test Core', event) }; }; const status = (r, name) => r.report.rows.find((row) => row.name === name)?.status; const raiseUrl = 'https://github.com/objectstack-ai/objectstack/issues/1#issuecomment-2'; @@ -849,6 +946,132 @@ function selfTest() { if (!threw(() => readCeilingTable({ ceilings: {} }))) throw new Error('table: a dataset with no `packages` was accepted'); }); + battery('the absolute floor: a ceiling under CEILING_FLOOR_SECONDS is never red, and prints "below floor"'); + // The ruling's two readings: a 3.40 s ceiling (knowledge-ragflow's) crossed + // at 4.2 s, and the lowest ceiling above the floor, 22.71 s, crossed at 25 s. + const floorSet = dataset({ + packages: { ...dataset().packages, tiny: 2.72, small: 18.17, edge: 16 }, + ceilings: { ...dataset().ceilings, tiny: 3.4, small: 22.71, edge: CEILING_FLOOR_SECONDS }, + }); + check(() => { + if (!(CEILING_FLOOR_SECONDS >= 20)) throw new Error(`floor: ${CEILING_FLOOR_SECONDS}s is below the ruled minimum of 20s`); + }); + check(() => { + const r = run([capture('1/6', [['tiny', 4.2], ['a', 10]])], floorSet); + if (r.exitCode !== 0 || r.verdict !== 'OK' || status(r, 'tiny') !== 'below-floor' || r.out.length !== 0) { + throw new Error(`floor: a 3.40s ceiling run at 4.20s read ${status(r, 'tiny')} / ${r.verdict}, exit ${r.exitCode}`); + } + }); + check(() => { + const r = run([capture('1/6', [['tiny', 4.2], ['a', 10]])], floorSet); + if (!r.err[0].includes('tiny: 4.20s against 3.40s (1.24x) -- below floor') || !r.err[0].includes('1 below the 20s floor')) { + throw new Error(`floor: the line does not say "below floor":\n${r.err[0]}`); + } + }); + check(() => { + // Off schedule too: below the floor is never a warning either. + const r = run([capture('1/6', [['tiny', 4.2], ['a', 10]])], floorSet, {}, 'merge_group'); + if (r.exitCode !== 0 || status(r, 'tiny') !== 'below-floor' || r.out.length !== 0) { + throw new Error(`floor: a below-floor package annotated off schedule: ${JSON.stringify(r.out)}`); + } + }); + check(() => { + const r = run([capture('1/6', [['small', 25]])], floorSet); + if (r.exitCode !== 1 || r.verdict !== 'OVER' || !r.out.some((l) => l.startsWith('::error title=Suite-duration ceiling::small: 25.00s, over its 22.71s ceiling'))) { + throw new Error(`floor: a 22.71s ceiling run at 25s on schedule was not red: ${r.verdict} ${JSON.stringify(r.out)}`); + } + }); + check(() => { + const r = run([capture('1/6', [['small', 25]])], floorSet, {}, 'merge_group'); + const errors = r.out.filter((l) => l.startsWith('::error')); + if (r.exitCode !== 0 || errors.length || r.out.length !== 1 || !r.out[0].startsWith('::warning title=Suite-duration ceiling::small: 25.00s')) { + throw new Error(`floor: a 22.71s ceiling run at 25s off schedule was not a warning alone: exit ${r.exitCode} ${JSON.stringify(r.out)}`); + } + }); + check(() => { + // AT the floor is graded (strict `<`), and so is a below-floor ceiling a ruled raise lifts to it. + const at = run([capture('1/6', [['edge', CEILING_FLOOR_SECONDS + 1]])], floorSet); + const raised = run([capture('1/6', [['tiny', 26]])], floorSet, { tiny: { seconds: 25, ruling: raiseUrl } }); + if (status(at, 'edge') !== 'over' || at.exitCode !== 1 || status(raised, 'tiny') !== 'over' || raised.exitCode !== 1) { + throw new Error(`floor: AT the floor read ${status(at, 'edge')}, a raised tiny read ${status(raised, 'tiny')}`); + } + }); + check(() => { + // Alone, a below-floor package grades nothing: NOT MEASURED, naming the floor. + const r = run([capture('1/6', [['tiny', 400]])], floorSet); + if (r.exitCode !== 0 || r.verdict !== 'NOT MEASURED' || !r.err[0].includes('at or above the 20s floor')) { + throw new Error(`floor: a below-floor package alone read ${r.verdict}:\n${r.err[0]}`); + } + }); + + battery('off `schedule` the gate reports, never reds (C2)'); + const offSchedule = ['merge_group', 'pull_request', 'push', 'workflow_dispatch']; + const twoOver = [capture('1/6', [['a', 151], ['b', 90]], [file('a', 'slow.test.ts', 90_000)])]; + check(() => { + for (const event of offSchedule) { + const r = run(twoOver, dataset(), {}, event); + const warnings = r.out.filter((l) => l.startsWith('::warning title=Suite-duration ceiling::')); + if (r.exitCode !== 0 || r.verdict !== 'OVER' || warnings.length !== 2 || r.out.length !== 2) { + throw new Error(`C2: two packages over on ${event} read ${r.verdict}, exit ${r.exitCode}, ${JSON.stringify(r.out)}`); + } + } + }); + check(() => { + const r = run(twoOver, dataset(), {}, 'merge_group'); + if (r.out.some((l) => l.startsWith('::error')) || r.out.some((l) => l.includes('\n')) || !r.out.every((l) => l.includes('merge_group'))) { + throw new Error(`C2: an off-schedule annotation is an error, spans lines, or does not name the event: ${JSON.stringify(r.out)}`); + } + }); + check(() => { + const r = run(twoOver); + if (r.exitCode !== 1 || r.out.length !== 2 || !r.out.every((l) => l.startsWith('::error title=Suite-duration ceiling::'))) { + throw new Error(`C2: two packages over on schedule were not red: exit ${r.exitCode} ${JSON.stringify(r.out)}`); + } + }); + check(() => { + // The SAME table on every event: only the mode line differs. + const body = (r) => r.err[0].split('\n').slice(0, -1).join('\n'); + const graded = run(twoOver); + const reported = run(twoOver, dataset(), {}, 'pull_request'); + if (body(graded) !== body(reported) || !body(graded).includes('Every executed package this run')) { + throw new Error(`C2: the report-only table is not the graded one:\n${graded.err[0]}\n---\n${reported.err[0]}`); + } + if (!graded.err[0].endsWith('the profile the baseline was measured on.') || !reported.err[0].endsWith('the step exits 0.')) { + throw new Error(`C2: the mode line is missing:\n${graded.err[0]}\n---\n${reported.err[0]}`); + } + }); + check(() => { + // Within every ceiling, off schedule: OK, no annotation, the mode said. + const r = run([capture('1/6', [['a', 10]])], dataset(), {}, 'push'); + if (r.exitCode !== 0 || r.verdict !== 'OK' || r.out.length !== 0 || !r.err[0].includes('report only: a `push` run')) { + throw new Error(`C2: an OK push run read ${r.verdict}:\n${r.err[0]}`); + } + }); + check(() => { + // A refusal is not a timing reading: red on every event. + const r = run([capture('1/6', [['a', 180]])], dataset(), { a: { seconds: 200, ruling: 'no' } }, 'merge_group'); + if (r.exitCode !== 1 || r.verdict !== 'REFUSED') throw new Error(`C2: a malformed raise record off schedule read ${r.verdict}`); + }); + check(() => { + // No table yet is unchanged off schedule: NOT MEASURED with its warning. + const { ceilings: _x, uncapped: _y, ...noTable } = dataset(); + const r = run([capture('1/6', [['a', 9999]])], noTable, {}, 'merge_group'); + if (r.exitCode !== 0 || r.verdict !== 'NOT MEASURED' || !r.out.some((l) => l.startsWith('::warning title=Suite-duration ceilings::NOT MEASURED'))) { + throw new Error(`C2: no table off schedule read ${r.verdict}`); + } + }); + check(() => { + // A missing event is refused, in the renderer and at the command line, never defaulted. + const report = gradeCeilings({ merged: foldCaptures([capture('1/6', [['a', 151]])]), dataset: dataset(), raises: {} }); + if (!threw(() => renderCeilingVerdict(report, 'Test Core'))) throw new Error('C2: a render with no event was graded'); + if (!threw(() => parseCeilingArgs(['--captures', 'd']))) throw new Error('C2: --captures with no --event was accepted'); + if (!threw(() => parseCeilingArgs(['--captures', 'd', '--event', '']))) throw new Error('C2: an empty --event was accepted'); + const parsed = parseCeilingArgs(['--captures', 'd', '--label', 'L', '--event', 'merge_group']); + if (parsed.event !== 'merge_group' || parsed.capturesDir !== 'd' || parsed.label !== 'L') { + throw new Error(`C2: the arguments parsed as ${JSON.stringify(parsed)}`); + } + }); + // The floor: every declared battery ran, and ran its cases. const floorFailures = []; const declared = Object.keys(SELF_TEST_BATTERIES); @@ -883,6 +1106,25 @@ function selfTest() { // ── Entry point ─────────────────────────────────────────────────────────── +/** + * The grading run's arguments. `--event` is required (C2): it decides whether + * a package over its ceiling exits 1 (`schedule`) or is a warning (any other + * event), and a missing one is refused by timingGateMode, never defaulted. + */ +export function parseCeilingArgs(argv) { + let capturesDir = null; + let label = 'Test Core'; + let event = null; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--captures') capturesDir = argv[++i]; + else if (argv[i] === '--label') label = argv[++i]; + else if (argv[i] === '--event') event = argv[++i]; + else throw new Error(`unrecognized argument: ${argv[i]}`); + } + if (capturesDir) timingGateMode(event); + return { capturesDir, label, event }; +} + function main() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { @@ -898,15 +1140,9 @@ function main() { return; } - let capturesDir = null; - let label = 'Test Core'; - for (let i = 0; i < argv.length; i++) { - if (argv[i] === '--captures') capturesDir = argv[++i]; - else if (argv[i] === '--label') label = argv[++i]; - else throw new Error(`unrecognized argument: ${argv[i]}`); - } + const { capturesDir, label, event } = parseCeilingArgs(argv); if (!capturesDir) { - console.error('usage: check-test-suite-ceilings.mjs --captures [--label ] | --self-test'); + console.error('usage: check-test-suite-ceilings.mjs --captures --event [--label ] | --self-test'); process.exit(1); } @@ -914,7 +1150,7 @@ function main() { const merged = foldCaptures(captures); const dataset = JSON.parse(readFileSync(DATASET_PATH, 'utf8')); const report = gradeCeilings({ merged, dataset }); - const rendered = renderCeilingVerdict(report, label); + const rendered = renderCeilingVerdict(report, label, event); for (const line of rendered.out) console.log(line); for (const line of rendered.err) console.error(line); // Not a verdict of their own: a shard capture that could not be read leaves diff --git a/scripts/partition-test-shards.mjs b/scripts/partition-test-shards.mjs index 52581cb6eaf..2c1bcf62605 100644 --- a/scripts/partition-test-shards.mjs +++ b/scripts/partition-test-shards.mjs @@ -75,7 +75,7 @@ // node scripts/partition-test-shards.mjs --shard N/M \ // [--exclude ]... // node scripts/partition-test-shards.mjs --check-drift ... \ -// [--label ] +// --event [--label ] // node scripts/partition-test-shards.mjs --self-test // // `--check-drift` is the half that keeps the dataset honest AFTER it is @@ -83,7 +83,8 @@ // run it just finished has written the measured truth to `.turbo/runs/`; this // mode reads that back, compares it to what this script PREDICTED for the same // packages, raises a `::warning::` past WARN_MEASURED_OVER_PREDICTED, and reds -// past MAX_MEASURED_OVER_PREDICTED. Without it the dataset +// past MAX_MEASURED_OVER_PREDICTED -- on a `schedule` run; on every other event +// that red is a `::warning::` and exits 0 (TIMING_GRADED_EVENT, C2). Without it the dataset // rots silently in one direction and the only instrument that notices is a // shard killed by the job timeout -- which is a shard that produced NO reading // while the rollup read green. @@ -200,6 +201,54 @@ export const MAX_MEASURED_OVER_PREDICTED = 1.5; // the warning cannot be made unreachable by moving either end. export const WARN_MEASURED_OVER_PREDICTED = 1.3; +// ── WHERE THE TWO TIMING GATES EXIT-GRADE: `schedule` ALONE (C2) ────────── +// +// Ruling (letter C2, for both timing steps): +// https://github.com/objectstack-ai/objectstack/issues/22949#issuecomment-6119186511 +// "a timing gate exit-grades only on the run profile its baseline was measured +// on; everywhere else it reports." +// +// Both timing gates read one baseline: the weights `--check-drift` predicts +// from, and the ceilings scripts/check-test-suite-ceilings.mjs grades against. +// The refresh lane measures it on ci.yml's hourly `schedule` runs and no others +// (DEFAULT_RUN_EVENT in scripts/ci/select-shard-timings-run.mjs). A +// `merge_group` or `pull_request` build executes far more of the workspace on +// the same runners. On the morning the refreshed dataset went live this step +// redded 3 of 19 queue builds and the heads of three PRs (#22953, #22954, +// #22959, filed as #22966) at 1.56-1.62x, on packages none of them touched, +// while every hourly run read green. So both gates take the run's event +// (`--event "$GITHUB_EVENT_NAME"` in ci.yml) and: +// +// schedule exit-graded, exactly as before: a red exits 1 +// every other REPORT ONLY (merge_group, pull_request, push, +// event workflow_dispatch): the same table prints, each red becomes a +// `::warning::` annotation, and the step exits 0 +// +// A real slowdown merged to `main` reds the next hourly run. A missing or empty +// `--event` is refused, never defaulted: a step that lost its event would +// otherwise grade everywhere or nowhere, and say nothing either way. +export const TIMING_GRADED_EVENT = 'schedule'; + +/** 'grade' on TIMING_GRADED_EVENT, 'report' on any other event; a missing event is refused. */ +export function timingGateMode(event) { + if (typeof event !== 'string' || event.trim() === '') { + throw new Error( + `the run's event is ${JSON.stringify(event ?? null)}. The timing gates exit-grade on ` + + `\`${TIMING_GRADED_EVENT}\` alone and report on every other event, so they need it: pass ` + + '`--event "$GITHUB_EVENT_NAME"`. A missing event is refused rather than guessed.' + ); + } + return event === TIMING_GRADED_EVENT ? 'grade' : 'report'; +} + +/** The one line both timing gates print to say which of the two modes this run is in. */ +export function timingModeNote(event) { + return timingGateMode(event) === 'grade' + ? `exit-graded: a \`${event}\` run is the profile the baseline was measured on` + : `report only: a \`${event}\` run is not the profile the baseline was measured on ` + + `(\`${TIMING_GRADED_EVENT}\`), so a red reading is a warning here and the step exits 0`; +} + // ── SHARDING ONE PACKAGE BELOW PACKAGE GRANULARITY (#16173) ──────────────── // // The floor argument at the top of this file is not a caveat, it is a wall: a @@ -1082,7 +1131,12 @@ export function escapeWorkflowCommandMessage(text) { return String(text).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'); } -export function renderDriftVerdict(report, label) { +// +// `event` is the run's (ci.yml passes `$GITHUB_EVENT_NAME`): off +// TIMING_GRADED_EVENT a DRIFT prints the same text, becomes a `::warning::` +// annotation, and exits 0 (C2, see timingGateMode). +export function renderDriftVerdict(report, label, event) { + const mode = timingGateMode(event); const skipped = report.unpredicted.length === 0 ? '' @@ -1115,25 +1169,37 @@ export function renderDriftVerdict(report, label) { `(${ratioOf(r)}${r.overshoot >= 0 ? '+' : ''}${r.overshoot.toFixed(1)}s)` ) .join('\n'); + const note = `\n ${timingModeNote(event)}.`; if (report.drifted) { - return { - verdict: 'DRIFT', - exitCode: 1, - out: [], - err: [ - `shard-timing-drift: DRIFT -- ${label}, ${head}.${skipped}\n` + - ' Heaviest overshoots:\n' + - `${worst}\n` + - ' scripts/test-shard-timings.json no longer describes this workspace, so the shard split\n' + - ' is balancing a quantity that is not the runtime. Refresh it -- see\n' + - ' scripts/measure-test-shard-timings.mjs for the two refresh paths -- and ⛔ do NOT\n' + - ' hand-edit the dataset or raise this bound to absorb the gap. Expect the refresh to red\n' + - " this script's own balance pins if a single suite has outgrown the acceptance bound:\n" + - ' that is those pins working, and the remedy they name is splitting that suite below\n' + - ' package granularity, never a different shard count.', - ], - }; + const text = + `shard-timing-drift: DRIFT -- ${label}, ${head}.${skipped}\n` + + ' Heaviest overshoots:\n' + + `${worst}\n` + + ' scripts/test-shard-timings.json no longer describes this workspace, so the shard split\n' + + ' is balancing a quantity that is not the runtime. Refresh it -- see\n' + + ' scripts/measure-test-shard-timings.mjs for the two refresh paths -- and ⛔ do NOT\n' + + ' hand-edit the dataset or raise this bound to absorb the gap. Expect the refresh to red\n' + + " this script's own balance pins if a single suite has outgrown the acceptance bound:\n" + + ' that is those pins working, and the remedy they name is splitting that suite below\n' + + ` package granularity, never a different shard count.${note}`; + if (mode === 'report') { + const top = report.rows[0]; + return { + verdict: 'DRIFT', + exitCode: 0, + out: [ + `::warning title=Test Core shard timing drift::${escapeWorkflowCommandMessage( + `${label}: ${head}, past the red bound. Heaviest overshoot: ${top.name}, predicted ` + + `${top.predicted.toFixed(1)}s, measured ${top.measured.toFixed(1)}s. Report only on a \`${event}\` run: ` + + `this step exit-grades on \`${TIMING_GRADED_EVENT}\` runs alone, the profile ` + + 'scripts/test-shard-timings.json was measured on, so a drift that is real reds the next hourly run on main.' + )}`, + ], + err: [text], + }; + } + return { verdict: 'DRIFT', exitCode: 1, out: [], err: [text] }; } if (report.warned) { @@ -1149,7 +1215,7 @@ export function renderDriftVerdict(report, label) { 'Not a failure: this step stays green until the red bound.' )}`, ], - err: [`shard-timing-drift: WARN -- ${label}, ${head}.${skipped}\n Heaviest overshoots:\n${worst}`], + err: [`shard-timing-drift: WARN -- ${label}, ${head}.${skipped}\n Heaviest overshoots:\n${worst}${note}`], }; } @@ -1157,7 +1223,7 @@ export function renderDriftVerdict(report, label) { verdict: 'OK', exitCode: 0, out: [], - err: [`shard-timing-drift: OK -- ${label}, ${head}.${skipped}`], + err: [`shard-timing-drift: OK -- ${label}, ${head}.${skipped}${note}`], }; } @@ -1249,11 +1315,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'file-level slice items (#16173)': 20, 'file-level slices reach vitest through OS_TEST_SHARD (#19278)': 11, 'density cap and the per-run slice decision (#22075)': 14, + 'the drift step exit-grades on `schedule` alone, and reports everywhere else (C2)': 8, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 12; +const SELF_TEST_BATTERY_FLOOR = 13; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -1898,7 +1965,7 @@ function selfTest() { // with a workflow command would mint a real annotation on every lint run). const label = 'Test Core (9/9)'; check(() => { - const v = renderDriftVerdict(tierOf(131), label); + const v = renderDriftVerdict(tierOf(131), label, TIMING_GRADED_EVENT); const commands = v.out.filter((line) => line.startsWith('::')); if (v.verdict !== 'WARN' || v.exitCode !== 0 || commands.length !== 1) { throw new Error( @@ -1910,19 +1977,19 @@ function selfTest() { } }); check(() => { - const v = renderDriftVerdict(tierOf(151), label); + const v = renderDriftVerdict(tierOf(151), label, TIMING_GRADED_EVENT); if (v.verdict !== 'DRIFT' || v.exitCode !== 1 || v.out.some((line) => line.startsWith('::'))) { throw new Error(`drift warning: a red rendered ${v.verdict} exit ${v.exitCode}, ${v.out.length} stdout line(s)`); } }); check(() => { for (const [seconds, expected] of [[118, 'OK'], [WARN_MEASURED_OVER_PREDICTED * 100, 'OK']]) { - const v = renderDriftVerdict(tierOf(seconds), label); + const v = renderDriftVerdict(tierOf(seconds), label, TIMING_GRADED_EVENT); if (v.verdict !== expected || v.exitCode !== 0 || v.out.length !== 0) { throw new Error(`drift warning: ${seconds / 100}x rendered ${v.verdict} with ${v.out.length} stdout line(s)`); } } - const nm = renderDriftVerdict(driftReport(replayed.samples, driftTimings), label); + const nm = renderDriftVerdict(driftReport(replayed.samples, driftTimings), label, TIMING_GRADED_EVENT); if (nm.verdict !== 'NOT MEASURED' || nm.exitCode !== 0 || nm.out.length !== 0) { throw new Error(`drift warning: a replay-only shard rendered ${nm.verdict}`); } @@ -1936,6 +2003,85 @@ function selfTest() { } }); + // -- EXIT-GRADED ON `schedule` ALONE (C2) -------------------------------- + // + // The baseline is measured on hourly `schedule` runs, so only those exit 1 + // on a DRIFT; every other event prints the same text, annotates a warning + // and exits 0. Read as rendered text, like the tier above: printing a + // `::warning::` from here would mint a real annotation on every lint run. + battery('the drift step exit-grades on `schedule` alone, and reports everywhere else (C2)'); + const offSchedule = ['merge_group', 'pull_request', 'push', 'workflow_dispatch']; + + check(() => { + if (TIMING_GRADED_EVENT !== 'schedule' || timingGateMode('schedule') !== 'grade') { + throw new Error(`C2: the graded event is ${TIMING_GRADED_EVENT}, and schedule reads ${timingGateMode('schedule')}`); + } + }); + check(() => { + const graded = offSchedule.filter((e) => timingGateMode(e) !== 'report'); + if (graded.length) throw new Error(`C2: ${graded.join(', ')} exit-grade, but only schedule may`); + }); + // A step that lost its event is refused, never defaulted to either mode. + check(() => { + const accepted = [undefined, null, '', ' '].filter((e) => !threw(() => timingGateMode(e))); + if (accepted.length) throw new Error(`C2: a missing event was read as a mode: ${JSON.stringify(accepted)}`); + if (!threw(() => renderDriftVerdict(tierOf(151), label))) throw new Error('C2: a render with no event was graded'); + }); + check(() => { + const v = renderDriftVerdict(tierOf(151), label, 'schedule'); + if (v.verdict !== 'DRIFT' || v.exitCode !== 1 || v.out.length !== 0 || !v.err[0].includes('exit-graded')) { + throw new Error(`C2: a schedule DRIFT rendered ${v.verdict} exit ${v.exitCode}, ${v.out.length} stdout line(s)`); + } + }); + check(() => { + for (const event of offSchedule) { + const v = renderDriftVerdict(tierOf(151), label, event); + const commands = v.out.filter((line) => line.startsWith('::')); + if (v.verdict !== 'DRIFT' || v.exitCode !== 0 || commands.length !== 1) { + throw new Error(`C2: a ${event} DRIFT rendered ${v.verdict} exit ${v.exitCode} with ${commands.length} command(s)`); + } + if (!commands[0].startsWith('::warning ') || !commands[0].includes(label) || !commands[0].includes(event) || commands[0].includes('\n')) { + throw new Error(`C2: a ${event} DRIFT is not one ::warning line naming the shard and the event: ${commands[0]}`); + } + } + }); + // The SAME table on every event: only the mode line differs. + check(() => { + const body = (v) => v.err[0].split('\n').slice(0, -1).join('\n'); + const graded = renderDriftVerdict(tierOf(151), label, 'schedule'); + const reported = renderDriftVerdict(tierOf(151), label, 'merge_group'); + if (body(graded) !== body(reported) || !body(graded).includes('Heaviest overshoots:')) { + throw new Error(`C2: the report-only text is not the graded text:\n${graded.err[0]}\n---\n${reported.err[0]}`); + } + if (!reported.err[0].endsWith('so a red reading is a warning here and the step exits 0.')) { + throw new Error(`C2: the report-only text does not say so:\n${reported.err[0]}`); + } + }); + check(() => { + // The tiers under the red are unchanged off schedule: WARN annotates, OK and NOT MEASURED say nothing on stdout. + const warn = renderDriftVerdict(tierOf(131), label, 'pull_request'); + const ok = renderDriftVerdict(tierOf(118), label, 'push'); + const nm = renderDriftVerdict(driftReport(replayed.samples, driftTimings), label, 'merge_group'); + if (warn.verdict !== 'WARN' || warn.exitCode !== 0 || warn.out.length !== 1 || !warn.out[0].startsWith('::warning ')) { + throw new Error(`C2: a pull_request WARN rendered ${warn.verdict} exit ${warn.exitCode}`); + } + if (ok.verdict !== 'OK' || ok.exitCode !== 0 || ok.out.length !== 0 || !ok.err[0].includes('report only')) { + throw new Error(`C2: a push OK rendered ${ok.verdict} exit ${ok.exitCode}:\n${ok.err[0]}`); + } + if (nm.verdict !== 'NOT MEASURED' || nm.exitCode !== 0 || nm.out.length !== 0) { + throw new Error(`C2: a merge_group replay-only shard rendered ${nm.verdict}`); + } + }); + // The CLI: `--event` is read, and its absence is refused before any summary is. + check(() => { + const parsed = parseDriftArgs(['--check-drift', 'a.json', '--label', 'L', '--event', 'merge_group']); + if (parsed.event !== 'merge_group' || parsed.inputs.join() !== 'a.json' || parsed.label !== 'L') { + throw new Error(`C2: --check-drift parsed as ${JSON.stringify(parsed)}`); + } + if (!threw(() => parseDriftArgs(['--check-drift', 'a.json']))) throw new Error('C2: --check-drift with no --event was accepted'); + if (!threw(() => parseDriftArgs(['--check-drift', 'a.json', '--event', '']))) throw new Error('C2: an empty --event was accepted'); + }); + // -- FILE-LEVEL SLICE ITEMS (#16173) ------------------------------------ // // The grammar, its refusals, and the two joins that read it. The balancing @@ -2474,19 +2620,31 @@ function selfTest() { // test tasks were all cache replays has said nothing about the dataset, and // reporting that as OK is the #4690 shape -- a check that read nothing // reporting as a check that found nothing wrong. -function checkDrift(argv) { +// +// `--event` is required (C2): the run's event decides whether a DRIFT exits 1 +// (`schedule`) or is reported as a warning (every other event), and a missing +// one is refused by timingGateMode rather than defaulted either way. +export function parseDriftArgs(argv) { const inputs = []; let label = 'this shard'; + let event = null; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--check-drift') continue; else if (arg === '--label') label = argv[++i]; + else if (arg === '--event') event = argv[++i]; else if (arg.startsWith('--')) throw new Error(`unrecognized argument: ${arg}`); else inputs.push(arg); } + timingGateMode(event); + return { inputs, label, event }; +} + +function checkDrift(argv) { + const { inputs, label, event } = parseDriftArgs(argv); if (inputs.length === 0) { console.error( - 'usage: partition-test-shards.mjs --check-drift ... [--label ]' + 'usage: partition-test-shards.mjs --check-drift ... --event [--label ]' ); process.exit(1); } @@ -2515,7 +2673,7 @@ function checkDrift(argv) { } const timings = loadTimings(); const report = driftReport(merged, timings, MAX_MEASURED_OVER_PREDICTED, observedSlices); - const rendered = renderDriftVerdict(report, label); + const rendered = renderDriftVerdict(report, label, event); for (const line of rendered.out) console.log(line); for (const line of rendered.err) console.error(line); if (rendered.exitCode !== 0) process.exit(rendered.exitCode); diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 55a009ef5ad..746c7553b1d 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -11,9 +11,9 @@ "a pin bump without regeneration goes RED there — that is the anti-rot half of the freshness gate.", "Regenerate + re-record: pnpm objectui:build && node scripts/gen-sdui-manifest-node.mjs" ], - "objectuiSha": "e5311165a8d6aa77b5cb7d9fd05da8451a2c21aa", + "objectuiSha": "088e0f705520e82bee1d65a5fa3c1f7a26672dde", "source": "built-tree", - "modulesRoot": ".cache/objectui-e5311165a8d6/apps/console", + "modulesRoot": ".cache/objectui-088e0f705520/apps/console", "objectuiWorkspaceVersion": "17.7.0", "generator": "scripts/gen-sdui-manifest-node.mjs", "generatedAt": "2026-10-12",