Skip to content

fix(core): raise declared @objectstack/spec floor to ^17.3.0 - #9032

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-9012-core-spec-floor
Sep 10, 2026
Merged

fix(core): raise declared @objectstack/spec floor to ^17.3.0#9032
os-warren merged 2 commits into
mainfrom
claude/issue-9012-core-spec-floor

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #9012

@object-ui/core declared @objectstack/spec: ^17.2.0 in dependencies (consumer-facing) while normalizeListViewSchema folds objectui's legacy toolbar flags onto three userActions keys the protocol only adopted in 17.3.0. The declared range therefore admitted a spec that refuses this package's own output by name.

This raises the floor to ^17.3.0 and puts something in place to hold it there.

The measurement, re-derived on this base

Not inherited from the card. Every published 17.x was installed into its own isolated consumer project (npm install @objectstack/spec@VERSION into a throwaway package, so the spec resolves through that project's own node_modules and the package's own exports map). No workspace resolution takes part anywhere. The fold's real output came from the built packages/core/dist, not from a hand-copied literal:

fold output = {"search":true,"sort":true,"filter":true,"rowHeight":true,
               "group":false,"hideFields":true,"rowColor":true}
spec UserActionsConfigSchema parse of the fold output firing control
17.0.0 8 keys REFUSED [group, hideFields, rowColor] fires
17.1.0 8 keys REFUSED [group, hideFields, rowColor] fires
17.2.0 8 keys REFUSED [group, hideFields, rowColor] fires
17.3.0 11 keys ACCEPTED fires
17.4.0 11 keys ACCEPTED fires

The firing control is an undeclared key (spelled zzUndeclared) added to the same block. It is refused by all five versions, so ACCEPTED is not the reading of a schema that accepts everything and REFUSED is not the reading of one that refuses everything.

17.3.0 is the FIRST accepting version, not the convenient one

The published 17.x stable line is exactly 17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0 (npm view @objectstack/spec versions; the only other 17.x tags are 17.0.0-rc.0 through rc.6, which ^17.2.0 never admitted). All five were measured, so there is no unexamined gap between the last refusing and the first accepting release.

A second, independent key family lands on the same floor

ListViewSchema gained pageName in the same release. The spec-valid page view fixture this package already pins (normalize-list-view.pageResidual-8429.test.ts) measures:

17.0.0 / 17.1.0 / 17.2.0   REFUSED  refused-keys=[pageName]  + invalid_value on `type`
17.3.0 / 17.4.0            ACCEPTED

So the new floor is not propped up by one key family. (The separate viewType refusal is objectui's deliberately-declared legacy spelling, unchanged at every version, deferred to objectui#2231.)

What holds the floor, since the floors gate cannot

scripts/check-spec-range-floors.mjs judges symbol presence in the floor's published artifact. I verified its blindness here rather than assuming it: with the floor mutated back to ^17.2.0, the gate fetched 17.2.0, judged the same 165 (subpath, symbol) pairs, and returned zero findings for @object-ui/core. Same run at ^17.3.0: also zero findings for core. The gate is green either way, exactly as the card said.

The floor is held instead by a new package-local pin, normalize-list-view.declaredSpecFloor-9012.test.ts, which reads the manifest's own dependencies entry. Its comparator carries firing controls proving it can redden (^17.2.0, ^17.1.0, ^17.0.0, * and workspace:* must all fail the predicate; ^17.3.0 and ^17.4.0 must pass), and it re-anchors to the live spec so it is not just a number guarding a number.

Ablation. With the floor reverted on disk to ^17.2.0 (mutation proven to reach disk by blob-hash comparison against the HEAD blob), the new pin FAILS:

AssertionError: declared "^17.2.0" admits a spec below 17.3.0,
which refuses this package's own fold output: expected false to be true

...while the existing objectui#5435 pin stayed green through the same mutation. That is the point: the new pin catches precisely what the existing one structurally cannot. Restored with git checkout HEAD -- PATH; git diff HEAD clean afterwards.

Census: is core the only one?

The card left this unmeasured. Method, and it is decisive rather than a grep:

  1. Dumped every exported schema shape and enum vocabulary from all five published versions across all 18 export subpaths, then diffed.
  2. For every schema that gained keys, fired a control key at it at the floor version to classify it CLOSED or OPEN. Only a CLOSED schema can refuse a key by name, so only a closed-at-the-floor schema can exhibit this defect class at all.

28 manifests declare a @objectstack/spec range; 19 declare it consumer-facing (dependencies / peerDependencies), and only those floor anything. Result: @object-ui/core is the only package where a control-backed measurement shows the declared floor admitting a spec that refuses what the package itself produces.

Two candidates were chased and refuted by measurement, which is worth recording:

  • @object-ui/plugin-gantt (floor ^17.0.0) looked like a stronger case than core: it imports GanttConfigSchema as a runtime value, calls safeParse on the authored block, and uses all ten keys the spec only declared at 17.4.0. But the firing control did not fire at 17.0.0 through 17.3.0 — GanttConfigSchema is an open schema at those versions, so it refuses nothing and there is no refusal to inherit. Not this defect class.
  • @object-ui/app-shell (floor ^17.0.0) reads userActions.group / .hideFields / .rowColor, but reads them off an already-authored view rather than emitting into a parsed structure; its runtime safeParse sites (ManifestSchema, ViewFilterRuleSchema) land on schemas that are open at its floor. Not established.

Both are written up in the report with their numbers. A real, separate consequence I did measure on plugin-gantt: FLAT_GANTT_CONFIG_KEYS is derived from GanttConfigSchema.shape at runtime, so on a resolved 17.0.0-17.3.0 it carries 19-20 members instead of 30 and its shadow diagnostic goes blind for ten keys. That is a degraded diagnostic, not a protocol refusal — reported, not fixed here.

Should the gate learn this class?

Not in this PR, per the dispatch order's own condition: I could not make it small. A behavioural criterion needs the floor tarball's runtime schemas parsed (the gate today reads type declarations with the TypeScript parser and never executes the artifact), plus a fixture per emitting site to parse against. That is a different gate, and the existing gate's header argues at length against widening its criterion. Filed as a card rather than smuggled in.

Verification

  • pnpm exec vitest run packages/core/142 files, 3021 tests passed
  • pnpm --filter @object-ui/core run type-check (tsc --noEmit + tsc -p tsconfig.test.json, which covers the new test) — exit 0
  • node scripts/check-changeset-presence.mjsexit 0, 1 changeset declared
  • node scripts/check-control-bytes.mjsexit 0 (7226 files)
  • node scripts/check-changeset-no-major.mjsexit 0
  • node scripts/check-governed-queue-guard.mjs --test on all four changed paths — NOT GOVERNED
  • eslint on both changed sources — 0 errors, 0 warnings (counts read from --format json, not grepped text)

check:spec-floors reports 16 no-artifact findings in this container because only core and types are built; that is a prerequisite-not-met, not a red, and core itself was judged clean. A full-workspace build per PR was ruled against on objectui#4846, so that gate stays CI's.

Every exit code was captured to a file before any pipe.

Scope

packages/core/package.json, two test files under packages/core/src/utils/__tests__/, and a changeset. Nothing in the reserved file surface was touched.


Generated by Claude Code

`@object-ui/core` declared `@objectstack/spec: ^17.2.0` in `dependencies`
(consumer-facing), while `normalizeListViewSchema` folds objectui's legacy
toolbar flags onto the `userActions` keys `group` / `hideFields` / `rowColor`,
which the protocol adopted only in 17.3.0. Any consumer resolution landing on
17.0.0 / 17.1.0 / 17.2.0 satisfied the declared range and got a normalizer whose
output the spec refuses BY NAME.

Re-measured against published artifacts, not the workspace copy: every published
17.x installed into its own isolated consumer project, the fold's real output
parsed against that install's own `./ui` entry.

    17.0.0 / 17.1.0 / 17.2.0   REFUSED  refused-keys=[group, hideFields, rowColor]
    17.3.0 / 17.4.0            ACCEPTED

An undeclared firing-control key was refused by ALL FIVE versions, so the
contrast measures those three keys and not the harness. 17.3.0 is the FIRST
accepting version across the whole published 17.x stable line, not the first one
that happened to work.

A second, independent key family lands on the same floor: `ListViewSchema` gained
`pageName` in 17.3.0, so the `page` view fixture this package already pins is
refused before it and accepted from it.

`scripts/check-spec-range-floors.mjs` cannot hold this: its criterion is symbol
PRESENCE, and `UserActionsConfigSchema` is exported by every version above. The
floor is held instead by a new pin carrying firing controls that prove its
comparator can redden. The standing instruction in the objectui#5435 pin — read
the file reddening on a resolved 17.2.x as "the declared floor is too low" — is
discharged in place.

Refs objectui#9012

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Console Performance Budget — not measured

This run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.

Step Outcome
Build packages skipped
Check console performance budget skipped

See the workflow run for details.

No package size report: it is only generated from a complete package build, so a partial one is never shown.

`pnpm-lock.yaml` records each importer's DECLARED specifier beside the resolved
version, so raising `packages/core`'s range without regenerating the lockfile
left the two disagreeing. CI installs with `--frozen-lockfile`, which refuses
that mismatch before any job does its own work — every check on the first push
failed with one root cause:

    specifiers in the lockfile don't match specifiers in package.json:
    * 1 dependencies are mismatched:
      - @objectstack/spec (lockfile: ^17.2.0, manifest: ^17.3.0)

Regenerated with `pnpm install --lockfile-only`. The diff is exactly one line —
the `specifier:` for `packages/core`'s `@objectstack/spec`. The resolved
`version:` beside it is unchanged at `17.4.0`, which is the direct evidence that
this moves no resolution for anybody: the floor is a declaration, and every
install already landed above it.

`pnpm install --frozen-lockfile` now exits 0, and
`scripts/check-lockfile-integrity.mjs` reports clean.

Refs objectui#9012

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.9 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-D-1-EOLB.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 207.56KB 57.44KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.64KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 130.98KB 34.54KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.44KB 65.17KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 211.36KB 57.45KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review

Head reviewed: 24f89ec4683bf9a7ca6d64478530022b685254c2 (claude/issue-9012-core-spec-floor, PR #9032, card #9012, round 1). Verified as the branch tip and the PR head immediately before posting.

Charter read at: objectstack origin/main efa2533dd88a6c8304761253c9916f81699c3dc9.claude/skills/pm-dispatch/references/contract-review.md (all 60 lines), landing-operations.md, true-green.md, platform-readings.md, SKILL.md §入队与落地. Fresh sparse clone, not the local checkout.

Tier: CONTRACT_REVIEW_TIER = 'claude-fable-5-1', read from scripts/pm/dispatch-gates.mjs:10507 at that sha. Served tier read from the harness per-message model stamp in this review's own transcript (agent-ae77a5d7ec5d0c8e4): 95/95 claude-fable-5-1, one distinct value, at the pre-post census. Not self-asserted, not a launch parameter.

Method — independent re-derivation, treating the PR body and the os-dev-report as claims to falsify

  • Every published @objectstack/spec 17.x stable — 17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0 — installed with npm install @objectstack/spec@VERSION into its own throwaway consumer project; ./ui and ./kernel imported through that install's own exports map. No workspace resolution anywhere.
  • The fold output was taken from @object-ui/core's built dist/utils/normalize-list-view.js at this head (fresh clone, turbo run build --filter=@object-ui/core), not from a literal copied out of the PR.
  • Version census straight from the registry (npm view @objectstack/spec versions, 166 versions): 17.x is exactly seven 17.0.0-rc.* tags (admitted by neither the old nor the new range) plus the five stables above; dist-tags.latest = 17.4.0 (published 2026-09-09), nothing newer. So the bisection below covers the whole published line, not its endpoints.

① Derived judgments — every accept-set / public-surface change the diff produces

  1. packages/core/package.jsondependencies["@objectstack/spec"]: ^17.2.0^17.3.0. Published manifest, public surface. The set of spec versions a consumer may resolve for core narrows from {17.2.0, 17.3.0, 17.4.0, future 17.x} to {17.3.0, 17.4.0, future 17.x}. RIGHT. Measured: the real fold output is {"search":true,"sort":true,"filter":true,"rowHeight":true,"group":false,"hideFields":true,"rowColor":true} (identical in key set and values to the PR's literal). UserActionsConfigSchema declares 8 keys at 17.0.0 / 17.1.0 / 17.2.0 and REFUSES that object with unrecognized_keys = [group, hideFields, rowColor]; declares 11 keys at 17.3.0 / 17.4.0 and ACCEPTS it. Controls: an undeclared key zzUndeclared is refused by all five (the negative control fires everywhere); {rowHeight:true} is accepted by all five (the positive control — the harness is not a schema that refuses everything). Even the minimal emission, one legacy flag → {rowColor:true}, is refused at 17.0.0–17.2.0. Last refusing = 17.2.0, first accepting = 17.3.0, no unexamined member between them. Direction against the protocol: this narrows no accept-set — core's inputs and outputs are byte-identical before and after; what changes is that the manifest stops claiming to work against a protocol version that refuses core's own emission, which is the Packages whose published d.ts references spec-17.1.0-only symbols still declare "@objectstack/spec": "^17.0.0" — the range admits a spec that lacks the symbols #5793 direction ("floors track reality"). For the record: the spec sits in dependencies, not peerDependencies, so this floor governs core's own resolution and is a public claim about core; it does not by itself pin the spec version a downstream parser uses. That is the deferred sweep's ground, not a defect in this diff.
  2. Second, independent key family on the same floor. The page fixture that normalize-list-view.pageResidual-8429.test.ts already pins is REFUSED by ListViewSchema at 17.0.0 / 17.1.0 / 17.2.0 (invalid_value on type, unrecognized_keys = [pageName]; the type enum has 9 members) and ACCEPTED at 17.3.0 / 17.4.0 (10 members, page added). RIGHT — it lands on 17.3.0 by a different route.
  3. pnpm-lock.yaml — importer packages/core: specifier: ^17.2.0^17.3.0, version: 17.4.0(ai@7.0.65(zod@4.4.3)) unchanged. Exactly one line (the hunk has 2 +/- lines). Not a published surface; moves no resolution for anyone. RIGHT. pnpm install --frozen-lockfile on a fresh clone of this head: exit 0. Lockfile Integrity Check green on the head.
  4. normalize-list-view.declaredSpecFloor-9012.test.ts (new) — the holder. Not in files, no public surface. RIGHT as a holder. 4/4 pass on the head. Ablation in my own clone with the floor mutated back to ^17.2.0 (mutation proven on disk: blob fb341c8c… differs from the HEAD blob 54cca7cc…): the pin FAILS with exactly declared "^17.2.0" admits a spec below 17.3.0, which refuses this package's own fold output; the pre-existing 5435 pin stays green through the same mutation (8/8) — it reads the resolved spec, not the manifest, which is the gap this file closes. Comparator controls read as written: ^17.2.0, ^17.1.0, ^17.0.0, ^16.99.99, *, workspace:* → false; ^17.3.0, ^17.4.0 → true; any range it cannot parse fails closed. new URL('../../../package.json', import.meta.url) resolves to packages/core/package.json. The anchor leg reads the resolved spec's .shape (11 keys, the three present) and the pageName fixture, so the number is tied to the live protocol rather than to itself. Noted, not flagged: a range spelled with a bare greater-or-equal comparator would satisfy the comparator without an upper bound — an upper-bound question this card does not ask.
  5. normalize-list-view.foldOutputAuthorable-5435.test.ts — docblock only. Zero non-comment changed lines against base 681d3f10e14ebd9b5a7074bed718971fb811bce9. No behaviour change. RIGHT. The standing instruction it carried ("reddening on a resolved 17.2.x means the floor is too low") is discharged by this PR, which is exactly what the rewritten docblock says.
  6. The stated reason a pin holds the floor rather than scripts/check-spec-range-floors.mjs. RIGHT, measured, not trusted. Gate run in my clone with core and types built: fetched @objectstack/spec@17.3.0 from the registry, read 6 entry points through the fetched package's own exports, judged 165 (subpath, symbol) pairs, 16 findings — every one no-artifact for an unbuilt package — and none for core. Same run with the floor mutated to ^17.2.0: fetched 17.2.0 (so the subject was looked at), same 165 pairs, still no finding for core. Green either way, by measurement. The gate's workflow is schedule plus publish-path only and its header forbids a pull_request trigger, so it did not and cannot run on this PR; that is why a package-local pin is the right holder.
  7. Whole-package control the dev did not run. Overriding the workspace so that @objectstack/spec resolves to exactly 17.3.0 (verified from packages/core and packages/types; lockfile specifier: 17.3.0 / version: 17.3.0), core's entire suite passes: 142 files, 3021 tests. The declared floor holds for the package's runtime imports (composeStacks, isRuntimeDefaultToken, SEARCH_VIRTUAL_TYPES, CONTEXT_TOKENS, DATE_RANGE_PRESETS, chartAggregate*Key, isAcceptedFilterComparand, FieldSchema, …) and its tests, not only for the fold. RIGHT^17.3.0 is neither too low nor higher than it needs to be.

② Semver grading

Changeset .changeset/9012-core-spec-floor.md declares '@object-ui/core': patch. Consistent with the change level. On every resolution that worked before (17.3.0, 17.4.0) the runtime is identical — same normalizer, same output; the only resolutions the new range removes (17.2.0, and 17.0.0 / 17.1.0 which ^17.2.0 never admitted) already produced protocol-refused output, so no consumer that worked stops working. Not major (repo rule: the fixed group never declares major; Changeset Bump Policy is check-changeset-no-major.mjs, green). Not a breaking-with-prose minor either, because nothing that worked breaks. The fixed group in .changeset/config.json carries all members to the same patch. Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report all green on the head.

③ Boundary-flag disposition — every dev flag, one by one

open_questions: [] — nothing owed there. The six out_of_scope_findings and the PR-body flags:

  1. Gate not extended; filed as finding(gate): check-spec-range-floors judges symbol PRESENCE, so a floor admitting a spec that declares the symbol but refuses the KEYS a package emits is green either way #9036. ESCALATED — verified. finding(gate): check-spec-range-floors judges symbol PRESENCE, so a floor admitting a spec that declares the symbol but refuses the KEYS a package emits is green either way #9036 exists, open, finding / tooling / domain:devx, carries the closed-vs-open trap and the 165-pair blindness measurement. It meets the dispatch condition ("only if small, with a firing control") by showing it is not small: the gate never executes the artifact, and key-set membership is a runtime Zod property.
  2. plugin-gantt FLAT_GANTT_CONFIG_KEYS degraded diagnostic, carried in finding(gate): check-spec-range-floors judges symbol PRESENCE, so a floor admitting a spec that declares the symbol but refuses the KEYS a package emits is green either way #9036. ESCALATED — verified in finding(gate): check-spec-range-floors judges symbol PRESENCE, so a floor admitting a spec that declares the symbol but refuses the KEYS a package emits is green either way #9036 §"Related, not this". My reading of GanttConfigSchema.shape across 17.0.0 → 17.4.0: 18 / 19 / 19 / 19 / 29 keys, consistent with the "19–20 instead of 30" reading.
  3. Zone-2 correction of the card's manifest count (23, not 26). ANSWERED — the dev's correction holds. My census of every package.json at this head declaring @objectstack/spec: 29 files including the workspace root (dev-only, floors nothing), 28 excluding it. ^17.0.0 = 23 excluding the root (24 with it); ^17.1.0 = 2 (plugin-detail, react); ^17.2.0 = 1 after this PR (data-objectstack; 2 before it); ^17.3.0 = 2 after (core, types; 1 before). Consumer-facing 19, dev-only 9 plus the root. The card's "26" was wrong.
  4. plugin-gantt refuted hypothesis — GanttConfigSchema OPEN at 17.0.0–17.3.0. ANSWERED — confirmed with a control that could have fired: a document carrying the three required fields plus zzUndeclared is ACCEPTED at 17.0.0 / 17.1.0 / 17.2.0 / 17.3.0 (open, nothing refused) and REFUSED with unrecognized_keys only at 17.4.0 (closed). Not this defect class.
  5. app-shell "not established; its safeParse sites land on schemas that are OPEN at its floor". ANSWERED — conclusion stands, the stated reason is half wrong. ManifestSchema (./kernel, PackageFormDialog.tsx): OPEN at 17.0.0–17.2.0 (24 keys) — correct at app-shell's ^17.0.0 floor — and CLOSED from 17.3.0 (25 keys). But ViewFilterRuleSchema (./ui, ObjectDataPage.tsx) is CLOSED at every one of the five versions: a valid rule plus zzUndeclared returns unrecognized_keys at 17.0.0 already. It carries the same three keys (field, operator, value) at all five, so no key was ever gained and no floor can admit a version that refuses a key a later one declares. "Not established" is right on that ground, not on openness. Belongs to the deferred sweep; nothing for this PR.
  6. Census method bug — Zod v4 schema instances are typeof 'function'. ANSWERED — hazard confirmed: typeof UserActionsConfigSchema === 'function' at all five installs, so a typeof !== 'object' guard would skip the very subject. The corrected census's conclusion for core is what ①.1 and ①.7 re-measure here independently of that tool.
  7. PR body: check:spec-floors reports 16 no-artifact findings with only core and types built. ANSWERED — reproduced exactly (16 findings, all no-artifact, core clean; ①.6). Prerequisite-not-met, not a red; the gate is not a per-PR job by its own header and by objectui#4846.
  8. PR body: the separate viewType refusal is deferred to Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231. ESCALATED already — verified: Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231 is open (ListViewSchema drift, domain:spec, pm:blocked). Unchanged across all five versions, so not a floor matter.
  9. Observation from this review, out of scope, for the seat — reserved file surface, ⛔ not for this PR. packages/types/src/zod/objectql.zod.ts:607 says UserActionsConfigSchema is "NOT .strict()" and that unknown keys were "silently stripped". Every published 17.x artifact refuses unknown keys by name (①.1 negative control). The comment is stale; the .extend() beneath it is harmless because 17.3.0+ already declares the three keys. A docblock fix under Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231 or a follow-up, never a rider here.
  10. Card decision A vs B. The PR takes A (bump, and hold it with a pin). Judged the only reading consistent with Packages whose published d.ts references spec-17.1.0-only symbols still declare "@objectstack/spec": "^17.0.0" — the range admits a spec that lacks the symbols #5793's "floors track reality" and with the maintainer's rule that objectui follows the protocol: the alternative B would keep a published claim the protocol refutes by name.

Check runs on the head

36/36 terminal (total_count 36 = returned array length 36, nothing behind pagination): 33 success, 3 skipped (Test (coverage shard ${{ matrix.shard }}/4), Test (coverage), dependabot — two matrix placeholders and a bot job), 0 failure, 0 cancelled. Legacy commit status 1/1 success (Vercel). Four third-party check suites (vercel, fly-io, claude, cloudflare) sit queued with 0 check runs — app installations without checks, not checks. Landing prerequisites beyond this record (check-clause2-carriers.mjs --pair 9032, governed-surface test, draft flip, auto-merge) are the seat's acts and were not performed here.

Independence

Implemented-by: claude/issue-9012-core-spec-floor
Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w

Same-session test, applied literally to the two identities as the charter defines them (contract-review.md L35–37 at efa2533d): a mode:subagent dev has no session of its own and is recorded by its branch; the seat issuing the ruling is recorded by its session. A branch and a session are not the same session, so the SELF-REVIEW clause does not fire. Stated plainly so the parent does not read more into it: the dev subagent was dispatched from that same seat session (the card's Claim: says so), and this record was produced by a context-isolated subagent of that seat, fed only the card, the existing rulings and the PR body — which is the shape L33–34 prescribe. The independence here is context isolation plus re-measurement from published artifacts, exactly what the charter grants and nothing beyond it.

Not measured here

  • Any downstream parser outside this repository (the protocol's own view save gate, the platform server); this record judges core's declared claim, not who enforces it.
  • The behavioural floors of the other 18 consumer-facing manifests beyond the three schemas probed in ③.4–5; that is the sweep the card defers.
  • An independent enumeration of every spec symbol core's dist references; the gate's own 165-pair count was taken as the population, with its fetched version verified in both runs.
  • The narrative of the first push's CI failure (frozen-lockfile mismatch); irrelevant to this head.
  • Lint and type-check locally; relied on Lint and Type Check green on the head plus the vitest runs above.

Verdict vocabulary: the charter (L29) spells the judgment PASS/FAIL; the dispatch brief spelled it PASS/REWORK. This one is PASS under both.

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — carriers cleared, landing

Clause-② contract review: VERDICT: PASS — record 5624799322 (#9032 (comment)), judged on head 24f89ec4683bf9a7ca6d64478530022b685254c2, verified as branch tip and PR head before posting and unchanged at this write.

Reviewer ran at CONTRACT_REVIEW_TIER = 'claude-fable-5-1' (scripts/pm/dispatch-gates.mjs:10507), transcript-verified 95/95 at the pre-post census and 107/107 at the final one, one distinct value, zero foreign stamps. Charter read at objectstack origin/main efa2533dd88a6c8304761253c9916f81699c3dc9 — the reviewer found a pre-existing cac62d5f charter copy in the scratchpad and discarded it rather than reading it, which is the right instinct and worth recording.

Independence — NOT self-review:

Implemented-by: claude/issue-9012-core-spec-floor
Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w

contract-review.md:36 records a mode:subagent dev by its branch (such a dev has no session), :35 records the ruling seat by session, and :37 fires only when both are the same session. A branch and a session are not the same session. This is the third review to reach that reading independently today, each from the charter text rather than from any brief.

Landing pre-checks — all measured just now

check reading
① in-seat clause-② review PASS on record, 同形, ceiling tier ✅ record 5624799322 on the head
check-clause2-carriers.mjs --pair 9032 rc 0 — both carriers agree
check-widening-tells.mjs --declaration yes --files rc 0
③ every check on the head, ⛔ not a required subset 36 check runs, 33 success + 3 skipped, 0 failed, 0 pending; total_count 36 = array length 36; combined commit status success
governed surface check-governed-merges.mjs --pr objectstack-ai/objectui#9032 → 0 of 5 paths hit the register ⇒ NOT governed
mergeability mergeable: true, mergeable_state: clean

What the review added beyond confirming the dev

  • The bisection is complete, not sampled. Real fold output from a built dist parsed against isolated installs of all five published stables: refused by name [group,hideFields,rowColor] at 17.0.0/17.1.0/17.2.0, accepted at 17.3.0/17.4.0, with zzUndeclared refused by all five and {rowHeight:true} accepted by all five as controls. Registry census confirms 17.x is exactly those five plus 7 rc tags.
  • A control the dev did not run: with spec pinned to exactly 17.3.0 (resolution verified), core's full suite passes 142 files / 3021 tests. So the floor holds for the whole package, not only for the fold that motivated it.
  • One dev flag corrected: the app-shell screening said ViewFilterRuleSchema is open at its floor — it is in fact CLOSED at all five versions. The dev's conclusion ("not established") still stands, because the schema carries the same three keys throughout; the reasoning was wrong, the answer right.
  • Manifest census confirmed at the dev's corrected 23 (29 files including root, 28 excluding; 19 consumer-facing) — the card body's "26" was the overstatement the dev caught.

One out-of-scope observation, already in flight — ⛔ not a new card

The reviewer noted that packages/types/src/zod/objectql.zod.ts:607's "NOT .strict()" comment is contradicted by every published artifact. That is precisely what PR #9017 fixes — its changeset calls the docblock "the urgent half" and pins the refusal with firing controls in both directions. #9017 is in the merge queue as of 20:10:09Z. The reviewer could not have known: it was fed only this card, the rulings on it, and this PR, exactly as contract-review.md:34 requires. Recorded here so nobody files it twice.

Both carriers (needs:contract-review on this PR and on card #9012) are stripped in this same act, and the PR flipped ready and enqueued.

⛔ Base is not being hand-merged. The merge queue rebuilds each PR on current main itself; hand-merging would move the head and void the record above.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(core): @object-ui/core's declared @objectstack/spec: ^17.2.0 floor admits a spec that refuses its own fold's userActions output BY NAME

2 participants