Skip to content

fix(spec): project the react-blocks contract through projectPublishedJsonSchema - #20304

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19100-react-blocks-published-projection
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19100-react-blocks-published-projection

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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: 573f51b2704cdc10ba8732e978919d377ac5d24e
blob after : 573f51b2704cdc10ba8732e978919d377ac5d24e
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):

@@ -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 feat(spec)!: form layout accepts only vertical | horizontal — the inline and grid arms retired (#20221) #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

…onSchema

The react-tier contract generator read each block's spec schema through a
bare z.toJSONSchema, outside the published-projection choke point, so the
refinement override every published JSON Schema artifact carries was not
applied to the schemas behind skills/objectstack-ui/references/react-blocks.md.

Route it through projectPublishedJsonSchema. Measured: of the three
schema-backed blocks, ObjectForm and ObjectChart project byte-identically
either way; ListView diverges only under conditionalFormatting[].condition
and bulkActionDefs[].visible (the EvaluatedExpression envelope's non-blank
source and source-or-ast refinements), neither of which is in ListView's
dataProps allow-list, so the regenerated react-blocks.md is byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
With the react-tier contract generator projecting through
projectPublishedJsonSchema, its direct-call allowance in
published-projection-choke-point.test.ts no longer describes the tree:
remove the row and the prose that named it, and hold the generator to the
helper the way build-schemas.ts and build-openapi.ts are held
(PUBLISHED_PRODUCERS). The choke point's own docblock named the generator as
a direct caller outside the helper; it now lists it as the fifth producer.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6a6a17b62e8b58d0ee31cbfabf5fc7322032ba94 → packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

2 participants