Repository navigation
Commit 4e0f72e
fix(spec): project the react-blocks contract through projectPublishedJsonSchema (#20304)
Fixes #19100
Clause-②: no
## What this does
`packages/spec/scripts/build-react-blocks-contract.ts` read each block's
spec schema through a bare `z.toJSONSchema(schema, { unrepresentable:
'any' })`. That call sat outside the published-projection choke point,
so the refinement override that every published JSON Schema artifact
carries was not applied to the schemas behind
`skills/objectstack-ui/references/react-blocks.md`. The generator now
projects through `projectPublishedJsonSchema(schema, { unrepresentable:
'any' })`. Its allowance row in
`published-projection-choke-point.test.ts` and the prose naming it are
removed.
**Measured outcome: branch (A), byte-identical.** The shipped
`react-blocks.md` is not in this diff, and no `skills/**` path is
touched.
## Measurement (taken before any repair, on `3f86dc52f2`)
### The shipped file, regenerated with the package's own generator
(`pnpm --filter @objectstack/spec gen:react-blocks`)
```
blob before: 573f51b
blob after : 573f51b
cmp before after: exit 0
$ git status --porcelain (after gen:react-blocks, generator edited)
M packages/spec/scripts/build-react-blocks-contract.ts
$ git diff --stat
packages/spec/scripts/build-react-blocks-contract.ts | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
```
The shipped file has no hunk. `check:react-blocks` on the final tree: `1
generated files in sync with packages/spec`.
### JSON projection of the 3 spec-backed block schemas, bare call vs
`projectPublishedJsonSchema`
| block | schema | before | after | identical |
|---|---|---|---|---|
| ObjectForm | `FormViewSchema` | 17923 B, sha256 `caec4524119dbe87` |
17923 B, sha256 `caec4524119dbe87` | yes |
| ListView | `ListViewSchema` | 93656 B, sha256 `7dad3d8b8b3dd51c` |
94666 B, sha256 `773bae792a7d435e` | **no** |
| ObjectChart | `ChartConfigSchema` | 13619 B, sha256 `b1974a1cf703a528`
| 13619 B, sha256 `b1974a1cf703a528` | yes |
The fourth block (`Block`) has no spec schema; it is overlay only.
**The divergent schema is `ListViewSchema`, at exactly two JSON paths:**
- `properties/conditionalFormatting/items/properties/condition/anyOf/1`
- `properties/bulkActionDefs/items/properties/visible/anyOf/1`
Both are the object branch of `EvaluatedExpressionInputSchema`, which is
`EvaluatedExpressionSchema`. The bare call dropped two declared
refinements there:
1. `NON_BLANK_STRING` on `source`
(`packages/spec/src/shared/expression.zod.ts:172`). Through the override
it projects as `"minLength": 1, "pattern": "\\S"`.
2. `requiredOneOf(['source', 'ast'])` on `ExpressionSchema`
(`expression.zod.ts:105`), which `EvaluatedExpressionSchema` inherits
via `safeExtend`. Through the override it projects as `allOf: [{ anyOf:
[{ required: ["source"] }, { required: ["ast"] }] }]`.
ObjectForm and ObjectChart are identical because their projections reach
no Expression envelope at all: zero `dialect` and zero `source` nodes,
so the override has nothing to write.
**Why the markdown does not move:** the generator keeps only the props
named in the block's `dataProps` allow-list
(`build-react-blocks-contract.ts:93`). ListView's list
(`packages/spec/src/ui/react-blocks.ts:305`) is `type, data, columns,
sort, searchableFields, userFilters, pagination, grouping, rowHeight,
selection, rowActions, inlineEdit`. It names neither
`conditionalFormatting` nor `bulkActionDefs`, so both divergent subtrees
are filtered out before any row is rendered. So no sentence on the
AI-facing page is wrong today. The change makes the generator project
the way the other published producers do; it does not alter what the
page says.
Full ListView projection diff (only these hunks):
```diff
@@ -1861,7 +1861,9 @@ (conditionalFormatting[].condition, object branch)
"source": {
- "type": "string"
+ "type": "string",
+ "minLength": 1,
+ "pattern": "\\S"
},
@@ -1881,7 +1883,23 @@
- "additionalProperties": false
+ "additionalProperties": false,
+ "allOf": [
+ { "anyOf": [ { "required": ["source"] }, { "required": ["ast"] } ] }
+ ]
@@ -1933,7 +1951,9 @@ (bulkActionDefs[].visible, object branch: same two additions)
@@ -1953,7 +1973,23 @@
```
(The `allOf` body is shown compacted; the emitted JSON is the same
structure, pretty-printed.)
## The diff
- `packages/spec/scripts/build-react-blocks-contract.ts`: import
`projectPublishedJsonSchema`, drop the `zod` import, route the one
projection through the helper, and update the header comment.
- `packages/spec/scripts/published-projection-choke-point.test.ts`:
- remove the `build-react-blocks-contract.ts` row from
`DECLARED_DIRECT_CALLS`, so the allowance goes from 3 rows to 2 (the
choke point itself plus the representability probe);
- rewrite the docblock that named it;
- add the generator to `PUBLISHED_PRODUCERS`, so the pin also asserts
that it imports the helper, the same hold `build-schemas.ts` and
`build-openapi.ts` are under.
- `packages/spec/scripts/lib/refinement-projection.ts`, **comment
only**: the choke point's docblock listed this generator as a direct
`z.toJSONSchema` caller outside the helper, which this change makes
false. It is now producer 5 of 5, and the "outside" bullet names only
the CLI's `os generate`.
## Governed-surface routing
`node scripts/pm/check-governed-merges.mjs --branch
claude/issue-19100-react-blocks-published-projection` gives exit 0: `0
of 3 path(s) hit the register ... NOT governed`, size 58 changed lines.
No `skills/**` path is in the diff.
About the generated-artifact exception: the same predicate, run with
`skills/objectstack-ui/references/react-blocks.md` added to the list,
gives exit 3, and the exception does **not** lift the path: `the tree
under test modifies the generator this exception trusts — the path stays
governed ... land the generator change and the artifact regeneration as
separate PRs`. So if the bytes had changed, this PR would have been Tier
H, with no exception route available. They did not change, so the
question does not arise.
## Changeset
None. `skip-changeset` applies because nothing here ships:
`@objectstack/spec`'s `files[]` is `dist, json-schema, liveness,
prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md,
api-surface, spec-changes.json`, and `scripts/` is not in it.
## Verification (final tree `c64a07ed94`)
- `pnpm --filter @objectstack/spec exec vitest run --project repo
scripts/published-projection-choke-point.test.ts`: 6/6 passed, VERDICT
command-exit 0.
- `vitest run --project local` on
`scripts/refinement-projection.test.ts` and
`src/ui/react-blocks.test.ts`: 2 files, 90 tests passed.
- `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit`,
`check:scripts-typecheck`, `check:test-typecheck`): VERDICT command-exit
0.
- `pnpm --filter @objectstack/spec test` (`--project local`): 551 files
passed and 1 skipped; 16253 tests passed, 1 skipped, 1 todo; VERDICT
command-exit 0.
- `check:react-blocks`: exit 0, in sync.
- Derived gate set (`node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`, 61 commands): 56 exit 0,
including `check:react-blocks`, `check:authorable-surface`,
`check:liveness`, `check:pm-governed-merges`, `check:pm-dispatch-gates`,
`check:nul-bytes`, `check:comment-mask-adoption` and
`check:test-source-alias`. 5 are **NOT MEASURED** (exit 3, PREREQUISITE
NOT MET): `check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content` and
`check:type-check-debt`. Each needs a whole-workspace `dist/` build,
which this diff cannot move, because no changed file ships. They are
declared to CI. `dispatch-gates --ran` reconciliation: 61 derived, 56
run, 5 NOT-MEASURED (derived from the recorded exit 3), 0 unrun.
- Targeted lint (`eslint --no-inline-config --format json` on the 3
changed files): 3 files linted, 0 errors, 0 warnings. The narrowing
excludes nothing: the ESLint config never enables type-aware linting (no
`parserOptions.project`, no typed rules, as stated at
`eslint.config.mjs:326-328`), so this diff cannot change the verdict on
any untouched file. The full `pnpm lint` run is CI's.
- NOT MEASURED locally: the rest of the spec `repo` vitest project (32
other files). It is outside the package's `pnpm test` script, and my one
local attempt was cut off by my own timeout after a 256 s lock wait. Its
one file related to this diff, the choke-point pin, ran green on its own
(above).
### Reverse verification (one-time, not kept as test files)
Both runs used `node scripts/ablation-replace.mjs` against the committed
tree `c64a07ed94`. Each restore was proven by blob equality with HEAD
(`224f9f42639e`) and an empty `git diff HEAD`.
1. Replacing the helper call with the old bare call makes the pin go red
as predicted: `no producer in scripts/ reaches z.toJSONSchema directly`
fails with `"build-react-blocks-contract.ts: 1 direct call(s), declared
0"` (1 failed, 5 passed).
2. Deleting the helper import makes the pin go red as predicted: `every
published producer imports the helper it is required to project through`
fails on `build-react-blocks-contract.ts` (1 failed, 5 passed).
## Acceptance notes
- ADR-0082 (line 39) describes the generator as reading the spec schemas
through `z.toJSONSchema`. That is still true at the mechanism level,
because the helper is the call that reaches it, so the ADR is not edited
here. It is a governed surface and outside this card.
- The ListView divergence is real but has no rendered effect today. If
`conditionalFormatting` or `bulkActionDefs` ever join ListView's
`dataProps`, the row type still renders from the top-level node
(`object[]`), which the override does not change.
- PR #20262 also regenerates one row of `react-blocks.md`. This PR does
not touch that file, so the two cannot conflict.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6ac33a5 commit 4e0f72e
3 files changed
Lines changed: 30 additions & 28 deletions
File tree
- packages/spec/scripts
- lib
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
7 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | | - | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
72 | 73 | | |
73 | 74 | | |
74 | 75 | | |
75 | | - | |
| 76 | + | |
76 | 77 | | |
77 | 78 | | |
78 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
297 | | - | |
| 297 | + | |
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
311 | 316 | | |
312 | 317 | | |
313 | 318 | | |
314 | | - | |
| 319 | + | |
315 | 320 | | |
316 | 321 | | |
317 | 322 | | |
| |||
323 | 328 | | |
324 | 329 | | |
325 | 330 | | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
332 | 335 | | |
333 | 336 | | |
334 | 337 | | |
| |||
Lines changed: 12 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
31 | | - | |
| 30 | + | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
| 35 | + | |
39 | 36 | | |
40 | | - | |
| 37 | + | |
41 | 38 | | |
42 | 39 | | |
43 | 40 | | |
| |||
113 | 110 | | |
114 | 111 | | |
115 | 112 | | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | 113 | | |
122 | 114 | | |
123 | | - | |
124 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
125 | 123 | | |
126 | 124 | | |
127 | 125 | | |
| |||
0 commit comments