You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 29c76e5
Browse filesBrowse the repository at this point in the historyBrowse files
test(types): the JoinedReportBlock pin compiles on the installed spec and on spec main, and trips at the bump (objectui#10916) (#10934)
Fixes#10916
Clause-②: no — a test-side tripwire and the ledger text beside it move
to what spec `main` now says; no published type, no export and no
accepted document changes (the local `JoinedReportBlock` interface and
the `index.ts` exports are untouched).
⚠️ Generic parameters are written as UPPERCASE WORDS in this body (for
example "IsUnknown OF SpecJoinedReportBlock"), never in angle brackets:
GitHub's body sanitizer deletes tag-shaped fragments on save, and
backticks do not protect them (AGENTS.md, "GitHub mutates body BYTES").
Diagnostics are quoted by code and text, without the file's line and
column, per AGENTS.md #11.
## What was red, and why the pin was right to go red
objectstack#20369 (`681868ca7`) removed the bare `z.ZodTypeAny`
annotation from `JoinedReportBlockSchema` on objectstack `main`. The
inverted pin `_specJoinedReportBlockIsStillUntyped` (`true satisfies`
IsUnknown OF SpecJoinedReportBlock) in
`packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`
then failed with TS1360 ("Type 'true' does not satisfy the expected type
'false'.") on the Spec Main Shape Gate, which compiles every head
against spec built from objectstack `main`. Its docblock said that
failure is the instruction: re-run the triage and burn it down.
The pull-request type-check compiles the SAME file against the INSTALLED
spec, the published 17.4.0 (the newest release on npm, published
2026-09-09, so before #20369). There the type is still `unknown`.
Whatever stands in the file has to compile against both until a spec
bump installs a release carrying #20369.
## The re-run triage: spec `main` against the local interface, member by
member
Read with the TypeScript checker off the source-built spec's `dist`
(objectstack `df3ba164a588`); `JoinedReportBlock` is neither `any` nor
`unknown` there and has no index signature.
| member | local `JoinedReportBlock` (`spec-report.ts`) | spec `main`
`JoinedReportBlock` (input) | verdict |
| --- | --- | --- | --- |
| `name` | `string`, required | `string`, required (snake_case at
runtime) | agree |
| `type` | optional `tabular` / `summary` / `matrix` | the same enum,
optional (defaults to `tabular`) | agree |
| `label`, `description` | string, or an object with `default` and
`translations` | string, or an inline locale map whose every value is a
string | diverge: the local object arm is not a spec label |
| `columns` | REQUIRED array of column OBJECTS (`field`, `aggregate`,
...) | optional array of dimension NAMES (matrix across-axis) | one
name, two concepts |
| `objectName` | declared | not declared; the closed schema refuses it
(its alias table points at `dataset`) | local-only |
| `groupingsDown`, `groupingsAcross` | declared | not declared; refused
| local-only |
| `filter` | declared | not declared; refused (alias table points at
`runtimeFilter`) | local-only |
| `chart` | declared | retired by objectstack#20161, refused with the
retirement prescription | local-only |
| index signature | `[k: string]: unknown` | none (closed schema) |
diverge |
| `dataset`, `rows`, `values`, `runtimeFilter`, `order` | not declared |
declared (the ADR-0021 dataset-bound selection) | spec-only |
**Verdict.** The spec type is now precise, and it describes a DIFFERENT
shape: guard case 2c on spec `main`, no longer 2b. The local interface
is the legacy inline-query block, and the spec's closed schema refuses
every one of its five local-only keys. So it is derivable, but not by a
re-export beside the local interface: the burn-down REPLACES the
published `JoinedReportBlock` (and the `blocks` of `JoinedSpecReport`)
with the spec's type. That changes a published type, so it is its own
slice, and it cannot land before the bump, because against the installed
spec the derived type would be `unknown`. Nothing in this tree reads the
local interface's legacy members (from a grep of the five local-only
keys and a reading of the renderer, not a runtime probe): the joined
branch of `DatasetReportRenderer` reads dataset-bound blocks, and the
local type is only re-exported and used by `JoinedSpecReport`.
## The chosen shape, and the precedent it follows
objectui#10080 (PR #10089) is the precedent for "the gate compiles
against spec `main`, the PR check against the installed pin": it kept
both jobs of the red declaration and changed its form so the file
compiles against both specs, then proved both legs with a control.
objectui#3177 (case 2c for `NavigationItem`) is the precedent for what
replaces an `IsAny` / IsUnknown pin once the spec is typed: per-key
probes that each stop compiling the day that one divergence moves. This
PR does both. The guard now has two halves, because two different runs
read two different specs:
- **Compile time, both runs.** A state pin,
`_specJoinedReportBlockTyping`, admits `erased` and `typed` and refuses
`any`; the `IsAny` pin stays as it was. On a typed spec, seven
divergence and agreement probes (`_localBlockIsNotTheSpecBlock`,
`_specOnlyBlockKeys`, `_localOnlyBlockKeys`,
`_blockColumnsAreTwoConcepts`, `_blockLabelObjectArmIsNotASpecLabel`,
`_blockNameAgrees`, `_blockTypeAgrees`) record the triage above and stop
compiling the day one of them moves, on the Spec Main Shape Gate first.
On the erased spec they are not asked (`OnTypedSpec`), since an erased
type declares no member to compare.
- **Test time, the installed spec only.** The Spec Main Shape Gate
type-checks and runs no test, so a vitest tripwire can act on the
INSTALLED spec alone. It reads the installed spec's built declarations
with the TypeScript checker, and it fails on the first bump past the
published 17.4.0. If that release types the block, the failure is the
burn-down's start signal: derive the published interface in its own
slice, and delete the state pin's `erased` arm so that an erasure stops
compiling again. If that release still erases the block, the spec erased
it again, which is an upstream regression; the `erased` arm loses its
licence either way. A lit control (`ReportSort` must read `typed`) keeps
the reader from passing vacuously.
⚠️ **Declared limit.** An erasure on objectstack `main` BEFORE that bump
cannot be told apart, at the type level, from the installed 17.4.0: both
are the same bare `z.ZodTypeAny`. The state pin therefore admits it on
the gate. The producer's own pin covers that window
(`packages/spec/src/ui/joined-report-block-type.test.ts` in objectstack,
landed with #20369), and the tripwire's version row catches it at the
bump at the latest.
Nothing was skipped, disabled or ts-ignored, and no workflow changed.
The ledger row (the `JoinedReportBlock` ALLOW entry in
`scripts/check-spec-symbol-derivation.mjs`, which is objectui#3162's
ledger in machine-readable form) and that script's case-2b header now
say what is true of both specs, and when the burn-down fires.
## Both compile legs, from committed state
Legs taken with `pnpm run type-check` in `packages/types` (all three
`tsc` programs, `tsconfig.test.json` included). The spec-`main` legs
follow the gate's own procedure: `@objectstack/spec` built from
objectstack `df3ba164a588` (a detached worktree, `pnpm install
--frozen-lockfile --filter @objectstack/spec...`, `pnpm --filter
@objectstack/spec build`), `npm pack --ignore-scripts`, then `node
scripts/spec-main-shape-gate.mjs inject` into this worktree's install.
The inject printed its consumer list and re-pointed `zod` to 4.6.1, as
the gate does. The installed legs ran after a clean reinstall, with the
gate's injection marker confirmed absent.
| leg | spec under the compiler | tree | result |
| --- | --- | --- | --- |
| installed, before | published 17.4.0, block is `unknown` | `733fd5ac6`
(base) | exit 0, 0 diagnostics |
| spec `main`, before | source-built `df3ba164a588`, block typed |
`733fd5ac6` | **exit 2, 1 diagnostic**: TS1360 "Type 'true' does not
satisfy the expected type 'false'." at
`_specJoinedReportBlockIsStillUntyped` (the same diagnostic the gate
reports) |
| spec `main`, after | same injected build | `8ce61bad3` | **exit 0, 0
diagnostics** |
| installed, after | published 17.4.0 restored | `8ce61bad3`, and again
at head `22226a8e9` | **exit 0, 0 diagnostics** |
The two commits after `8ce61bad3` touch only `scripts/` and
`.changeset/`, which no `packages/types` program reads.
⚠️ Narrowed on purpose: the gate runs `turbo run type-check` over every
package. Locally only `@object-ui/types` was compiled against spec
`main`, because it is the only package the gate reported and the only
one this diff touches. The full farm is the gate's own run on this head.
## The tripwire, fired for real
Under the injected spec-`main` install, `pnpm exec vitest run
packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`
gives **1 failed, 46 passed (47)**. The one failure is the typing row,
with its message: "the installed @objectstack/spec 17.4.0 now types
JoinedReportBlock — this is the bump the burn-down is owed at. [...]
expected 'typed' to be 'erased'". The lit control and the version row
pass (a source-built `main` still reports version 17.4.0). Under the
installed spec the same file gives **47 passed (47)**.
## Ablations, each proved on disk
All through `ablation-replace.mjs` (objectstack `scripts/`) in wrap
mode. For every leg the tool reported "mutation landed", and after it
"restored: blob == HEAD (`a1b2c707461e`) and `git diff HEAD` is empty".
Spec `main` injected, `tsc -p tsconfig.test.json`:
| leg | mutation | result |
| --- | --- | --- |
| A1 | drop `order` from `_specOnlyBlockKeys`' expected set | TS1360 at
that pin |
| A2 | drop `chart` from `_localOnlyBlockKeys`' expected set | TS1360 at
that pin |
| A3 | spec `columns` expected as `number[]` | TS1360 at
`_blockColumnsAreTwoConcepts` |
| A4 | state pin without its `typed` arm | TS1360 "Type '"typed"' does
not satisfy the expected type '"erased"'." |
| A5 | state pin without its `erased` arm | 0 diagnostics (the
burn-down's future edit is valid on a typed spec) |
| A6 | `_blockTypeAgrees` compares `name` against `type` | TS1360 at
that pin |
| A7 | local label replaced by a plain string in
`_blockLabelObjectArmIsNotASpecLabel` | TS1360 at that pin |
Installed 17.4.0:
| leg | mutation | result |
| --- | --- | --- |
| A5 | state pin without its `erased` arm | TS1360 "Type '"erased"' does
not satisfy the expected type '"typed"'." (the arm is load-bearing
today) |
| A1 | as above | 0 diagnostics: the typed-spec probes are not asked of
an erased spec, as declared |
| V | tripwire version row expects `17.3.0` | vitest: that row alone
red, 1 failed, 46 passed |
⚠️ The first attempt at A1, A2 and A5 on spec `main` was a no-op. The
replacement text was a substring of the anchor, so `ablation-replace`
refused it ("the replacement count moved 1 to 1", paraphrased) and
restored. Those three legs were re-run in `--delete` mode, and the table
shows the re-run.
## Tests and gates, on head `22226a8e9`
- `pnpm exec vitest run --maxWorkers=2 packages/types/`: **264 files,
5856 tests, all passed** (installed spec).
- Every test that references `scripts/check-spec-symbol-derivation.mjs`
(31 files, `scripts/__tests__` and package suites): **31 files, 630
tests, all passed**.
- `pnpm check:spec-symbols`: exit 0 ("0 untriaged collisions in 0
packages").
- `check-changeset-presence`: RED first ("1 source file(s) of 1 released
package(s) changed, and this change adds no changeset"). That gate
counts every file under a released package's `src/`, tests included. So
this PR carries `.changeset/10916-joined-report-block-pin-two-specs.md`
with an EMPTY frontmatter, and the gate then reads exit 0 ("declared as
releasing nothing").
- Exit 0 as well: `check-changeset-no-major`, `check-changeset-fixed`,
`check-changeset-overwrite`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check:control-bytes`,
`check:new-line-citations` (0 new citations), `check:test-path-roots`,
`check:installed-pin-claims`, `check:shell-escape-residue`,
`check:vi-mock-specifiers`, `check:vi-mock-inherit`.
- `check-governed-queue-guard --test` on the three paths: NOT GOVERNED.
- eslint (`--no-inline-config`) on the two changed source files: 2 files
linted, 0 errors, 0 warnings. Narrowed on purpose; the config enables no
type-aware linting, so this diff cannot move the verdict on an untouched
file.
- NOT MEASURED, prerequisite: `check:spec-floors` needs every package
built (it reports `no-artifact` for all 19 spec consumers in an unbuilt
worktree); this diff moves no manifest or `dist`.
## The second slice this owes (not in this PR)
At the first spec bump past 17.4.0, derive `JoinedReportBlock` from
`@objectstack/spec/ui` and converge `JoinedSpecReport`'s `blocks`.
Reading: **Clause-② yes (narrowing)**. The published local type accepts
any key through its index signature and types the legacy members; the
spec's is closed and dataset-bound. It should land WITH the bump: on a
typed spec, `JoinedSpecReport` intersects the spec's `blocks` (from
`ReportParsed`) with the local ones, which makes `columns` both names
and objects (read from the two declarations, not compiled).
## Acceptance notes
- objectui#3162 is CLOSED and its body still records the 17.2.0 verdict
for this symbol. It is not edited here; the in-tree ledger row is.
- `spec-derived-unions.test.ts` keeps its batch-8 note that this symbol
was "STILL erased"; that note is a dated reading at spec 17.2.0 and is
left as history.
This change was made in Claude Code session
`https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments