Skip to content

docs(plugin-detail): retire the stale strip-mode narration in two spec-parity tests - #5879

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4918-stale-strip-mode-prose
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-4918-stale-strip-mode-prose

Conversation

@claude

@claude claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #4918

Comment-only. No assertion changed meaning — every added and removed line in this diff is a // comment (proof below).

Two plugin-detail spec-parity blocks narrated strip mode as a present-tense fact about the installed @objectstack/spec. The props schemas were closed upstream, so an undeclared top-level key now draws a named unrecognized_keys refusal. Because no assertion depended on the sentence, it survived the pin bump reading as current — the exact shape that nearly made a seat implement a no-op once before.

Disposition follows the sibling that already models it, recordHighlightsInputs.spec-parity.test.ts (PR #4671): state the pin-independent verdict, note that the contract expresses it two ways depending on the installed pin, and point at specRefusesUnknownTopLevelKeys as the behavioural probe instead of narrating a mode as fact.

Premise re-measured on today's origin/main

Both blocks still narrated strip mode at base 1939c9610, so premise_still_valid: true for both halves. Neither had been corrected by anything in flight.

Before / after

packages/plugin-detail/src/__tests__/recordRelatedListInputs.spec-parity.test.ts (~line 100)

Before:

    // A KEY-reachability claim, so the criterion is that the key SURVIVES the
    // parse rather than merely that the parse succeeds. `RecordRelatedListProps`
    // is a strip-mode `z.object`: an UNDECLARED key also parses green, it is just
    // silently gone from `data` afterwards — which is exactly how this gap stayed
    // invisible for as long as it did.

After:

    // A KEY-reachability claim, so the criterion is that the key SURVIVES the
    // parse rather than merely that the parse succeeds. The verdict behind that
    // choice holds on every pin — an UNDECLARED top-level key is not an authoring
    // surface — but the contract states it two ways depending on the installed
    // `@objectstack/spec`: the closed props schemas refuse it with a named
    // `unrecognized_keys`, while the older strip-mode `z.object`s parsed it green
    // and dropped it from `data` in silence, which is exactly how this gap stayed
    // invisible for as long as it did. Key survival is the criterion that reads
    // the same either way; `success === true` does not, because on a stripping
    // pin an undeclared key gets that too. For the behavioural probe of which way
    // the installed pin says it, see `specRefusesUnknownTopLevelKeys` in the
    // sibling `recordHighlightsInputs.spec-parity.test.ts` — strictness is per
    // schema, so that probe is the shape to copy, not evidence about this one.

packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts (~line 278)

Before:

    // A KEY-reachability claim, so the criterion is that the key SURVIVES the
    // parse. These props schemas are strip-mode `z.object`s: an undeclared key
    // is dropped from `data` with no error at all, which is exactly why the gap
    // was silent — so "it is still there afterwards" is the proof, not
    // `success === true` (which an undeclared key also gets).

After:

    // A KEY-reachability claim, so the criterion is that the key SURVIVES the
    // parse. The verdict behind that choice is the same on every pin — an
    // undeclared top-level key is not an authoring surface — while the contract
    // states it two ways depending on the installed `@objectstack/spec`: the
    // closed props schemas refuse it with a named `unrecognized_keys` (the
    // top-level twin of `specRefusesUnknownSectionKeys` above), where the older
    // strip-mode `z.object`s dropped it from `data` with no error at all, which
    // is exactly why the gap was silent. So "it is still there afterwards" is the
    // proof on both, and `success === true` is not — on a stripping pin an
    // undeclared key gets that too. Probed behaviourally rather than off a
    // version string: `specRefusesUnknownTopLevelKeys` in the sibling
    // `recordHighlightsInputs.spec-parity.test.ts` models the shape, and
    // strictness is per schema, so it is the pattern rather than a reading of
    // this one.

This also settles the card's second observation — recordDetailsInputs disagreeing with itself. Its section-key block was already pin-aware and asserts unrecognized_keys; its top-level block forty lines down still narrated strip mode. The two now read consistently, and the top-level block names the section-level probe as its twin.

No version literal was written into the new prose: it says "the installed pin", never a number.

The claim is measured, not asserted

Prose-only, so no ablation is available — there is no assertion whose removal could turn anything red, which is precisely why the stale sentence survived. Stating that plainly rather than staging a decorative reverse-verification.

What stands in for it: the new prose makes a behavioural claim, so that claim was probed against the installed pin — an undeclared top-level key fed to each props schema, with a declared-key control that must parse green.

=== RecordRelatedListProps (file A, ~line 100) ===
  control(declared keys only).success = true
  probe(+undeclared top-level key).success = false
  MODE = strict; issue codes = ["unrecognized_keys"]
  unrecognized_keys names = ["__objectui_4918_probe__"]

=== RecordDetailsProps (file B, ~line 278) ===
  control(declared keys only).success = true
  probe(+undeclared top-level key).success = false
  MODE = strict; issue codes = ["unrecognized_keys"]
  unrecognized_keys names = ["__objectui_4918_probe__"]

The control is the load-bearing half: a bare "it failed" would also be satisfied by a rejection of the declared keys, which is the half that must stay valid. Both controls parse green, and the refusal names the probe key and only the probe key.

No assertion moved

$ git diff -U0 | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-][[:space:]]*//'
(none — every changed line is a // comment)

 .../recordDetailsInputs.spec-parity.test.ts     | 17 +++++++++++++----
 .../recordRelatedListInputs.spec-parity.test.ts | 16 ++++++++++++----
 2 files changed, 25 insertions(+), 8 deletions(-)

Verification

All legs below ran against the final commit, 0c28940e6. Heavy legs went through the shared verify lock.

$ pnpm exec vitest run packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts \
    packages/plugin-detail/src/__tests__/recordRelatedListInputs.spec-parity.test.ts \
    scripts/__tests__/doc-version-claims.test.ts --maxWorkers=2
 Test Files  3 passed (3)
      Tests  41 passed (41)
os-verify-lock: VERDICT command-exit 0 · held the lock 10s · waited 142s (2m22s)

$ pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build
os-verify-lock: VERDICT command-exit 0 · held the lock 70s (1m10s) · waited 17s

$ pnpm --filter @object-ui/plugin-detail type-check
> @object-ui/plugin-detail@17.6.0 type-check
> tsc --noEmit && tsc -p tsconfig.test.json
os-verify-lock: VERDICT command-exit 0 · held the lock 16s · waited 394s (6m34s)

$ node scripts/check-changeset-presence.mjs          # EXIT=0
✅  2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
    Every one of them has an EMPTY frontmatter — declared as releasing nothing.

$ node scripts/check-changeset-no-major.mjs          # EXIT=0
✅  No changeset declares a `major` bump.

$ node scripts/check-control-bytes.mjs               # EXIT=0
✅  check-control-bytes: OK (scanned 4891 tracked text file(s); skipped 85 binary).

$ node scripts/check-changeset-fixed.mjs             # EXIT=0
✅  All workspace packages are in the changeset fixed group.

$ node scripts/check-spec-symbol-derivation.mjs      # EXIT=0
✅  spec symbol derivation: 1300 files scanned against 4959 spec export names

Exit codes were captured before any pipe (cmd > out 2>&1; EXIT=$?; tail out), and each line quoted above is the gate's own printed verdict, not a bare $?.

The changeset is empty-frontmatter: comment-only, releases nothing.

Declared lint narrowing

Linted the changed files rather than sweeping the repo (the whole-repo sweep is CI's run, and holding the shared lock for it would starve the queue). The narrowing is measured, with all three pieces:

  1. Population read from eslint's own config, not guessed: of the 3 changed files, eslint reports the .changeset/*.md as File ignored because no matching configuration was supplied — it is outside the linted population by the config's own account. The 2 .ts files are in it.
  2. Count read from --format json: 3 entries returned, 2 linted, 0 errors / 0 warnings on both, ESLINT_EXIT=0.
  3. Invariance for untouched files: this repo's eslint.config.js sets no project / projectService, so linting is not type-aware — a comment-only edit inside two test files cannot move the verdict on any file it does not touch. Confirmed by grep against the config.

Gate re-derivation: scripts/pm/dispatch-gates.mjs is a framework-repo tool with no objectui counterpart, so the set was re-derived by hand over the actual changed paths — which added check-changeset-fixed and check-spec-symbol-derivation (the new prose names spec export symbols) beyond the dispatched list. Both green above.

One judgement call left open for the PM

The card and the triage comment both flag it: should the recordDetailsInputs top-level block also grow a pin-aware probe (specRefusesUnknownTopLevelKeys), rather than only losing the stale sentence? Triage leaned yes. This dispatch was scoped prose-only with an explicit bar on assertion changes, so it is not done here and is reported rather than silently absorbed — a prose card quietly growing an assertion is the shape this repo keeps paying for.

Worth knowing for that decision: neither of these two files can borrow the sibling's probe as evidence. Strictness is per schema — recordDetailsInputs says so itself at its section-level probe — so a real top-level probe here would have to be a new constant per file, roughly the four lines the sibling spends. The new prose is written to stay correct either way: it names the sibling's probe as the shape to copy, not as a reading of these schemas.


Generated by Claude Code

…c-parity tests

Both blocks stated strip mode as a present-tense fact about the installed
`@objectstack/spec`: an undeclared top-level key parses green and is silently
dropped from `data`. The props schemas were closed upstream, so an undeclared
key now draws a named `unrecognized_keys` refusal. No assertion depended on the
sentence, so it survived the pin bump reading as current.

Both now state the pin-independent verdict (an undeclared top-level key is never
authoring surface), note that the contract expresses it two ways depending on
the installed pin, and point at `specRefusesUnknownTopLevelKeys` in the sibling
`recordHighlightsInputs.spec-parity.test.ts` as the behavioural probe. This also
settles `recordDetailsInputs` disagreeing with itself: its section-key block was
already pin-aware while its top-level block was not.

Comment-only. No assertion changed meaning.

Part of #4918

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3229.4 KB 3990.2 KB
Main entry chunk (gzip) 153.4 KB 350 KB
Entry file index-ByA44buf.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) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
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) 5.13KB 2.35KB
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) 504.16KB 114.08KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 186.91KB 44.22KB
plugin-dashboard (index.js) 133.35KB 34.44KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 242.34KB 60.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.80KB 27.20KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
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 (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
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

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

Projects

None yet

2 participants