Uh oh!
There was an error while loading. Please reload this page.
test(platform): gate canonical ExpressionInputSchema envelopes on platform Page exports - #11481
Merged
Merged
Conversation
…tform Page exports
A `Page` authored as a raw typed object literal is never parsed, so
`ExpressionInputSchema`'s bare-string -> `{ dialect: 'cel', source }` transform
never runs and whatever the author wrote reaches the wire verbatim. objectui
routes bare strings to its legacy JS evaluator, which has no `has()`; component
visibility is fail-soft, so a guarded predicate throws and resolves to SHOWN --
a declared gate stops gating, silently, in a production console bundle.
Every authoring-time signal stays green while that is true, so this adds the
gate rather than relying on review. Three parse doors, unioned:
1. `PageSchema` over the whole page -- every schema-typed position.
2. `PageComponentSchema` over each walked component -- the components nested
inside the opaque `properties` bag that door 1 walks straight past.
3. `ComponentPropsMap[type]` over each `properties` bag -- the expression keys
a per-type props schema declares (`record:alert.properties.visible`).
Positions are identified behaviourally, not by name: each door walks authored
vs parsed in lockstep and flags "the author wrote a string, the parse turned
that same string into an envelope". Nothing hardcodes `visibleWhen`, so the
deprecated `visibility` alias, every dialect, and any expression key added later
are covered without an edit here.
Each door's precondition is asserted too -- a door that cannot parse reports
nothing, which is indistinguishable from clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2`check:type-check-coverage --re-measure` reported `@objectstack/platform-objects: TEST_DEBT records 3 … now reports 4 (+1)`. This package's tsconfig.json excludes `**/*.test.ts`, so its own `pnpm typecheck` never read the new test file at all -- passing green while saying nothing about it. The ratchet is what reads that layer, and the error it found was TS1470: `import.meta` is not allowed in files that build into CommonJS output. Repaired the way this package's sibling `managed-api-method-affordance-sweep` already documents: seed from `__dirname`, which type-checks under the package's own config, is defined at runtime by vitest's transform, and stays one of the spellings `check:cross-package-test-inputs` resolves statically. The ledger is untouched -- fixing the error is the author's remedy; raising a shrink-only ratchet is not. Also sharpens the mutation control's precondition message: when the shipped page is ALREADY bare, that assertion is not the failure to read -- the gate above is, and it names the offending path. Re-measured: back to the recorded 3, all three pre-existing in feature-gate-guard.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
Contributor
📓 Docs Drift CheckNothing 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. |
os-steve
marked this pull request as ready for review
August 23, 2026 22:30
This was referenced Aug 23, 2026
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11255
The silent un-gating this closes
A
Pageauthored as a raw typed object literal (export const X: Page = { … }) is type-checked but never parsed, soExpressionInputSchema's bare-string transform never runs and whatever the author wrote reaches/api/v1/meta/pageverbatim; objectui routes bare strings to its legacy JS evaluator for its back-compat window, and that evaluator has nohas(). Component visibility is fail-soft, so a guarded predicate throws and resolves to SHOWN — a declared gate stops gating, and in a production console bundle nothing is logged at all, becauseSchemaRenderer's diagnostic probe sits behindif (__DEV__).Every authoring-time signal stays green while that is true: the type accepts the bare string,
tscpasses, the suite passes, the gate farm passes. That is why this is a gate and not a review habit. The one live instance was fixed in #11254; this makes the next one impossible to ship silently.Covered surface, and why
packages/platform-objectsSysUserDetailPage,SysOrganizationDetailPage,SysPositionDetailPagepackages/qa/downstream-contractWelcomePageprivate: true, a frozen third-party-consumer fixture whose file header states its purpose is to author "exactly like this", i.e. the way an external project on a published release naively would. It reaches no wire, and a gate demanding canonical envelopes there would contradict the contract the fixture exists to express.packages/cloud-connectionCloudConnectionSettingsPage*.page.tssweep missed, because it lives incloud-connection-ui.ts. It is published and does reach the kernel via the plugin's manifest bundle, so it is the same class — but it carries zero expression keys today, and covering it needs the detector in a shared home (@objectstack/lintpublic API) rather than a second copy. That is an API call, not a test call.The gate does not trust the filename. It scans this package's own
src/forexport const X: Page =declarations and asserts the pages barrel covers every one, so a page added tomorrow in a*-ui.tsfile is caught here rather than missed the way the original sweep missed one.Detector design
There is no single parse that reaches every expression position on a page, so the gate uses three doors and unions their findings:
PageSchemapropertiesPageComponentSchemaproperties(page:tabs→items[].children[],page:card→body/footer)propertiesbag itselfComponentPropsMap[type]propertiesrecord:alert.properties.visible)Doors 2 and 3 exist because
PageComponentSchema.propertiesisz.record(z.unknown())— an opaque bag served verbatim. Door 1 walks straight past a whole nested component tree, and a props-level predicate never reachesExpressionInputSchemaat all.Positions are identified behaviourally, not by name. Nothing hardcodes
visibleWhen. Each door walks the authored object and its parsed counterpart in lockstep and flags exactly one shape: the author wrote a string, and the parse turned that same string into an expression envelope. That is the observable signature of anExpressionInputSchemaposition and of nothing else — sovisibility, every dialect (cel/cron/template), and any expression key added later are covered with no edit here. Iteration is driven by the authored side, so keys the parse merely materializes (defaults) are never mistaken for findings. The deprecated-alias case is caught too: when the parse consumed an authored key and re-homed its value, the finding names both.Component traversal reuses
walkPageComponentsfrom@objectstack/lintrather than reimplementing it — that walk's own header records that duplicating it already produced one dead rule.Preconditions are asserted, not assumed. A door that cannot parse reports nothing, which is indistinguishable from clean. So each door's precondition is its own test (the page parses, every component parses, every type is mapped, every authored props bag parses). If one goes red, the message says which door stopped reading instead of this file going quietly green over a population it no longer covers.
Negative-control proof
Six controls, and each one shows a door catching something the others cannot — none is decoration:
properties→ caught by door 2, and door 1 run in isolation is asserted to find nothing — proof door 2 is load-bearing.propertiesbag → caught by door 3, with doors 1+2 run in isolation asserted to find nothing.visibilitykey → caught, reporting the canonical key it normalizes to.SysUserDetailPage, deep-cloned with its one envelope down-graded to the bare source it wraps → caught, namingsys_user_detail · slots.alerts[0].visibleWhen; the untouched export is asserted still clean in the same test.Beyond the in-memory controls, the operative acceptance criterion was exercised as a real ablation against the shipped source:
sys-user.page.ts's predicate was rewritten from thePenvelope to the bare string on disk (mutation confirmed on disk before the run — envelope-form occurrences 1 to 0, bare-form 0 to 1), the gate ran red at exit 1, and a restore trap put the file back (restore confirmed: envelope-form back to 1, bare-form 0,git status --porcelainempty). The red reads:Verification
All runs below are on
1060c357c3, serialized throughscripts/pm/os-verify-lock.sh; exit codes captured before any pipe.pnpm --filter @objectstack/platform-objects typecheck— exit 0.pnpm --filter @objectstack/platform-objects test— exit 0, 28 files / 454 tests passed (this file contributes 22).node scripts/check-cross-package-test-inputs.mjs --self-test && node scripts/check-cross-package-test-inputs.mjs— exit 0 each; "OK: 14 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob."node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(never a hand-rolled diff), all exit 0:check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-ci-filter-parity,check-plugin-teardown-shape,check-affected-docs,check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher,check:i18n,check:nul-bytes,check:type-check-debt.Two of those found real defects in this diff and are worth naming, since both are exactly the class this PR is about:
check:nul-bytes— a raw NUL byte materialized into the new file while it was being written. Fixed; the gate now reports "no raw ASCII control bytes".check:type-check-debt --re-measure— red with@objectstack/platform-objects: TEST_DEBT records 3 … now reports 4 (+1). This package'stsconfig.jsonexcludes**/*.test.ts, sopnpm typecheckpassing said nothing about the new test file; the ratchet is what reads that layer. The error wasTS1470: 'import.meta' … not allowed in files which will build into CommonJS output— the seed for the source scan. Repaired the way this package's siblingmanaged-api-method-affordance-sweep.test.tsalready documents (__dirname, which also stays a spellingcheck:cross-package-test-inputsresolves statically), not by touching the ledger. Re-measured: back to the recorded 3, all three pre-existing and infeature-gate-guard.test.ts.No changeset: this is a tests-only diff that publishes nothing, matching the two most recent merged tests-only PRs on
main(#11416, #11403), neither of which carried one.skip-changesetapplied.Scope: no
packages/spec/src/**in the diff — no contract acceptance changed, no public surface widened. Option B (routing platform pages throughdefinePage()) and objectui's bare-string back-compat window are untouched, per the card's triage.The first gate run surfaced no pre-existing looseness in the platform pages: all three parse clean through
PageSchema, all 34 walked components parse clean throughPageComponentSchema, and every authoredpropertiesbag parses against its declared props schema.Generated by Claude Code